From f64c279049b2c334fb2651812de701f6e6bf16bc Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 16:15:50 +1100 Subject: [PATCH 01/52] drm: apple: isolate an experimental external H17P transport Add a separate external H17P method profile and compact service serializer for bounded trials. Keep the existing internal H17P send rejection and boot-mode inheritance unchanged. Restrict the new serializer to the existing DisplayPort and EDID service calls, validate ring occupancy, and reject unknown callbacks without fabricating success. The compact transmit header and existing command descriptor are protocol hypotheses derived from public interfaces. No hardware compatibility is claimed. This commit does not enable or match an external controller. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/afk.c | 130 +++++++++++++++++++------ drivers/gpu/drm/apple/afk.h | 2 + drivers/gpu/drm/apple/dcp-internal.h | 3 + drivers/gpu/drm/apple/dcp.c | 10 ++ drivers/gpu/drm/apple/dcp.h | 2 + drivers/gpu/drm/apple/dptxep.c | 6 +- drivers/gpu/drm/apple/iomfb.c | 19 +++- drivers/gpu/drm/apple/iomfb_template.c | 8 +- 8 files changed, 148 insertions(+), 32 deletions(-) diff --git a/drivers/gpu/drm/apple/afk.c b/drivers/gpu/drm/apple/afk.c index ff1f0f57afd9f..8161db7eb440f 100644 --- a/drivers/gpu/drm/apple/afk.c +++ b/drivers/gpu/drm/apple/afk.c @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -572,8 +573,10 @@ static void afk_recv_handle_std_service(struct apple_dcp_afkep *ep, u32 channel, dev_err(ep->dcp->dev, "AFK[ep:%02x]: channel %d received unhandled standard service message: %x / %x\n", ep->endpoint, channel, type, eshdr->category); - print_hex_dump(KERN_INFO, "AFK: ", DUMP_PREFIX_NONE, 16, 1, payload, - payload_size, true); + if (ep->dcp->hw.iomfb_method_profile != + DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) + print_hex_dump(KERN_INFO, "AFK: ", DUMP_PREFIX_NONE, 16, 1, + payload, payload_size, true); } static bool afk_validate_h17p_header(const u8 *data, size_t size) @@ -645,6 +648,16 @@ static void afk_recv_handle(struct apple_dcp_afkep *ep, u32 channel, u32 type, payload_size = data_size - hdr_len; trace_afk_recv_handle(ep, channel, type, data_size, ehdr, eshdr); + if (ep->dcp->hw.iomfb_method_profile == + DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL && + ep->experimental_rx_logs < 32) { + ep->experimental_rx_logs++; + dev_info(ep->dcp->dev, + "external AFK ep=%u channel=%u type=%u subtype=%u category=%u bytes=%zu\n", + ep->endpoint, channel, type, subtype, eshdr->category, + data_size); + } + service = afk_epic_find_service(ep, channel); @@ -698,8 +711,10 @@ static void afk_recv_handle(struct apple_dcp_afkep *ep, u32 channel, u32 type, dev_err(ep->dcp->dev, "AFK[ep:%02x]: channel %d received unhandled message " "(type %x subtype %x)\n", ep->endpoint, channel, type, subtype); - print_hex_dump(KERN_INFO, "AFK: ", DUMP_PREFIX_NONE, 16, 1, payload, - payload_size, true); + if (ep->dcp->hw.iomfb_method_profile != + DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) + print_hex_dump(KERN_INFO, "AFK: ", DUMP_PREFIX_NONE, 16, 1, + payload, payload_size, true); return; too_small: @@ -878,6 +893,8 @@ int afk_send_epic(struct apple_dcp_afkep *ep, u32 channel, u16 tag, struct epic_sub_hdr *eshdr; unsigned long flags; size_t total_epic_size, total_size; + bool compact = ep->dcp->hw.iomfb_method_profile == + DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL; int ret; /* @@ -885,30 +902,73 @@ int afk_send_epic(struct apple_dcp_afkep *ep, u32 channel, u16 tag, * 8-byte sub-header, with the service channel carried in the EPIC * header on queue channel 0. Only the receive side of that format is * known, and the 24-byte framing below would be misparsed, so send - * nothing. The display path needs no AP-initiated EPIC traffic: the + * nothing for the established internal profile. The external-only + * experiment below tests receive-layout symmetry. Internal callers are: the * callers are optional (verbose firmware logging, EDID copy, audio, * DPTX, debugfs) or replies to calls only DPTX services make. */ - if (ep->dcp->fw_compat == DCP_FIRMWARE_H17P) { + if (ep->dcp->fw_compat == DCP_FIRMWARE_H17P && !compact) { dev_dbg_once(ep->dcp->dev, "AFK[ep:%02x]: not sending EPIC message on H17P\n", ep->endpoint); return -EOPNOTSUPP; } + /* Only existing DPTX/EDID service commands participate in this trial. */ + if (compact && + (channel > U16_MAX || payload_len > SZ_1M || + (payload_len && !payload) || stype != EPIC_SUBTYPE_STD_SERVICE || + (ep->endpoint != DPTX_ENDPOINT && ep->endpoint != DPAVSERV_ENDPOINT) || + !((ecat == EPIC_CAT_COMMAND && etype == EPIC_TYPE_COMMAND && + payload_len == sizeof(struct epic_cmd)) || + (ecat == EPIC_CAT_REPLY && + (etype == EPIC_TYPE_REPLY || etype == EPIC_TYPE_NOTIFY_ACK))))) + return -EOPNOTSUPP; + spin_lock_irqsave(&ep->lock, flags); if (ep->stopping) { ret = -ESHUTDOWN; goto out; } + if (compact && (!ep->txbfr.ready || !ep->txbfr.rptr || + !ep->txbfr.wptr || !ep->txbfr.buf)) { + ret = -ESHUTDOWN; + goto out; + } + dma_rmb(); rptr = le32_to_cpu(*ep->txbfr.rptr); wptr = le32_to_cpu(*ep->txbfr.wptr); trace_afk_send_rwptr_pre(ep, rptr, wptr); - total_epic_size = sizeof(*ehdr) + sizeof(*eshdr) + payload_len; + total_epic_size = sizeof(*ehdr) + + (compact ? sizeof(struct epic_sub_hdr_h17p) : sizeof(*eshdr)) + + payload_len; total_size = sizeof(*hdr) + total_epic_size; + if (compact) { + u32 available, needed; + + if (!ep->txbfr.ready || !ep->txbfr.stride || + (ep->txbfr.stride & (ep->txbfr.stride - 1)) || + rptr >= ep->txbfr.bufsz || wptr >= ep->txbfr.bufsz || + !IS_ALIGNED(rptr, ep->txbfr.stride) || + !IS_ALIGNED(wptr, ep->txbfr.stride) || + total_size >= ep->txbfr.bufsz) { + ret = -EPROTO; + goto out; + } + available = rptr > wptr ? rptr - wptr : ep->txbfr.bufsz - wptr + rptr; + needed = ALIGN(total_size, ep->txbfr.stride); + if (total_size > ep->txbfr.bufsz - wptr) + needed += ep->txbfr.bufsz - wptr; + /* Retain an empty slot; equal read/write pointers mean empty. */ + if (needed >= available) { + ret = -ENOSPC; + goto out; + } + } + hdr = hdr2 = NULL; /* @@ -989,31 +1049,45 @@ int afk_send_epic(struct apple_dcp_afkep *ep, u32 channel, u16 tag, hdr->magic = cpu_to_le32(QE_MAGIC); hdr->size = cpu_to_le32(total_epic_size); - hdr->channel = cpu_to_le32(channel); + hdr->channel = cpu_to_le32(compact ? 0 : channel); hdr->type = cpu_to_le32(etype); if (hdr2) memcpy(hdr2, hdr, sizeof(*hdr)); - ehdr = ep->txbfr.buf + wptr; - memset(ehdr, 0, sizeof(*ehdr)); - ehdr->version = 2; - ehdr->seq = cpu_to_le16(ep->qe_seq++); - ehdr->timestamp = cpu_to_le64(0); - wptr += sizeof(*ehdr); - - eshdr = ep->txbfr.buf + wptr; - memset(eshdr, 0, sizeof(*eshdr)); - eshdr->length = cpu_to_le32(payload_len); - eshdr->version = 4; - eshdr->category = ecat; - eshdr->type = cpu_to_le16(stype); - eshdr->timestamp = cpu_to_le64(0); - eshdr->tag = cpu_to_le16(tag); - if (ecat == EPIC_CAT_REPLY) - eshdr->inline_len = cpu_to_le32(payload_len - 4); - else - eshdr->inline_len = cpu_to_le32(0); - wptr += sizeof(*eshdr); + if (compact) { + u8 *wire = ep->txbfr.buf + wptr; + + /* Experimental symmetry with the published H17P receive layout. */ + memset(wire, 0, sizeof(*ehdr) + sizeof(struct epic_sub_hdr_h17p)); + wire[0] = ep->qe_seq++; + put_unaligned_le16(channel, wire + 2); + put_unaligned_le32(total_epic_size - 8, wire + 4); + wire[16] = stype; + wire[17] = ecat; + put_unaligned_le32(tag, wire + 20); + wptr += sizeof(*ehdr) + sizeof(struct epic_sub_hdr_h17p); + } else { + ehdr = ep->txbfr.buf + wptr; + memset(ehdr, 0, sizeof(*ehdr)); + ehdr->version = 2; + ehdr->seq = cpu_to_le16(ep->qe_seq++); + ehdr->timestamp = cpu_to_le64(0); + wptr += sizeof(*ehdr); + + eshdr = ep->txbfr.buf + wptr; + memset(eshdr, 0, sizeof(*eshdr)); + eshdr->length = cpu_to_le32(payload_len); + eshdr->version = 4; + eshdr->category = ecat; + eshdr->type = cpu_to_le16(stype); + eshdr->timestamp = cpu_to_le64(0); + eshdr->tag = cpu_to_le16(tag); + if (ecat == EPIC_CAT_REPLY) + eshdr->inline_len = cpu_to_le32(payload_len - 4); + else + eshdr->inline_len = cpu_to_le32(0); + wptr += sizeof(*eshdr); + } memcpy(ep->txbfr.buf + wptr, payload, payload_len); wptr += payload_len; diff --git a/drivers/gpu/drm/apple/afk.h b/drivers/gpu/drm/apple/afk.h index 0cef14fa6dfe8..8eb797e12ef07 100644 --- a/drivers/gpu/drm/apple/afk.h +++ b/drivers/gpu/drm/apple/afk.h @@ -291,6 +291,8 @@ struct afk_ringbuffer { struct apple_dcp_afkep { struct apple_dcp *dcp; + /* Bounded external protocol observations, owned by the RX worker. */ + u8 experimental_rx_logs; u32 endpoint; struct workqueue_struct *wq; diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index 8be2f4f267cfa..3f4a5620999e3 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -174,6 +174,7 @@ struct dcp_panel { enum dcp_iomfb_method_profile { DCP_IOMFB_METHODS_DEFAULT, DCP_IOMFB_METHODS_H17G, + DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL, }; struct apple_dcp_hw_data { @@ -208,6 +209,8 @@ struct apple_dcp { struct apple_connector *connector; struct apple_dcp_hw_data hw; + /* Protected by the H17P IOMFB receive mutex. */ + u8 experimental_callback_logs; /* firmware version and compatible firmware version */ enum dcp_firmware_version fw_compat; diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 9228483aed8d6..8cccb95efdfc3 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -130,6 +130,16 @@ static void dcp_release_rtkit(void *data) #define DCP_BOOT_TIMEOUT msecs_to_jiffies(1000) +static bool experimental_h17p_external; +module_param(experimental_h17p_external, bool, 0444); +MODULE_PARM_DESC(experimental_h17p_external, + "Enable experimental T8140 external display protocol"); + +bool dcp_h17p_external_enabled(void) +{ + return experimental_h17p_external; +} + static bool show_notch; module_param(show_notch, bool, 0644); MODULE_PARM_DESC(show_notch, "Use the full display height and shows the notch"); diff --git a/drivers/gpu/drm/apple/dcp.h b/drivers/gpu/drm/apple/dcp.h index a2ef5176f3fee..bbd14f3316e6e 100644 --- a/drivers/gpu/drm/apple/dcp.h +++ b/drivers/gpu/drm/apple/dcp.h @@ -12,6 +12,8 @@ #include "dcp-internal.h" #include "parser.h" +bool dcp_h17p_external_enabled(void); + struct apple_crtc { struct drm_crtc base; struct drm_pending_vblank_event *event; diff --git a/drivers/gpu/drm/apple/dptxep.c b/drivers/gpu/drm/apple/dptxep.c index 11091084f05e2..ec190a7461df9 100644 --- a/drivers/gpu/drm/apple/dptxep.c +++ b/drivers/gpu/drm/apple/dptxep.c @@ -694,7 +694,8 @@ static int dptxport_call(struct apple_epic_service *service, u32 idx, trace_dptxport_apcall(dptx, idx, data_size); dev_dbg(service->ep->dcp->dev, "DPTXPort: APCALL %u (%zu bytes)\n", idx, data_size); - if (data_size) + if (data_size && service->ep->dcp->hw.iomfb_method_profile != + DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) print_hex_dump_debug("DPTXPort: apcall data: ", DUMP_PREFIX_OFFSET, 16, 1, data, min(data_size, (size_t)64), true); @@ -792,6 +793,9 @@ static int dptxport_call(struct apple_epic_service *service, u32 idx, memset(reply, 0, 4); return 0; default: + if (service->ep->dcp->hw.iomfb_method_profile == + DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) + return -EOPNOTSUPP; /* just try to ACK and hope for the best... */ dev_info(service->ep->dcp->dev, "DPTXPort: acking unhandled call %u\n", idx); diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index 8c8cfbb5cc579..ac23f92329f74 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -790,6 +790,14 @@ static void dcpep_handle_cb(struct apple_dcp *dcp, enum dcp_context_id context, } tag = dcp_parse_tag(hdr->tag); + if (dcp->hw.iomfb_method_profile == + DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL && + dcp->experimental_callback_logs < 64) { + dcp->experimental_callback_logs++; + dev_info(dev, "external IOMFB callback=%d in=%u out=%u context=%u\n", + tag, hdr->in_len, hdr->out_len, context); + } + handled = tag >= 0 && tag < IOMFB_MAX_CB && dcp->cb_handlers && dcp->cb_handlers[tag]; if (dcp->fw_compat == DCP_FIRMWARE_H17P && @@ -812,6 +820,13 @@ static void dcpep_handle_cb(struct apple_dcp *dcp, enum dcp_context_id context, !test_and_set_bit(tag, dcp->unknown_callbacks)) dev_warn(dev, "received unknown callback %c%c%c%c\n", hdr->tag[3], hdr->tag[2], hdr->tag[1], hdr->tag[0]); + if (dcp->hw.iomfb_method_profile == + DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) { + /* Retain the failed session without inventing a successful reply. */ + dcp->crashed = true; + complete(&dcp->start_done); + return; + } /* * Leaving a callback unanswered wedges the coprocessor: it * waits for the ack forever and the outer call never returns. @@ -1041,7 +1056,9 @@ static bool dcp_mode_settable(struct apple_dcp *dcp, if (!dcp_mode) return false; - if (dcp->fw_compat == DCP_FIRMWARE_H17P) + if (dcp->fw_compat == DCP_FIRMWARE_H17P && + dcp->hw.iomfb_method_profile != + DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) return dcp_mode->mode.type & DRM_MODE_TYPE_PREFERRED; return true; diff --git a/drivers/gpu/drm/apple/iomfb_template.c b/drivers/gpu/drm/apple/iomfb_template.c index f14031085142f..575b99606e1ac 100644 --- a/drivers/gpu/drm/apple/iomfb_template.c +++ b/drivers/gpu/drm/apple/iomfb_template.c @@ -2194,10 +2194,14 @@ int DCP_FW_NAME(iomfb_modeset)(struct apple_dcp *dcp, dcp->swap_submit_timestamp = 0; - if (DCP_INHERIT_BOOT_MODE) { + if (DCP_INHERIT_BOOT_MODE && + dcp->hw.iomfb_method_profile != + DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) { dev_dbg(dcp->dev, "inheriting the bootloader's display mode\n"); complete_set_digital_out_mode(dcp, NULL, cookie); - } else if (mode->vrr) + } else if (mode->vrr && + dcp->hw.iomfb_method_profile != + DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) dcp_set_adaptive_sync(dcp, crtc_state->vrr_enabled ? mode->min_vrr : 0, cookie); From b65e24146a0ef604d3b1a6ae96c30c39888ee8e9 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 16:15:50 +1100 Subject: [PATCH 02/52] drm: apple: prepare optional external H17P before component matching Add a default-off T8140 external-controller experiment. Require the inherited START, STOPPED and IDLE state before the standard start/wake path and pin its DMA lifetime before any startup write. Complete this optional preparation during platform probe so failed or deferred controllers do not become prerequisites for the internal DRM master. Only successfully bound, prepared controllers are included. Preserve started-session mappings on failure and quiesce retained external work during shutdown. Internal H17P lifecycle remains unchanged. Hardware qualification and validated external resource mappings remain required before this experiment can be used as a supported display path. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/apple_drv.c | 3 +- drivers/gpu/drm/apple/dcp-internal.h | 1 + drivers/gpu/drm/apple/dcp.c | 100 ++++++++++++++++++++++++++- drivers/gpu/drm/apple/dcp.h | 1 + 4 files changed, 101 insertions(+), 4 deletions(-) diff --git a/drivers/gpu/drm/apple/apple_drv.c b/drivers/gpu/drm/apple/apple_drv.c index df31009e3ff6b..3e1cfa1a69b2c 100644 --- a/drivers/gpu/drm/apple/apple_drv.c +++ b/drivers/gpu/drm/apple/apple_drv.c @@ -648,7 +648,8 @@ static const struct of_device_id apple_dcp_id_tbl[] = { static bool apple_dcp_usable(struct device_node *np) { return of_device_is_available(np) && - !of_device_is_compatible(np, "apple,t8140-dcpext"); + (!of_device_is_compatible(np, "apple,t8140-dcpext") || + dcp_h17p_external_ready(np)); } static int apple_drm_init_dcp(struct device *dev) diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index 3f4a5620999e3..f552a89bda977 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -211,6 +211,7 @@ struct apple_dcp { struct apple_dcp_hw_data hw; /* Protected by the H17P IOMFB receive mutex. */ u8 experimental_callback_logs; + bool external_prepared; /* firmware version and compatible firmware version */ enum dcp_firmware_version fw_compat; diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 8cccb95efdfc3..c973d15a7b9c1 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -50,6 +51,9 @@ #define APPLE_DCP_COPROC_CPU_CONTROL 0x44 #define APPLE_DCP_COPROC_CPU_CONTROL_RUN BIT(4) +#define APPLE_DCP_COPROC_CPU_STATUS 0x48 +#define APPLE_DCP_COPROC_CPU_STATUS_STOPPED BIT(1) +#define APPLE_DCP_COPROC_CPU_STATUS_IDLE BIT(5) struct dcp_session_ref { struct list_head link; @@ -80,7 +84,10 @@ static int dcp_pin_live_session(struct apple_dcp *dcp) { struct dcp_session_ref *session; - if (!dcp->hw.adopt_live_session || dcp->fw_compat != DCP_FIRMWARE_H17P || + if ((!dcp->hw.adopt_live_session && + dcp->hw.iomfb_method_profile != + DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) || + dcp->fw_compat != DCP_FIRMWARE_H17P || dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17G || dcp->retain_dma) return 0; session = kzalloc_obj(*session); @@ -140,6 +147,27 @@ bool dcp_h17p_external_enabled(void) return experimental_h17p_external; } +bool dcp_h17p_external_ready(struct device_node *np) +{ + struct platform_device *pdev; + struct apple_dcp *dcp; + bool ready = false; + + if (!dcp_h17p_external_enabled()) + return false; + pdev = of_find_device_by_node(np); + if (!pdev) + return false; + /* Failed and deferred probes must never become required components. */ + if (device_is_bound(&pdev->dev)) { + dcp = platform_get_drvdata(pdev); + ready = dcp && READ_ONCE(dcp->external_prepared) && + !READ_ONCE(dcp->quiescing) && !READ_ONCE(dcp->crashed); + } + put_device(&pdev->dev); + return ready; +} + static bool show_notch; module_param(show_notch, bool, 0644); MODULE_PARM_DESC(show_notch, "Use the full display height and shows the notch"); @@ -3286,6 +3314,17 @@ static void dcp_enable_typec_work(struct apple_dcp *dcp) mod_delayed_work(system_freezable_wq, &dcp->typec_reconnect_wq, 0); } +static bool dcp_external_is_idle(struct apple_dcp *dcp) +{ + u32 control = readl_relaxed(dcp->coproc_reg + APPLE_DCP_COPROC_CPU_CONTROL); + u32 status = readl_relaxed(dcp->coproc_reg + APPLE_DCP_COPROC_CPU_STATUS); + + /* Same externally inherited idle state accepted by the public loader. */ + return (control & APPLE_DCP_COPROC_CPU_CONTROL_RUN) && + (status & APPLE_DCP_COPROC_CPU_STATUS_STOPPED) && + (status & APPLE_DCP_COPROC_CPU_STATUS_IDLE); +} + static int dcp_comp_bind(struct device *dev, struct device *main, void *data) { struct device_node *panel_np; @@ -3297,6 +3336,9 @@ static int dcp_comp_bind(struct device *dev, struct device *main, void *data) return dev_err_probe(dev, -EBUSY, "Live DCP session cannot be rebound; reboot required\n"); + if (dcp->external_prepared) + return 0; + /* A timed-out prior session may still scan these mappings. */ mutex_lock(&dcp->swapped_out_fbs_lock); ret = list_empty(&dcp->swapped_out_fbs) ? 0 : -EBUSY; @@ -3327,6 +3369,12 @@ static int dcp_comp_bind(struct device *dev, struct device *main, void *data) "DCP was not left running by the bootloader\n"); } + if (dcp->hw.iomfb_method_profile == + DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL && + !dcp_external_is_idle(dcp)) + return dev_err_probe(dev, -EBUSY, + "External DCP is not in the inherited idle state\n"); + 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); @@ -3407,6 +3455,19 @@ static int dcp_comp_bind(struct device *dev, struct device *main, void *data) INIT_WORK(&dcp->vblank_wq, dcp_delayed_vblank); INIT_DELAYED_WORK(&dcp->swap_watchdog_wq, dcp_swap_watchdog); + if (dcp->hw.iomfb_method_profile == + DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) { + /* Pin before the first write or mailbox wake can expose DMA. */ + ret = dcp_pin_live_session(dcp); + if (ret) + goto err_piodma; + if (!dcp_external_is_idle(dcp)) { + ret = dev_err_probe(dev, -EBUSY, + "External DCP changed state before startup\n"); + goto err_piodma; + } + } + if (!dcp->hw.adopt_live_session) { cpu_ctrl = readl_relaxed(dcp->coproc_reg + APPLE_DCP_COPROC_CPU_CONTROL); @@ -3557,7 +3618,8 @@ static int dcp_platform_probe(struct platform_device *pdev) return dev_err_probe(dev, -EBUSY, "Previous live DCP session requires a reboot\n"); /* Not part of the display subsystem yet; see apple_dcp_usable(). */ - if (of_device_is_compatible(dev->of_node, "apple,t8140-dcpext")) { + if (of_device_is_compatible(dev->of_node, "apple,t8140-dcpext") && + !dcp_h17p_external_enabled()) { dev_info(dev, "external display coprocessor not supported yet\n"); return -ENODEV; } @@ -3759,7 +3821,24 @@ static int dcp_platform_probe(struct platform_device *pdev) if (ret) return ret; - return component_add(&pdev->dev, &dcp_comp_ops); + if (dcp->hw.iomfb_method_profile == + DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) { + /* Complete optional startup before the DRM master can depend on it. */ + ret = dcp_comp_bind(dev, NULL, NULL); + if (ret) { + dcp_disable_typec_work(dcp, true); + disable_delayed_work_sync(&dcp->typec_fabric_retrain_wq); + return ret; + } + WRITE_ONCE(dcp->external_prepared, true); + } + + ret = component_add(&pdev->dev, &dcp_comp_ops); + if (ret && dcp->external_prepared) { + dcp_disable_typec_work(dcp, true); + disable_delayed_work_sync(&dcp->typec_fabric_retrain_wq); + } + return ret; } static void dcp_platform_remove(struct platform_device *pdev) @@ -3771,6 +3850,12 @@ static void dcp_platform_shutdown(struct platform_device *pdev) { struct apple_dcp *dcp = platform_get_drvdata(pdev); + if (dcp && dcp->retain_dma && dcp->hw.iomfb_method_profile == + DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) { + dcp_comp_unbind(&pdev->dev, NULL, NULL); + return; + } + if (dcp && dcp->hw.adopt_live_session && dcp->fw_compat == DCP_FIRMWARE_H17P && dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17G) { @@ -3909,6 +3994,14 @@ static const struct apple_dcp_hw_data apple_dcp_hw_t8140 = { .firmware_request = 0x68000, }; +/* Separate from the established internal-panel H17P profile. */ +static const struct apple_dcp_hw_data apple_dcp_hw_t8140_external = { + .num_dptx_ports = 1, + .iomfb_method_profile = DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL, + .adopt_live_session = false, + .firmware_compat = DCP_FIRMWARE_H17P, +}; + /* * M5 (T8142) runs H17-generation DCP firmware with the H17G method numbering * and is left running by the bootloader. @@ -3932,6 +4025,7 @@ 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,t8140-dcp", .data = &apple_dcp_hw_t8140, }, + { .compatible = "apple,t8140-dcpext", .data = &apple_dcp_hw_t8140_external, }, { .compatible = "apple,t8142-dcp", .data = &apple_dcp_hw_t8142, }, { .compatible = "apple,dcp", .data = &apple_dcp_hw_dcp, }, { .compatible = "apple,dcpext", .data = &apple_dcp_hw_dcpext, }, diff --git a/drivers/gpu/drm/apple/dcp.h b/drivers/gpu/drm/apple/dcp.h index bbd14f3316e6e..df1e46c439697 100644 --- a/drivers/gpu/drm/apple/dcp.h +++ b/drivers/gpu/drm/apple/dcp.h @@ -13,6 +13,7 @@ #include "parser.h" bool dcp_h17p_external_enabled(void); +bool dcp_h17p_external_ready(struct device_node *np); struct apple_crtc { struct drm_crtc base; From c67346da36d614fd267714dbc634d7489e58dce0 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 16:18:18 +1100 Subject: [PATCH 03/52] drm: apple: drain the work items present in the pinned base The optional external error path referenced a retrain work item absent from this source base. Use dcp_disable_typec_work(), which already blocks and drains both existing Type-C work items. Preserve the initial compilation failure for review. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/dcp.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index c973d15a7b9c1..96ce0e741c055 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -3827,7 +3827,6 @@ static int dcp_platform_probe(struct platform_device *pdev) ret = dcp_comp_bind(dev, NULL, NULL); if (ret) { dcp_disable_typec_work(dcp, true); - disable_delayed_work_sync(&dcp->typec_fabric_retrain_wq); return ret; } WRITE_ONCE(dcp->external_prepared, true); @@ -3836,7 +3835,6 @@ static int dcp_platform_probe(struct platform_device *pdev) ret = component_add(&pdev->dev, &dcp_comp_ops); if (ret && dcp->external_prepared) { dcp_disable_typec_work(dcp, true); - disable_delayed_work_sync(&dcp->typec_fabric_retrain_wq); } return ret; } From 40a9c929c55f2b13c39e732fc2ae3f10e1506634 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 16:20:49 +1100 Subject: [PATCH 04/52] arm64: dts: apple: describe disabled J700 external resources Describe the external display controller and four display apertures plus its fifth clock aperture from bounded read-only ADT observations. Select the first observed DCP mapper and the observed PIODMA stream. Add the reciprocal rear Type-C graph while retaining disabled controller and route status. Register names follow existing public bindings. Mailbox, firmware handoff, routing and bandwidth integration remain qualification requirements. This resource description is not a display readiness claim. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- .../boot/dts/apple/t8140-j700-dcpext.dtsi | 69 +++++++++++++++++++ arch/arm64/boot/dts/apple/t8140-j700.dts | 3 + 2 files changed, 72 insertions(+) create mode 100644 arch/arm64/boot/dts/apple/t8140-j700-dcpext.dtsi diff --git a/arch/arm64/boot/dts/apple/t8140-j700-dcpext.dtsi b/arch/arm64/boot/dts/apple/t8140-j700-dcpext.dtsi new file mode 100644 index 0000000000000..7f3adb838473f --- /dev/null +++ b/arch/arm64/boot/dts/apple/t8140-j700-dcpext.dtsi @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: GPL-2.0+ OR MIT +/* + * T8140 external display resources measured through the public ADT API. + * Generated-by: LLM + * Assisted-by: LLM + * + * The controller and route remain disabled. Their live firmware + * profile, bandwidth request, routing selector and handoff must be qualified + * before enabling the controller. The existing T8140 refusal remains in force. + */ + +&{/soc} { + dcpext: dcp@416e00000 { + compatible = "apple,t8140-dcpext"; + reg = <0x4 0x16e00000 0x0 0x4000>, + <0x4 0x14000000 0x0 0xafc000>, + <0x4 0x16240000 0x0 0x10000>, + <0x4 0x163ac000 0x0 0x4000>, + <0x4 0x16800000 0x0 0x800000>, + <0x3 0x02800000 0x0 0xbc000>, + <0x4 0x16850000 0x0 0x4000>; + reg-names = "coproc", "disp-0", "disp-1", "disp-2", + "disp-3", "disp-4", "iop-vbar"; + mboxes = <&dcpext_mbox>; + mbox-names = "mbox"; + /* First mapper in the measured ADT iommu-parent list. */ + iommus = <&dcpext0_dart 23>; + power-domains = <&ps_dispext0_cpu>; + apple,connector-type = "USB-C"; + status = "disabled"; + + #address-cells = <2>; + #size-cells = <2>; + ranges; + + dispext0_piodma: piodma { + iommus = <&dispext0_dart 16>; + }; + + typec-routes { + #address-cells = <1>; + #size-cells = <0>; + + route@0 { + reg = <0>; + status = "disabled"; + mode-switch; + + port { + dcpext_typec0: endpoint { + remote-endpoint = <&typec0_dcp>; + }; + }; + }; + }; + }; +}; + +&typec0 { + ports { + port@3 { + reg = <3>; + + typec0_dcp: endpoint { + remote-endpoint = <&dcpext_typec0>; + }; + }; + }; +}; diff --git a/arch/arm64/boot/dts/apple/t8140-j700.dts b/arch/arm64/boot/dts/apple/t8140-j700.dts index 842e1b2bcf572..5b102fe5fcbba 100644 --- a/arch/arm64/boot/dts/apple/t8140-j700.dts +++ b/arch/arm64/boot/dts/apple/t8140-j700.dts @@ -1918,3 +1918,6 @@ local-mac-address = [00 00 00 00 00 00]; }; }; + +/* External resources stay disabled pending the separate firmware profile. */ +#include "t8140-j700-dcpext.dtsi" From 98038492dc481d784c76fb2c52f3dd030dfad8f5 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 16:30:45 +1100 Subject: [PATCH 05/52] drm: apple: recognize the T8140 external binding in the display master Use the generic DCP fallback required by the published T8140 external binding and explicitly classify that specific compatible as an external pipeline. Otherwise the display master never discovers a successfully prepared external node. The device and route remain disabled by default. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/boot/dts/apple/t8140-j700-dcpext.dtsi | 2 +- drivers/gpu/drm/apple/apple_drv.c | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/arch/arm64/boot/dts/apple/t8140-j700-dcpext.dtsi b/arch/arm64/boot/dts/apple/t8140-j700-dcpext.dtsi index 7f3adb838473f..cb3e7ea4ebdc8 100644 --- a/arch/arm64/boot/dts/apple/t8140-j700-dcpext.dtsi +++ b/arch/arm64/boot/dts/apple/t8140-j700-dcpext.dtsi @@ -11,7 +11,7 @@ &{/soc} { dcpext: dcp@416e00000 { - compatible = "apple,t8140-dcpext"; + compatible = "apple,t8140-dcpext", "apple,dcp"; reg = <0x4 0x16e00000 0x0 0x4000>, <0x4 0x14000000 0x0 0xafc000>, <0x4 0x16240000 0x0 0x10000>, diff --git a/drivers/gpu/drm/apple/apple_drv.c b/drivers/gpu/drm/apple/apple_drv.c index 3e1cfa1a69b2c..eda6ea33fa225 100644 --- a/drivers/gpu/drm/apple/apple_drv.c +++ b/drivers/gpu/drm/apple/apple_drv.c @@ -667,6 +667,7 @@ static int apple_drm_init_dcp(struct device *dev) if (!apple_dcp_usable(np)) continue; dcp_ext = of_device_is_compatible(np, "apple,dcpext") || + of_device_is_compatible(np, "apple,t8140-dcpext") || of_property_present(np, "phys"); dcp[num_dcp] = of_find_device_by_node(np); From 205a56d33cb07184d7dfe17ab8aba0996cff3efe Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 16:31:34 +1100 Subject: [PATCH 06/52] arm64: dts: apple: retain the inherited external display supply Keep the external CPU supply on after the loader validates and publishes its power-domain chain. The supply remains disabled in the static tree, so a refused handoff does not register or power it. This preserves firmware ownership while allowing mailbox consumers to bind. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/boot/dts/apple/t8140-j700-dcpext.dtsi | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/arch/arm64/boot/dts/apple/t8140-j700-dcpext.dtsi b/arch/arm64/boot/dts/apple/t8140-j700-dcpext.dtsi index cb3e7ea4ebdc8..6a6538dc6dba6 100644 --- a/arch/arm64/boot/dts/apple/t8140-j700-dcpext.dtsi +++ b/arch/arm64/boot/dts/apple/t8140-j700-dcpext.dtsi @@ -67,3 +67,8 @@ }; }; }; + +/* Retain the inherited firmware supply once the loader publishes it. */ +&ps_dispext0_cpu { + apple,always-on; +}; From 24c20cf0924ec8a4096cb41d013713edbbdd88c2 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 16:40:04 +1100 Subject: [PATCH 07/52] drm: apple: validate external display clock and bandwidth resources Separate the number of display windows from the shared firmware clock aperture. Preserve the internal five-window profile and validate the external four-window profile against its existing final clock resource. Require explicit read-only experimental bandwidth offsets for the external profile, with alignment, distinctness and aperture bounds checks before startup. Keep both offsets unset by default and reject missing external clock rates rather than guessing them. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/dcp-external-bw.h | 24 ++++++++ drivers/gpu/drm/apple/dcp-internal.h | 1 + drivers/gpu/drm/apple/dcp.c | 56 ++++++++++++++++++- .../drivers/gpu/test_t8140_external_bw.py | 45 +++++++++++++++ 4 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 drivers/gpu/drm/apple/dcp-external-bw.h create mode 100644 tools/testing/selftests/drivers/gpu/test_t8140_external_bw.py diff --git a/drivers/gpu/drm/apple/dcp-external-bw.h b/drivers/gpu/drm/apple/dcp-external-bw.h new file mode 100644 index 0000000000000..439db008b5084 --- /dev/null +++ b/drivers/gpu/drm/apple/dcp-external-bw.h @@ -0,0 +1,24 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ +/* Copyright 2026 Aurora Silicon */ +#ifndef APPLE_DCP_EXTERNAL_BW_H +#define APPLE_DCP_EXTERNAL_BW_H + +#include + +static inline bool apple_dcp_external_bw_valid(u64 size, int scratch, int request) +{ + if (size < sizeof(u32) || scratch < 0 || request < 0) + return false; + if ((scratch & 3) || (request & 3) || scratch == request) + return false; + return (u64)scratch <= size - sizeof(u32) && + (u64)request <= size - sizeof(u32); +} + +static inline bool apple_dcp_external_clock_window(u64 start, u64 size, + u64 expected_start, u64 expected_size) +{ + return start == expected_start && size == expected_size; +} + +#endif diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index f552a89bda977..ba790968a2dd1 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -188,6 +188,7 @@ struct apple_dcp_hw_data { enum dcp_firmware_version firmware_compat; /* Firmware-visible clock aperture, independent of the AP PMGR window. */ const struct resource *firmware_clock; + u32 firmware_display_windows; u32 firmware_scratch; u32 firmware_request; }; diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 96ce0e741c055..3f60b70042202 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -45,6 +45,7 @@ #include "av.h" #include "dcp.h" #include "dcp-internal.h" +#include "dcp-external-bw.h" #include "iomfb.h" #include "parser.h" #include "trace.h" @@ -142,6 +143,16 @@ module_param(experimental_h17p_external, bool, 0444); MODULE_PARM_DESC(experimental_h17p_external, "Enable experimental T8140 external display protocol"); +static int experimental_h17p_external_bw_scratch = -1; +module_param(experimental_h17p_external_bw_scratch, int, 0444); +MODULE_PARM_DESC(experimental_h17p_external_bw_scratch, + "Experimental external bandwidth scratch offset; unset by default"); + +static int experimental_h17p_external_bw_request = -1; +module_param(experimental_h17p_external_bw_request, int, 0444); +MODULE_PARM_DESC(experimental_h17p_external_bw_request, + "Experimental external bandwidth request offset; unset by default"); + bool dcp_h17p_external_enabled(void) { return experimental_h17p_external; @@ -3158,8 +3169,25 @@ static int dcp_get_disp_regs(struct apple_dcp *dcp) } if (dcp->hw.firmware_clock) { - /* The internal firmware's last aperture follows five display windows. */ - if (count != 5 || resource_size(dcp->hw.firmware_clock) < 4 || + unsigned int windows = dcp->hw.firmware_display_windows; + bool external = dcp->hw.iomfb_method_profile == + DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL; + + if (!windows || windows >= MAX_DISP_REGISTERS) + return -EINVAL; + if (external) { + /* The external clock aperture is already described by disp-4. */ + if (count != windows + 1) + return -EINVAL; + res = dcp->disp_registers[windows]; + if (!apple_dcp_external_clock_window(res->start, + resource_size(res), + dcp->hw.firmware_clock->start, + resource_size(dcp->hw.firmware_clock))) + return -EINVAL; + count = windows; + } + if (count != windows || resource_size(dcp->hw.firmware_clock) < 4 || dcp->hw.firmware_scratch > resource_size(dcp->hw.firmware_clock) - 4 || dcp->hw.firmware_request > resource_size(dcp->hw.firmware_clock) - 4) return -EINVAL; @@ -3448,6 +3476,15 @@ static int dcp_comp_bind(struct device *dev, struct device *main, void *data) goto err_piodma; } + if (dcp->hw.iomfb_method_profile == + DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL && + (!dcp->clk_194 || !clk_get_rate(dcp->clk) || + !clk_get_rate(dcp->clk_194))) { + ret = dev_err_probe(dev, -EINVAL, + "External display clocks are unavailable\n"); + goto err_piodma; + } + 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); @@ -3662,6 +3699,18 @@ static int dcp_platform_probe(struct platform_device *pdev) spin_lock_init(&dcp->mode_state.lock); spin_lock_init(&dcp->dcpavserv.lock); dcp->hw = *(struct apple_dcp_hw_data *)of_device_get_match_data(dev); + if (dcp->hw.iomfb_method_profile == + DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) { + if (!dcp->hw.firmware_clock || + !apple_dcp_external_bw_valid(resource_size(dcp->hw.firmware_clock), + experimental_h17p_external_bw_scratch, + experimental_h17p_external_bw_request)) + return dev_err_probe(dev, -EINVAL, + "External bandwidth offsets are unset or invalid\n"); + dcp->hw.firmware_scratch = experimental_h17p_external_bw_scratch; + dcp->hw.firmware_request = experimental_h17p_external_bw_request; + } + /* The domain use must survive driver-core DMA cleanup for a live session. */ ret = iommu_device_use_default_domain(dev); @@ -3988,6 +4037,7 @@ static const struct apple_dcp_hw_data apple_dcp_hw_t8140 = { .adopt_live_session = true, .firmware_compat = DCP_FIRMWARE_H17P, .firmware_clock = &t8140_firmware_clock, + .firmware_display_windows = 5, .firmware_scratch = 0x20000, .firmware_request = 0x68000, }; @@ -3998,6 +4048,8 @@ static const struct apple_dcp_hw_data apple_dcp_hw_t8140_external = { .iomfb_method_profile = DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL, .adopt_live_session = false, .firmware_compat = DCP_FIRMWARE_H17P, + .firmware_clock = &t8140_firmware_clock, + .firmware_display_windows = 4, }; /* diff --git a/tools/testing/selftests/drivers/gpu/test_t8140_external_bw.py b/tools/testing/selftests/drivers/gpu/test_t8140_external_bw.py new file mode 100644 index 0000000000000..73f0dba8d4d22 --- /dev/null +++ b/tools/testing/selftests/drivers/gpu/test_t8140_external_bw.py @@ -0,0 +1,45 @@ +# SPDX-License-Identifier: MIT +import pathlib +import subprocess +import tempfile +import unittest + + +class BandwidthBounds(unittest.TestCase): + def test_shared_header_boundaries(self): + root = pathlib.Path(__file__).resolve().parents[5] + code = r""" +#include +#include +#include "dcp-external-bw.h" +int main(void) { + const u64 n=0xbc000; + assert(!apple_dcp_external_bw_valid(n,-1,-1)); + assert(!apple_dcp_external_bw_valid(n,0,-1)); + assert(!apple_dcp_external_bw_valid(n,-1,0)); + assert(!apple_dcp_external_bw_valid(n,4,4)); + assert(!apple_dcp_external_bw_valid(n,2,4)); + assert(!apple_dcp_external_bw_valid(n,4,6)); + assert(!apple_dcp_external_bw_valid(n,n,4)); + assert(!apple_dcp_external_bw_valid(n,0,n)); + assert(!apple_dcp_external_bw_valid(3,0,4)); + assert(!apple_dcp_external_bw_valid(n,INT_MAX,0)); + assert(apple_dcp_external_bw_valid(n,0,4)); + assert(apple_dcp_external_bw_valid(n,n-4,0)); + assert(apple_dcp_external_bw_valid(n,0,n-4)); + assert(apple_dcp_external_clock_window(0x302800000ULL,n,0x302800000ULL,n)); + assert(!apple_dcp_external_clock_window(0x302800000ULL,n-4,0x302800000ULL,n)); + assert(!apple_dcp_external_clock_window(0x302800004ULL,n,0x302800000ULL,n)); + return 0; +} +""" + with tempfile.TemporaryDirectory() as directory: + tmp=pathlib.Path(directory);(tmp/'linux').mkdir() + (tmp/'linux/types.h').write_text('#include \n#include \ntypedef uint64_t u64; typedef uint32_t u32;\n') + (tmp/'test.c').write_text(code) + subprocess.run(['clang','-std=c11','-Wall','-Wextra','-Werror','-pedantic','-I'+str(tmp),'-I'+str(root/'drivers/gpu/drm/apple'),str(tmp/'test.c'),'-o',str(tmp/'test')],check=True) + subprocess.run([str(tmp/'test')],check=True) + + +if __name__=='__main__': + unittest.main() From 2e6003f6db07b0e0a6b2bf2b977753bee47556b0 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 16:40:12 +1100 Subject: [PATCH 08/52] arm64: dts: apple: add an explicit external display trial tree Describe the rear Type-C route and both scanout DARTs in a separate test DTB. Keep the normal J700 tree unchanged. The external DART must be withdrawn by the paired loader when external validation fails. Route selectors and external bandwidth parameters still require hardware qualification. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/boot/dts/apple/Makefile | 1 + .../dts/apple/t8140-j700-external-test.dts | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 arch/arm64/boot/dts/apple/t8140-j700-external-test.dts diff --git a/arch/arm64/boot/dts/apple/Makefile b/arch/arm64/boot/dts/apple/Makefile index 06e84a336b5b0..27cb75f1e2856 100644 --- a/arch/arm64/boot/dts/apple/Makefile +++ b/arch/arm64/boot/dts/apple/Makefile @@ -111,4 +111,5 @@ dtb-$(CONFIG_ARCH_APPLE) += t8122-j504.dtb dtb-$(CONFIG_ARCH_APPLE) += t8122-j613.dtb dtb-$(CONFIG_ARCH_APPLE) += t8122-j615.dtb dtb-$(CONFIG_ARCH_APPLE) += t8140-j700.dtb +dtb-$(CONFIG_ARCH_APPLE) += t8140-j700-external-test.dtb dtb-$(CONFIG_ARCH_APPLE) += t8142-j813.dtb diff --git a/arch/arm64/boot/dts/apple/t8140-j700-external-test.dts b/arch/arm64/boot/dts/apple/t8140-j700-external-test.dts new file mode 100644 index 0000000000000..6dde297589d65 --- /dev/null +++ b/arch/arm64/boot/dts/apple/t8140-j700-external-test.dts @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-2.0+ OR MIT +/* Explicit external-display trial; the ordinary J700 tree is unchanged. */ +#include "t8140-j700.dts" +#include + +&display { + iommus = <&disp0_dart 0>, <&dispext0_dart 0>; +}; + +&dcpext { + clocks = <&clk_disp0_154>, <&clk_disp0_194>; + clock-names = "clock-154", "clock-194"; + phys = <&atcphy0 PHY_TYPE_DP>; + phy-names = "typec0"; + mux-controls = <&dpxbar0 0>; + mux-control-names = "typec0"; + apple,typec-mux-indices = <0>; + apple,typec-dptx-phys = <0>; + apple,dcp-index = <1>; +}; + +&{/soc/dcp@416e00000/typec-routes/route@0} { + status = "okay"; +}; From b5c2960b4b441fa51b7f0ccdbaa17eae8a9de4bc Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 16:47:51 +1100 Subject: [PATCH 09/52] soc: apple: rtkit: identify failed boot handshake stages Report which bounded startup wait failed so endpoint discovery, host readiness and IOP acknowledgment failures can be distinguished without logging message payloads. Successful handshakes remain unchanged. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/soc/apple/rtkit.c | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/drivers/soc/apple/rtkit.c b/drivers/soc/apple/rtkit.c index eb0da50d092a2..85d70c46015db 100644 --- a/drivers/soc/apple/rtkit.c +++ b/drivers/soc/apple/rtkit.c @@ -1034,22 +1034,28 @@ static int apple_rtkit_boot_common(struct apple_rtkit *rtk, bool early_ap_power) dev_dbg(rtk->dev, "RTKit: waiting for boot to finish\n"); ret = apple_rtkit_wait_for_completion(&rtk->epmap_completion); - if (ret) + if (ret) { + dev_err(rtk->dev, "RTKit: endpoint discovery failed: %d\n", ret); return ret; + } if (rtk->boot_result) return rtk->boot_result; /* Some helpers need AP readiness before acknowledging IOP power. */ if (early_ap_power) { ret = apple_rtkit_set_ap_power_state(rtk, APPLE_RTKIT_PWR_STATE_ON); - if (ret) + if (ret) { + dev_err(rtk->dev, "RTKit: early AP readiness failed: %d\n", ret); return ret; + } } dev_dbg(rtk->dev, "RTKit: waiting for IOP power state ACK\n"); ret = apple_rtkit_wait_for_completion(&rtk->iop_pwr_ack_completion); - if (ret) + if (ret) { + dev_err(rtk->dev, "RTKit: IOP power acknowledgment failed: %d\n", ret); return ret; + } return early_ap_power ? 0 : apple_rtkit_set_ap_power_state(rtk, APPLE_RTKIT_PWR_STATE_ON); From 5da6455cdd5182bdd70160d036cd45723e9f5448 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 16:47:51 +1100 Subject: [PATCH 10/52] drm/apple: announce AP readiness early for experimental external H17P The external H17P trial completes endpoint setup but its normal RTKit wake sequence times out. Use the existing early-AP wake helper for this experimental profile, which announces host readiness before waiting for IOP readiness. Keep the normal wake sequence for other profiles. This ordering is a hardware qualification candidate. The previous trial retained the internal panel but did not expose an external connector. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/dcp.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 3f60b70042202..6a9286a2cf562 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -3529,7 +3529,12 @@ static int dcp_comp_bind(struct device *dev, struct device *main, void *data) if (dcp->hw.adopt_live_session) dev_info(dev, "negotiating RTKit with the running DCP\n"); - ret = apple_rtkit_wake(dcp->rtk); + /* Announce host readiness before awaiting external IOP readiness. */ + if (dcp->hw.iomfb_method_profile == + DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) + ret = apple_rtkit_wake_early_ap(dcp->rtk); + else + ret = apple_rtkit_wake(dcp->rtk); if (ret) { ret = dev_err_probe(dev, ret, "Failed to boot RTKit: %d\n", ret); goto err_piodma; From e00ebb9cde3c14fc30e252965969b232c683b139 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 16:54:13 +1100 Subject: [PATCH 11/52] drm/apple: acknowledge firmware-owned external H17P buffers The external H17P startup requests a shared buffer at a firmware-provided IOVA. Its setup callback maps that buffer, but the standard RTKit ops do not acknowledge mapped buffers. Select the existing co-processor-owned buffer protocol for this experimental profile so RTKit acknowledges the request and starts the crashlog endpoint before its endpoint-map reply. Keep other display profiles on their existing ops. Restore the normal wake ordering after the earlier AP-readiness trial also timed out, while retaining diagnostics for subsequent qualification. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/dcp.c | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 6a9286a2cf562..d884a5aeb91bb 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -2092,6 +2092,14 @@ static struct apple_rtkit_ops rtkit_ops = { .shmem_destroy = dcp_rtk_shmem_destroy, }; +static const struct apple_rtkit_ops external_h17p_rtkit_ops = { + .flags = APPLE_RTKIT_COPROC_PLACES_BUFFERS, + .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) { if (READ_ONCE(dcp->quiescing)) @@ -3516,7 +3524,13 @@ static int dcp_comp_bind(struct device *dev, struct device *main, void *data) ret = dcp_pin_live_session(dcp); if (ret) goto err_piodma; - dcp->rtk = apple_rtkit_init(dev, dcp, "mbox", 0, &rtkit_ops); + /* External H17P supplies shared-buffer addresses during RTKit startup. */ + if (dcp->hw.iomfb_method_profile == + DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) + dcp->rtk = apple_rtkit_init(dev, dcp, "mbox", 0, + &external_h17p_rtkit_ops); + else + dcp->rtk = apple_rtkit_init(dev, dcp, "mbox", 0, &rtkit_ops); if (IS_ERR(dcp->rtk)) { ret = dev_err_probe(dev, PTR_ERR(dcp->rtk), "Failed to initialize RTKit\n"); @@ -3529,12 +3543,7 @@ static int dcp_comp_bind(struct device *dev, struct device *main, void *data) if (dcp->hw.adopt_live_session) dev_info(dev, "negotiating RTKit with the running DCP\n"); - /* Announce host readiness before awaiting external IOP readiness. */ - if (dcp->hw.iomfb_method_profile == - DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) - ret = apple_rtkit_wake_early_ap(dcp->rtk); - else - ret = apple_rtkit_wake(dcp->rtk); + ret = apple_rtkit_wake(dcp->rtk); if (ret) { ret = dev_err_probe(dev, ret, "Failed to boot RTKit: %d\n", ret); goto err_piodma; From fa66e6f2c3899d58951b63cfb47ac1ca932b1955 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 17:00:10 +1100 Subject: [PATCH 12/52] drm/apple: pair external buffer acknowledgments with early AP readiness The early-AP trial still lacked acknowledgments for firmware-owned shared buffers. The buffer-protocol trial supplied those acknowledgments but kept waiting for IOP readiness before announcing AP readiness. Neither trial established that one change alone could complete the external handshake. Combine the existing early-AP helper with the corrected buffer protocol for the experimental external profile. Keep normal display profiles and all timeout and lifetime rules unchanged. Hardware qualification of the combined sequence remains required. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/dcp.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index d884a5aeb91bb..5a29d432c7967 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -3543,7 +3543,12 @@ static int dcp_comp_bind(struct device *dev, struct device *main, void *data) if (dcp->hw.adopt_live_session) dev_info(dev, "negotiating RTKit with the running DCP\n"); - ret = apple_rtkit_wake(dcp->rtk); + /* Announce host readiness before awaiting external IOP readiness. */ + if (dcp->hw.iomfb_method_profile == + DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) + ret = apple_rtkit_wake_early_ap(dcp->rtk); + else + ret = apple_rtkit_wake(dcp->rtk); if (ret) { ret = dev_err_probe(dev, ret, "Failed to boot RTKit: %d\n", ret); goto err_piodma; From 78fc11047efa814a238113c46b4bb07aa7d641fb Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 17:04:31 +1100 Subject: [PATCH 13/52] arm64: dts: apple: give the external J700 trial its firmware DMA window The internal display DART already describes a 1 TiB-based, 64 GiB DMA window. The external trial omitted that range, so its shared buffer was allocated below the firmware window while the internal controller used the high window and completed its power handshake. Describe the same window on the external DART in the test tree. This matches the high address of the external firmware-owned buffer observed during startup. Leave the normal J700 tree unchanged. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/boot/dts/apple/t8140-j700-external-test.dts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/arch/arm64/boot/dts/apple/t8140-j700-external-test.dts b/arch/arm64/boot/dts/apple/t8140-j700-external-test.dts index 6dde297589d65..d3965ab2c5061 100644 --- a/arch/arm64/boot/dts/apple/t8140-j700-external-test.dts +++ b/arch/arm64/boot/dts/apple/t8140-j700-external-test.dts @@ -22,3 +22,8 @@ &{/soc/dcp@416e00000/typec-routes/route@0} { status = "okay"; }; + +/* Match the external firmware buffer window rather than allocating below it. */ +&dcpext0_dart { + apple,dma-range = <0x100 0x0 0x10 0x0>; +}; From 60f78724df46169d3d583e5f97a71444c11083da Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 17:07:14 +1100 Subject: [PATCH 14/52] drm/apple: recognize the external H17P display service The external H17P firmware announces dispext-service on the existing IBOOT endpoint. Match that measured service name for the experimental external profile, keeping the normal disp0-service table unchanged. Propagate endpoint allocation and startup failures to the caller. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/ibootep.c | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/drivers/gpu/drm/apple/ibootep.c b/drivers/gpu/drm/apple/ibootep.c index ae4bc8a69f2a8..79d5dc19569c3 100644 --- a/drivers/gpu/drm/apple/ibootep.c +++ b/drivers/gpu/drm/apple/ibootep.c @@ -20,10 +20,23 @@ static const struct apple_epic_service_ops ibootep_ops[] = { {} }; +static const struct apple_epic_service_ops external_ibootep_ops[] = { + { + .name = "dispext-service", + .init = disp_service_init, + }, + {} +}; + int ibootep_init(struct apple_dcp *dcp) { - dcp->ibootep = afk_init(dcp, DISP0_ENDPOINT, ibootep_ops); - afk_start(dcp->ibootep); + const struct apple_epic_service_ops *ops = ibootep_ops; + + if (dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) + ops = external_ibootep_ops; + dcp->ibootep = afk_init(dcp, DISP0_ENDPOINT, ops); + if (IS_ERR(dcp->ibootep)) + return PTR_ERR(dcp->ibootep); - return 0; + return afk_start(dcp->ibootep); } From 7739e55cdbb669a3add3cd8b5ee3063377697eb8 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 17:07:15 +1100 Subject: [PATCH 15/52] drm/apple: report external AFK startup progress Report external-profile control-message types and ring readiness when startup times out. Exclude normal receive notifications and rate-limit control diagnostics; no payload contents or addresses are logged. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/afk.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/drivers/gpu/drm/apple/afk.c b/drivers/gpu/drm/apple/afk.c index 8161db7eb440f..c7e2baf56a93a 100644 --- a/drivers/gpu/drm/apple/afk.c +++ b/drivers/gpu/drm/apple/afk.c @@ -146,6 +146,11 @@ int afk_start(struct apple_dcp_afkep *ep) afk_send(ep, FIELD_PREP(RBEP_TYPE, RBEP_INIT)); ret = wait_for_completion_timeout(&ep->started, msecs_to_jiffies(1000)); + if (ret <= 0 && ep->dcp->hw.iomfb_method_profile == + DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) + dev_warn(ep->dcp->dev, + "external AFK startup ep=%#x tx_ready=%u rx_ready=%u bytes=%zu\n", + ep->endpoint, ep->txbfr.ready, ep->rxbfr.ready, ep->bfr_size); if (ret <= 0) return -ETIMEDOUT; else @@ -818,6 +823,11 @@ static void afk_receive_message_worker(struct work_struct *work_) work = container_of(work_, struct afk_receive_message_work, work); type = FIELD_GET(RBEP_TYPE, work->message); + if (work->ep->dcp->hw.iomfb_method_profile == + DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL && type != RBEP_RECV) + dev_info_ratelimited(work->ep->dcp->dev, + "external AFK control ep=%#x type=%#x\n", + work->ep->endpoint, type); switch (type) { case RBEP_INIT_ACK: break; From ca886226f17cd09a1ec844da2c944a81dd181d99 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 17:11:52 +1100 Subject: [PATCH 16/52] drm/apple: start the external H17P allocator before service discovery External H17P reaches AFK transport readiness but does not announce the DPTX port service. Its existing allocator endpoint is currently started only later with IOMFB. Make that known endpoint available before external service discovery, and skip its duplicate start during IOMFB startup. Keep normal H17P ordering unchanged. This external-only ordering change requires hardware validation; no new endpoint or message is introduced. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/dcp.c | 7 +++++++ drivers/gpu/drm/apple/iomfb.c | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 5a29d432c7967..84761884261e0 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -2546,6 +2546,13 @@ int dcp_start(struct platform_device *pdev) return -ENODEV; } + /* Make the H17P allocator available before external service discovery. */ + if (dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) { + ret = apple_rtkit_start_ep(dcp->rtk, REMOTE_ALLOC_ENDPOINT); + if (ret) + return dev_err_probe(dcp->dev, ret, "Failed to start remote allocator\n"); + } + /* start RTKit endpoints */ ret = systemep_init(dcp); if (ret) diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index ac23f92329f74..1950599effd74 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -1563,7 +1563,8 @@ int iomfb_start_rtkit(struct apple_dcp *dcp) * has nothing answering it on the AP side, and without this one the * display DART faults. */ - if (dcp->fw_compat == DCP_FIRMWARE_H17P) { + if (dcp->fw_compat == DCP_FIRMWARE_H17P && + dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) { ret = apple_rtkit_start_ep(dcp->rtk, REMOTE_ALLOC_ENDPOINT); if (ret) return ret; From f58c17ce883538c28cf14000844e47ed903947a7 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 17:11:52 +1100 Subject: [PATCH 17/52] drm/apple: isolate DPTX discovery diagnostics Limit external AFK control diagnostics to DPTX so unrelated endpoint startup cannot consume their rate limit. Distinguish a ready transport with no announced port service from a transport-start timeout. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/afk.c | 3 ++- drivers/gpu/drm/apple/dptxep.c | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/drivers/gpu/drm/apple/afk.c b/drivers/gpu/drm/apple/afk.c index c7e2baf56a93a..c8a6d1426171a 100644 --- a/drivers/gpu/drm/apple/afk.c +++ b/drivers/gpu/drm/apple/afk.c @@ -824,7 +824,8 @@ static void afk_receive_message_worker(struct work_struct *work_) type = FIELD_GET(RBEP_TYPE, work->message); if (work->ep->dcp->hw.iomfb_method_profile == - DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL && type != RBEP_RECV) + DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL && + work->ep->endpoint == DPTX_ENDPOINT && type != RBEP_RECV) dev_info_ratelimited(work->ep->dcp->dev, "external AFK control ep=%#x type=%#x\n", work->ep->endpoint, type); diff --git a/drivers/gpu/drm/apple/dptxep.c b/drivers/gpu/drm/apple/dptxep.c index ec190a7461df9..571f6f1b03d3e 100644 --- a/drivers/gpu/drm/apple/dptxep.c +++ b/drivers/gpu/drm/apple/dptxep.c @@ -871,10 +871,13 @@ int dptxep_init(struct apple_dcp *dcp) for (port = 0; port < dcp->hw.num_dptx_ports; port++) { ret = wait_for_completion_timeout(&dcp->dptxport[port].enable_completion, timeout); - if (!ret) + if (!ret) { + dev_warn(dcp->dev, "DPTX transport ready but port %u service not announced\n", + port); return -ETIMEDOUT; - else if (ret < 0) + } else if (ret < 0) { return ret; + } timeout = ret; } From 79f124c8e43c4c8bee7879723a88611f539f76f7 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 17:17:13 +1100 Subject: [PATCH 18/52] drm/apple: initialize the external DPTX controller before port discovery The public m1n1 display startup initializes the DPTX controller endpoint before the port endpoint. Add that existing AFK controller interface to the experimental external H17P path, wait for its service announcement, and then continue port discovery. Send no controller service commands. Use a dedicated endpoint pointer and route its incoming messages through the existing AFK worker. Cover both retained-session quiescing and normal teardown, and never publish an allocation error as an endpoint pointer. Normal display profiles retain their existing startup sequence. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/dcp-internal.h | 3 +++ drivers/gpu/drm/apple/dcp.c | 19 +++++++++++++++ drivers/gpu/drm/apple/dcp.h | 1 + drivers/gpu/drm/apple/dptxep.c | 35 ++++++++++++++++++++++++++++ 4 files changed, 58 insertions(+) diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index ba790968a2dd1..3b99d5b403e9f 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -89,6 +89,7 @@ enum { TEST_ENDPOINT = 0x21, DCP_EXPERT_ENDPOINT = 0x22, DISP0_ENDPOINT = 0x23, + DPTX_CONTROLLER_ENDPOINT = 0x24, DPAVSERV_ENDPOINT = 0x28, AV_ENDPOINT = 0x29, DPTX_ENDPOINT = 0x2a, @@ -357,6 +358,8 @@ struct apple_dcp { struct audiosrv_data *audiosrv; struct apple_dcp_afkep *dptxep; + struct apple_dcp_afkep *dptx_controller_ep; + struct completion dptx_controller_ready; struct dptx_port dptxport[2]; diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 84761884261e0..d426bf4c90c14 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -1875,6 +1875,8 @@ static struct apple_dcp_afkep *dcp_afkep(struct apple_dcp *dcp, u8 endpoint) return dcp->dcpavservep; case DPTX_ENDPOINT: return dcp->dptxep; + case DPTX_CONTROLLER_ENDPOINT: + return dcp->dptx_controller_ep; default: return NULL; } @@ -1958,6 +1960,9 @@ static void dcp_recv_msg(void *cookie, u8 endpoint, u64 message) case DPTX_ENDPOINT: ep = dcp->dptxep; break; + case DPTX_CONTROLLER_ENDPOINT: + ep = dcp->dptx_controller_ep; + break; default: ep = NULL; break; @@ -2571,6 +2576,14 @@ int dcp_start(struct platform_device *pdev) dev_warn(dcp->dev, "Failed to start IBOOT endpoint: %d\n", ret); + if (dcp->hw.iomfb_method_profile == + DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) { + ret = dptx_controller_init(dcp); + if (ret) + return dev_err_probe(dcp->dev, ret, + "Failed to start DPTX controller\n"); + } + ret = dptxep_init(dcp); if (ret) { dev_warn(dcp->dev, "Failed to start DPTX endpoint: %d\n", @@ -3589,6 +3602,7 @@ static void dcp_comp_unbind(struct device *dev, struct device *main, void *data) iomfb_queue_stop(dcp); afk_quiesce(dcp->avep); afk_quiesce(dcp->dptxep); + afk_quiesce(dcp->dptx_controller_ep); afk_quiesce(dcp->ibootep); afk_quiesce(dcp->systemep); afk_quiesce(dcp->dcpavservep); @@ -3625,6 +3639,11 @@ static void dcp_comp_unbind(struct device *dev, struct device *main, void *data) dcp->dptxep = NULL; } + if (!IS_ERR_OR_NULL(dcp->dptx_controller_ep)) { + afk_shutdown(dcp->dptx_controller_ep); + dcp->dptx_controller_ep = NULL; + } + if (dcp->ibootep) { afk_shutdown(dcp->ibootep); dcp->ibootep = NULL; diff --git a/drivers/gpu/drm/apple/dcp.h b/drivers/gpu/drm/apple/dcp.h index df1e46c439697..600899c71546b 100644 --- a/drivers/gpu/drm/apple/dcp.h +++ b/drivers/gpu/drm/apple/dcp.h @@ -97,6 +97,7 @@ void iomfb_recv_msg(struct apple_dcp *dcp, u64 message); int systemep_init(struct apple_dcp *dcp); int dptxep_init(struct apple_dcp *dcp); +int dptx_controller_init(struct apple_dcp *dcp); int ibootep_init(struct apple_dcp *dcp); int dpavservep_init(struct apple_dcp *dcp); int avep_init(struct apple_dcp *dcp); diff --git a/drivers/gpu/drm/apple/dptxep.c b/drivers/gpu/drm/apple/dptxep.c index 571f6f1b03d3e..e1eeb8aa6bd47 100644 --- a/drivers/gpu/drm/apple/dptxep.c +++ b/drivers/gpu/drm/apple/dptxep.c @@ -849,6 +849,41 @@ static const struct apple_epic_service_ops dptxep_ops[] = { {} }; +static void dptx_controller_service_init(struct apple_epic_service *service, + const char *name, const char *class, s64 unit) +{ + complete(&service->ep->dcp->dptx_controller_ready); +} + +static const struct apple_epic_service_ops dptx_controller_ops[] = { + { + .name = "AppleDCPDPTXController", + .init = dptx_controller_service_init, + }, + {} +}; + +int dptx_controller_init(struct apple_dcp *dcp) +{ + struct apple_dcp_afkep *ep; + int ret; + + init_completion(&dcp->dptx_controller_ready); + ep = afk_init(dcp, DPTX_CONTROLLER_ENDPOINT, dptx_controller_ops); + if (IS_ERR(ep)) + return PTR_ERR(ep); + dcp->dptx_controller_ep = ep; + + ret = afk_start(ep); + if (ret) + return ret; + if (!wait_for_completion_timeout(&dcp->dptx_controller_ready, + msecs_to_jiffies(1000))) + return -ETIMEDOUT; + + return 0; +} + int dptxep_init(struct apple_dcp *dcp) { int ret; From 892ceabbc9dd110edda363c8e62e99069a2c57e7 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 17:27:24 +1100 Subject: [PATCH 19/52] drm/apple: discover the version-two external DPTX service The external H17P test reaches transport readiness on the legacy DPTX endpoint without receiving a port announcement. The public version-two firmware implementation uses endpoint 0x2c, which this firmware advertises. Use that endpoint for an isolated service-discovery test. Keep a separate endpoint object with an initialization-only service table, RX routing and lifecycle handling. This does not enable operational port callbacks or transmit service commands; the discovered identity and framing must be validated before extending the command path. Normal profiles retain the legacy DPTX endpoint. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/afk.c | 3 +- drivers/gpu/drm/apple/dcp-internal.h | 3 ++ drivers/gpu/drm/apple/dcp.c | 12 ++++++++ drivers/gpu/drm/apple/dptxep.c | 41 ++++++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/apple/afk.c b/drivers/gpu/drm/apple/afk.c index c8a6d1426171a..546774c00f45c 100644 --- a/drivers/gpu/drm/apple/afk.c +++ b/drivers/gpu/drm/apple/afk.c @@ -825,7 +825,8 @@ static void afk_receive_message_worker(struct work_struct *work_) type = FIELD_GET(RBEP_TYPE, work->message); if (work->ep->dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL && - work->ep->endpoint == DPTX_ENDPOINT && type != RBEP_RECV) + (work->ep->endpoint == DPTX_ENDPOINT || + work->ep->endpoint == DPTX_ENDPOINT_V2) && type != RBEP_RECV) dev_info_ratelimited(work->ep->dcp->dev, "external AFK control ep=%#x type=%#x\n", work->ep->endpoint, type); diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index 3b99d5b403e9f..700512815c2af 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -93,6 +93,7 @@ enum { DPAVSERV_ENDPOINT = 0x28, AV_ENDPOINT = 0x29, DPTX_ENDPOINT = 0x2a, + DPTX_ENDPOINT_V2 = 0x2c, HDCP_ENDPOINT = 0x2b, REMOTE_ALLOC_ENDPOINT = 0x2d, IOMFB_ENDPOINT = 0x37, @@ -360,6 +361,8 @@ struct apple_dcp { struct apple_dcp_afkep *dptxep; struct apple_dcp_afkep *dptx_controller_ep; struct completion dptx_controller_ready; + struct apple_dcp_afkep *dptx_v2_probe_ep; + struct completion dptx_v2_probe_ready; struct dptx_port dptxport[2]; diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index d426bf4c90c14..2b5b6bdef87e1 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -1875,6 +1875,9 @@ static struct apple_dcp_afkep *dcp_afkep(struct apple_dcp *dcp, u8 endpoint) return dcp->dcpavservep; case DPTX_ENDPOINT: return dcp->dptxep; + case DPTX_ENDPOINT_V2: + return dcp->hw.iomfb_method_profile == + DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL ? dcp->dptx_v2_probe_ep : NULL; case DPTX_CONTROLLER_ENDPOINT: return dcp->dptx_controller_ep; default: @@ -1960,6 +1963,9 @@ static void dcp_recv_msg(void *cookie, u8 endpoint, u64 message) case DPTX_ENDPOINT: ep = dcp->dptxep; break; + case DPTX_ENDPOINT_V2: + ep = dcp->dptx_v2_probe_ep; + break; case DPTX_CONTROLLER_ENDPOINT: ep = dcp->dptx_controller_ep; break; @@ -3603,6 +3609,7 @@ static void dcp_comp_unbind(struct device *dev, struct device *main, void *data) afk_quiesce(dcp->avep); afk_quiesce(dcp->dptxep); afk_quiesce(dcp->dptx_controller_ep); + afk_quiesce(dcp->dptx_v2_probe_ep); afk_quiesce(dcp->ibootep); afk_quiesce(dcp->systemep); afk_quiesce(dcp->dcpavservep); @@ -3639,6 +3646,11 @@ static void dcp_comp_unbind(struct device *dev, struct device *main, void *data) dcp->dptxep = NULL; } + if (!IS_ERR_OR_NULL(dcp->dptx_v2_probe_ep)) { + afk_shutdown(dcp->dptx_v2_probe_ep); + dcp->dptx_v2_probe_ep = NULL; + } + if (!IS_ERR_OR_NULL(dcp->dptx_controller_ep)) { afk_shutdown(dcp->dptx_controller_ep); dcp->dptx_controller_ep = NULL; diff --git a/drivers/gpu/drm/apple/dptxep.c b/drivers/gpu/drm/apple/dptxep.c index e1eeb8aa6bd47..e00c4cbcea171 100644 --- a/drivers/gpu/drm/apple/dptxep.c +++ b/drivers/gpu/drm/apple/dptxep.c @@ -884,12 +884,53 @@ int dptx_controller_init(struct apple_dcp *dcp) return 0; } +static void dptx_v2_probe_service(struct apple_epic_service *service, + const char *name, const char *class, s64 unit) +{ + dev_info(service->ep->dcp->dev, + "DPTX v2 discovery name=%s class=%s unit=%lld channel=%u\n", + name ?: "(none)", class ?: "(none)", unit, service->channel); + complete(&service->ep->dcp->dptx_v2_probe_ready); +} + +static const struct apple_epic_service_ops dptx_v2_probe_ops[] = { + { + .name = "AppleDCPDPTXRemotePort", + .init = dptx_v2_probe_service, + }, + {} +}; + +static int dptx_v2_discover(struct apple_dcp *dcp) +{ + struct apple_dcp_afkep *ep; + int ret; + + init_completion(&dcp->dptx_v2_probe_ready); + ep = afk_init(dcp, DPTX_ENDPOINT_V2, dptx_v2_probe_ops); + if (IS_ERR(ep)) + return PTR_ERR(ep); + dcp->dptx_v2_probe_ep = ep; + ret = afk_start(ep); + if (ret) + return ret; + if (!wait_for_completion_timeout(&dcp->dptx_v2_probe_ready, + msecs_to_jiffies(1000))) + return -ETIMEDOUT; + + /* Discovery does not enable port callbacks or transmit service commands. */ + return -EOPNOTSUPP; +} + int dptxep_init(struct apple_dcp *dcp) { int ret; u32 port; unsigned long timeout = msecs_to_jiffies(1000); + if (dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) + return dptx_v2_discover(dcp); + init_completion(&dcp->dptxport[0].enable_completion); init_completion(&dcp->dptxport[1].enable_completion); init_completion(&dcp->dptxport[0].linkcfg_completion); From d063a2ab3be82f5aca9267725e1b84060314489e Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 17:39:11 +1100 Subject: [PATCH 20/52] drm/apple: operate the external DPTX service over AFKV2 The external T8140 controller announces its port on endpoint 0x2c using version-two frames. Select that transport per endpoint, open announced interfaces and map the single physical DCP unit to logical port zero. Encode bounded command descriptors and validate replies against pending owned buffers before completing them. Preserve callback tokens and limit peer-buffer access to active allocations of the same service. Keep the legacy transport for other display profiles. Add host selftests for frame encoding, malformed records, response correlation, command identifiers and the production owned-copy helper. These pass 905 codec assertions and owned-copy cases with sanitizers; hardware qualification of the operational transport remains pending. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- MAINTAINERS | 1 + drivers/gpu/drm/apple/afk.c | 355 +++++++++++++++++- drivers/gpu/drm/apple/afk.h | 6 + drivers/gpu/drm/apple/afkv2-codec.h | 270 +++++++++++++ drivers/gpu/drm/apple/dcp-internal.h | 2 - drivers/gpu/drm/apple/dcp.c | 10 +- drivers/gpu/drm/apple/dptxep.c | 81 ++-- .../drivers/gpu/apple-afkv2/Makefile | 9 + .../selftests/drivers/gpu/apple-afkv2/README | 24 ++ .../selftests/drivers/gpu/apple-afkv2/codec.c | 183 +++++++++ .../drivers/gpu/apple-afkv2/extract-owned.py | 28 ++ .../apple-afkv2/host-include/linux/errno.h | 5 + .../apple-afkv2/host-include/linux/string.h | 2 + .../apple-afkv2/host-include/linux/types.h | 8 + .../drivers/gpu/apple-afkv2/owned-prefix.c | 50 +++ .../drivers/gpu/apple-afkv2/owned-suffix.c | 56 +++ .../gpu/apple-afkv2/run-afkv2-tests.sh | 40 ++ 17 files changed, 1050 insertions(+), 80 deletions(-) create mode 100644 drivers/gpu/drm/apple/afkv2-codec.h create mode 100644 tools/testing/selftests/drivers/gpu/apple-afkv2/Makefile create mode 100644 tools/testing/selftests/drivers/gpu/apple-afkv2/README create mode 100644 tools/testing/selftests/drivers/gpu/apple-afkv2/codec.c create mode 100644 tools/testing/selftests/drivers/gpu/apple-afkv2/extract-owned.py create mode 100644 tools/testing/selftests/drivers/gpu/apple-afkv2/host-include/linux/errno.h create mode 100644 tools/testing/selftests/drivers/gpu/apple-afkv2/host-include/linux/string.h create mode 100644 tools/testing/selftests/drivers/gpu/apple-afkv2/host-include/linux/types.h create mode 100644 tools/testing/selftests/drivers/gpu/apple-afkv2/owned-prefix.c create mode 100644 tools/testing/selftests/drivers/gpu/apple-afkv2/owned-suffix.c create mode 100755 tools/testing/selftests/drivers/gpu/apple-afkv2/run-afkv2-tests.sh diff --git a/MAINTAINERS b/MAINTAINERS index c68108f464848..3f1523e9fdb31 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -1952,6 +1952,7 @@ L: dri-devel@lists.freedesktop.org S: Maintained T: git git://anongit.freedesktop.org/drm/drm-misc F: drivers/gpu/drm/apple/ +F: tools/testing/selftests/drivers/gpu/apple-afkv2/ APPLE PCIE CONTROLLER DRIVER M: Marc Zyngier diff --git a/drivers/gpu/drm/apple/afk.c b/drivers/gpu/drm/apple/afk.c index 546774c00f45c..e7c76e2d8a610 100644 --- a/drivers/gpu/drm/apple/afk.c +++ b/drivers/gpu/drm/apple/afk.c @@ -14,6 +14,7 @@ #include #include "afk.h" +#include "afkv2-codec.h" #include "trace.h" struct afk_receive_message_work { @@ -305,6 +306,10 @@ static struct apple_epic_service *afk_epic_find_service(struct apple_dcp_afkep * return NULL; } +static int afk_send_epic_token(struct apple_dcp_afkep *ep, u32 channel, u16 tag, + enum epic_type etype, enum epic_category ecat, u8 stype, + const void *payload, size_t payload_len, u64 token); + static void afk_recv_handle_init(struct apple_dcp_afkep *ep, u32 channel, u8 *payload, size_t payload_size) { @@ -383,6 +388,22 @@ static void afk_recv_handle_init(struct apple_dcp_afkep *ep, u32 channel, goto free; } ep->num_channels = max(ep->num_channels, ch_idx + 1); + if (ep->wire_v2) { + int ret = afk_send_epic(ep, channel, 0, EPIC_TYPE_NOTIFY, + EPIC_CAT_REPORT, AFKV2_OPEN, NULL, 0); + + if (ret) { + unsigned long flags; + + spin_lock_irqsave(&service->lock, flags); + service->enabled = false; + service->torndown = true; + spin_unlock_irqrestore(&service->lock, flags); + dev_err(ep->dcp->dev, "AFKV2 interface %u OPEN failed: %d\n", + channel, ret); + goto free; + } + } ops->init(service, epic_name, epic_class, epic_unit); dev_info(ep->dcp->dev, "AFK[ep:%02x]: new service %s on channel %d\n", ep->endpoint, service_name, channel); @@ -430,7 +451,7 @@ static void afk_recv_handle_reply(struct apple_dcp_afkep *ep, u32 channel, struct epic_cmd *cmd = payload; struct apple_epic_service *service; unsigned long flags; - u8 idx = tag & 0xff; + u8 idx = tag & (ep->wire_v2 ? 0xf : 0xff); void *rxbuf, *txbuf; dma_addr_t rxbuf_dma, txbuf_dma; size_t rxlen, txlen; @@ -592,6 +613,233 @@ static bool afk_validate_h17p_header(const u8 *data, size_t size) get_unaligned_le32(data + 4) == size - 8; } +static void afk_v2_status(struct apple_dcp_afkep *ep, + const struct afkv2_frame *frame, + const struct afkv2_command *command, u32 status) +{ + u8 record[AFKV2_HEADER_SIZE + AFKV2_COMMAND_SIZE + AFKV2_OOB_SIZE]; + size_t size; + int ret; + + ret = afkv2_encode_error_reply(record, sizeof(record), 0, frame, command, + status, &size); + if (!ret) + ret = afk_send_epic_token(ep, frame->interface, command->id, + EPIC_TYPE_REPLY, EPIC_CAT_REPLY, frame->type, + record + AFKV2_HEADER_SIZE, + size - AFKV2_HEADER_SIZE, frame->token); + if (ret) + dev_warn(ep->dcp->dev, "AFKV2 error reply failed: %d\n", ret); +} + +static int afk_v2_access_owned(struct apple_epic_service *service, u64 dva, + void *buffer, size_t size, bool to_peer, bool copy) +{ + unsigned long flags; + unsigned int i; + int ret = -EACCES; + + if (!service || !service->ep->wire_v2 || size > SZ_1M || (copy && size && !buffer)) + return -EINVAL; + if (!size) + return 0; + spin_lock_irqsave(&service->lock, flags); + for (i = 0; i < MAX_PENDING_CMDS; i++) { + struct epic_cmd_info *cmd = &service->cmds[i]; + void *owned = NULL; + u64 offset; + + if (!test_bit(i, service->cmd_map) || cmd->done) + continue; + if (cmd->rxbuf && afkv2_owned_span(cmd->rxbuf_dma, cmd->rxlen, dva, size)) { + offset = dva - cmd->rxbuf_dma; + owned = cmd->rxbuf + offset; + } else if (cmd->txbuf && afkv2_owned_span(cmd->txbuf_dma, cmd->txlen, dva, size)) { + offset = dva - cmd->txbuf_dma; + owned = cmd->txbuf + offset; + } + if (!owned) + continue; + if (!copy) { + ret = 0; + break; + } + if (to_peer) { + memcpy(owned, buffer, size); + dma_wmb(); + } else { + dma_rmb(); + memcpy(buffer, owned, size); + } + ret = 0; + break; + } + spin_unlock_irqrestore(&service->lock, flags); + if (ret) + dev_warn_ratelimited(service->ep->dcp->dev, + "AFKV2 unowned buffer: ep=%u interface=%u size=%zu write=%u\n", + service->ep->endpoint, service->channel, size, to_peer); + return ret; +} + +int afk_v2_copy_owned(struct apple_epic_service *service, u64 dva, + void *buffer, size_t size, bool to_peer) +{ + return afk_v2_access_owned(service, dva, buffer, size, to_peer, true); +} + +static void afk_v2_callback(struct apple_dcp_afkep *ep, + const struct afkv2_frame *frame, + const struct afkv2_command *command) +{ + struct apple_epic_service *service; + const struct epic_service_call *call; + const u8 *request = command->inline_data; + u8 *owned_request = NULL, *reply = NULL; + size_t request_size = command->oob ? command->tx_size : command->inline_size; + size_t body_size, reply_size; + u32 status = 0xe00002c7; + int ret; + + if (frame->type != EPIC_SUBTYPE_STD_SERVICE || + request_size < sizeof(*call) + sizeof(u32) || + request_size > SZ_1M - AFKV2_COMMAND_SIZE || + request_size > command->value || + (command->oob && request_size > command->rx_size)) + goto reject; + service = afk_service_get(afk_epic_find_service(ep, frame->interface)); + if (!service || READ_ONCE(service->torndown) || !service->ops->call) { + if (service) + afk_service_put(service); + goto reject; + } + if (command->oob) { + /* Reject an unowned response destination before any callback side effect. */ + ret = afk_v2_access_owned(service, command->rx_address, NULL, + request_size, true, false); + if (ret) + goto release; + owned_request = kmalloc(request_size, GFP_KERNEL); + if (!owned_request) { + status = 0xe00002bd; + goto release; + } + ret = afk_v2_copy_owned(service, command->tx_address, + owned_request, request_size, false); + if (ret) + goto release; + request = owned_request; + } + call = (const void *)request; + body_size = request_size - sizeof(*call); + if (le16_to_cpu(call->group) || + le32_to_cpu(call->magic) != EPIC_SERVICE_CALL_MAGIC || + le32_to_cpu(call->data_len) != body_size) + goto release; + reply = kzalloc(AFKV2_COMMAND_SIZE + request_size, GFP_KERNEL); + if (!reply) { + status = 0xe00002bd; + goto release; + } + reply[1] = command->id; + memcpy(reply + AFKV2_COMMAND_SIZE, call, sizeof(*call)); + ret = service->ops->call(service, le32_to_cpu(call->command), + request + sizeof(*call), body_size, + reply + AFKV2_COMMAND_SIZE + sizeof(*call), body_size); + if (ret) + goto release; + reply_size = AFKV2_COMMAND_SIZE + request_size; + if (command->oob) { + ret = afk_v2_copy_owned(service, command->rx_address, + reply + AFKV2_COMMAND_SIZE, request_size, true); + if (ret) + goto release; + ret = afkv2_encode_oob_command(reply, reply_size, command->id, + command->rx_address, request_size, + command->tx_address, command->tx_size); + if (ret) + goto release; + afkv2_put32(reply + 4, 0); + reply_size = AFKV2_COMMAND_SIZE + AFKV2_OOB_SIZE; + } + ret = afk_send_epic_token(ep, frame->interface, command->id, EPIC_TYPE_REPLY, + EPIC_CAT_REPLY, frame->type, reply, reply_size, frame->token); + kfree(reply); + kfree(owned_request); + afk_service_put(service); + if (ret) + dev_warn(ep->dcp->dev, "AFKV2 callback reply failed: %d\n", ret); + return; +release: + kfree(reply); + kfree(owned_request); + afk_service_put(service); +reject: + afk_v2_status(ep, frame, command, status); +} + +static void afk_v2_receive(struct apple_dcp_afkep *ep, u32 channel, u32 type, + const void *data, size_t size) +{ + struct afkv2_frame frame; + struct afkv2_command response; + struct apple_epic_service *service; + struct epic_cmd legacy = {}; + unsigned long flags; + unsigned int slot; + int ret; + + if (channel || type || afkv2_decode_frame(data, size, false, &frame) || + !frame.interface) + return; + if (frame.category == AFKV2_REPORT) { + if (frame.type == EPIC_SUBTYPE_ANNOUNCE_H17P) + afk_recv_handle_init(ep, frame.interface, (void *)frame.payload, + frame.payload_size); + else if (frame.type == AFKV2_CLOSE && frame.interface_flags == 1) + afk_recv_handle_teardown(ep, frame.interface); + return; + } + if (afkv2_decode_command(frame.payload, frame.payload_size, &response)) + return; + if (frame.category == AFKV2_COMMAND) { + afk_v2_callback(ep, &frame, &response); + return; + } + service = afk_epic_find_service(ep, frame.interface); + if (!service) + return; + spin_lock_irqsave(&service->lock, flags); + if (bitmap_empty(service->cmd_map, MAX_PENDING_CMDS)) { + spin_unlock_irqrestore(&service->lock, flags); + return; + } + service->users++; + spin_unlock_irqrestore(&service->lock, flags); + slot = response.id & 0xf; + spin_lock_irqsave(&service->lock, flags); + ret = -EPROTO; + if (slot < MAX_PENDING_CMDS && test_bit(slot, service->cmd_map) && + !service->cmds[slot].done && response.id == service->cmds[slot].tag) + ret = afkv2_check_response(&frame, &response, service->channel, + service->cmds[slot].type, service->cmds[slot].tag, + service->cmds[slot].rxbuf_dma, + service->cmds[slot].rxlen, + service->cmds[slot].txbuf_dma, + service->cmds[slot].txlen); + if (!ret) + service->cmds[slot].reply_length = response.value ? 0 : response.rx_size; + spin_unlock_irqrestore(&service->lock, flags); + if (!ret) { + legacy.retcode = cpu_to_le32(response.value); + afk_recv_handle_reply(ep, frame.interface, response.id, + &legacy, sizeof(legacy)); + } else { + dev_warn(ep->dcp->dev, "AFKV2 reply rejected: %d\n", ret); + } + afk_service_put(service); +} + static void afk_recv_handle(struct apple_dcp_afkep *ep, u32 channel, u32 type, u8 *data, size_t data_size) { @@ -605,6 +853,11 @@ static void afk_recv_handle(struct apple_dcp_afkep *ep, u32 channel, u32 type, u8 *payload; size_t payload_size; + if (ep->wire_v2) { + afk_v2_receive(ep, channel, type, data, data_size); + return; + } + if (ep->dcp->fw_compat == DCP_FIRMWARE_H17P) { const struct epic_sub_hdr_h17p *c = (const struct epic_sub_hdr_h17p *)(data + sizeof(*ehdr)); @@ -895,9 +1148,9 @@ int afk_receive_message(struct apple_dcp_afkep *ep, u64 message) return 0; } -int afk_send_epic(struct apple_dcp_afkep *ep, u32 channel, u16 tag, - enum epic_type etype, enum epic_category ecat, u8 stype, - const void *payload, size_t payload_len) +static int afk_send_epic_token(struct apple_dcp_afkep *ep, u32 channel, u16 tag, + enum epic_type etype, enum epic_category ecat, u8 stype, + const void *payload, size_t payload_len, u64 token) { u32 rptr, wptr; struct afk_qe *hdr, *hdr2; @@ -905,7 +1158,10 @@ int afk_send_epic(struct apple_dcp_afkep *ep, u32 channel, u16 tag, struct epic_sub_hdr *eshdr; unsigned long flags; size_t total_epic_size, total_size; - bool compact = ep->dcp->hw.iomfb_method_profile == + u8 v2_command[AFKV2_COMMAND_SIZE + AFKV2_OOB_SIZE]; + u8 v2_category = 0; + u16 v2_flags = 0; + bool compact = !ep->wire_v2 && ep->dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL; int ret; @@ -919,7 +1175,7 @@ int afk_send_epic(struct apple_dcp_afkep *ep, u32 channel, u16 tag, * callers are optional (verbose firmware logging, EDID copy, audio, * DPTX, debugfs) or replies to calls only DPTX services make. */ - if (ep->dcp->fw_compat == DCP_FIRMWARE_H17P && !compact) { + if (ep->dcp->fw_compat == DCP_FIRMWARE_H17P && !compact && !ep->wire_v2) { dev_dbg_once(ep->dcp->dev, "AFK[ep:%02x]: not sending EPIC message on H17P\n", ep->endpoint); @@ -937,14 +1193,44 @@ int afk_send_epic(struct apple_dcp_afkep *ep, u32 channel, u16 tag, (etype == EPIC_TYPE_REPLY || etype == EPIC_TYPE_NOTIFY_ACK))))) return -EOPNOTSUPP; + if (ep->wire_v2) { + if (!channel || channel > U16_MAX || tag > U8_MAX || payload_len > SZ_1M) + return -EINVAL; + if (ecat == EPIC_CAT_REPORT) { + if ((stype != AFKV2_OPEN && stype != AFKV2_CLOSE) || payload_len) + return -EOPNOTSUPP; + v2_flags = 1; + } else if (ecat == EPIC_CAT_COMMAND) { + const struct epic_cmd *cmd = payload; + + if (stype != EPIC_SUBTYPE_STD_SERVICE || !cmd || + payload_len != sizeof(*cmd)) + return -EINVAL; + ret = afkv2_encode_oob_command(v2_command, sizeof(v2_command), tag, + le64_to_cpu(cmd->rxbuf), + le32_to_cpu(cmd->rxlen), + le64_to_cpu(cmd->txbuf), + le32_to_cpu(cmd->txlen)); + if (ret) + return ret; + payload = v2_command; + payload_len = sizeof(v2_command); + v2_category = AFKV2_COMMAND; + } else if (ecat == EPIC_CAT_REPLY) { + v2_category = AFKV2_RESPONSE; + } else { + return -EOPNOTSUPP; + } + } + spin_lock_irqsave(&ep->lock, flags); if (ep->stopping) { ret = -ESHUTDOWN; goto out; } - if (compact && (!ep->txbfr.ready || !ep->txbfr.rptr || - !ep->txbfr.wptr || !ep->txbfr.buf)) { + if ((compact || ep->wire_v2) && + (!ep->txbfr.ready || !ep->txbfr.rptr || !ep->txbfr.wptr || !ep->txbfr.buf)) { ret = -ESHUTDOWN; goto out; } @@ -954,11 +1240,11 @@ int afk_send_epic(struct apple_dcp_afkep *ep, u32 channel, u16 tag, wptr = le32_to_cpu(*ep->txbfr.wptr); trace_afk_send_rwptr_pre(ep, rptr, wptr); total_epic_size = sizeof(*ehdr) + - (compact ? sizeof(struct epic_sub_hdr_h17p) : sizeof(*eshdr)) + + ((compact || ep->wire_v2) ? sizeof(struct epic_sub_hdr_h17p) : sizeof(*eshdr)) + payload_len; total_size = sizeof(*hdr) + total_epic_size; - if (compact) { + if (compact || ep->wire_v2) { u32 available, needed; if (!ep->txbfr.ready || !ep->txbfr.stride || @@ -1061,12 +1347,21 @@ int afk_send_epic(struct apple_dcp_afkep *ep, u32 channel, u16 tag, hdr->magic = cpu_to_le32(QE_MAGIC); hdr->size = cpu_to_le32(total_epic_size); - hdr->channel = cpu_to_le32(compact ? 0 : channel); - hdr->type = cpu_to_le32(etype); + hdr->channel = cpu_to_le32((compact || ep->wire_v2) ? 0 : channel); + hdr->type = cpu_to_le32(ep->wire_v2 ? 0 : etype); if (hdr2) memcpy(hdr2, hdr, sizeof(*hdr)); - if (compact) { + if (ep->wire_v2) { + size_t written; + + ret = afkv2_encode_frame(ep->txbfr.buf + wptr, total_epic_size, + ep->qe_seq++, channel, token, stype, v2_category, + v2_flags, payload, payload_len, &written); + if (ret) + goto out; + wptr += written; + } else if (compact) { u8 *wire = ep->txbfr.buf + wptr; /* Experimental symmetry with the published H17P receive layout. */ @@ -1101,8 +1396,10 @@ int afk_send_epic(struct apple_dcp_afkep *ep, u32 channel, u16 tag, wptr += sizeof(*eshdr); } - memcpy(ep->txbfr.buf + wptr, payload, payload_len); - wptr += payload_len; + if (!ep->wire_v2) { + memcpy(ep->txbfr.buf + wptr, payload, payload_len); + wptr += payload_len; + } wptr = afk_ring_advance(wptr, 0, ep->txbfr.bufsz, ep->txbfr.stride); trace_afk_send_rwptr_post(ep, rptr, wptr); @@ -1119,6 +1416,14 @@ int afk_send_epic(struct apple_dcp_afkep *ep, u32 channel, u16 tag, return ret; } +int afk_send_epic(struct apple_dcp_afkep *ep, u32 channel, u16 tag, + enum epic_type etype, enum epic_category ecat, u8 stype, + const void *payload, size_t payload_len) +{ + return afk_send_epic_token(ep, channel, tag, etype, ecat, stype, + payload, payload_len, 0); +} + int afk_send_command(struct apple_epic_service *service, u8 type, const void *payload, size_t payload_len, void *output, size_t output_len, u32 *retcode) @@ -1151,6 +1456,12 @@ int afk_send_command_timeout(struct apple_epic_service *service, u8 type, goto err_put_service; } + if (ep->wire_v2 && (payload_len > SZ_1M || output_len > SZ_1M || + (payload_len && !payload))) { + ret = -EINVAL; + goto err_put_service; + } + rxbuf = dma_alloc_coherent(ep->dcp->dev, output_len, &rxbuf_dma, GFP_KERNEL); if (!rxbuf) { @@ -1184,11 +1495,13 @@ int afk_send_command_timeout(struct apple_epic_service *service, u8 type, goto err_unlock; } - tag = (service->cmd_tag & 0xff) << 8; - tag |= idx & 0xff; + tag = ep->wire_v2 ? afkv2_command_id(service->cmd_tag, idx) : + ((service->cmd_tag & 0xff) << 8) | idx; service->cmd_tag++; service->cmds[idx].tag = tag; + service->cmds[idx].type = type; + service->cmds[idx].reply_length = 0; service->cmds[idx].rxbuf = rxbuf; service->cmds[idx].txbuf = txbuf; service->cmds[idx].rxbuf_dma = rxbuf_dma; @@ -1234,8 +1547,14 @@ int afk_send_command_timeout(struct apple_epic_service *service, u8 type, ret = 0; if (retcode) *retcode = service->cmds[idx].retcode; - if (output && output_len) + if (ep->wire_v2 && !service->cmds[idx].retcode && + service->cmds[idx].reply_length < output_len) + ret = -EMSGSIZE; + else if (output && output_len && (!ep->wire_v2 || !service->cmds[idx].retcode)) { + if (ep->wire_v2) + dma_rmb(); memcpy(output, rxbuf, output_len); + } err_free_cmd: spin_lock_irqsave(&service->lock, flags); diff --git a/drivers/gpu/drm/apple/afk.h b/drivers/gpu/drm/apple/afk.h index 8eb797e12ef07..a4de9090681c2 100644 --- a/drivers/gpu/drm/apple/afk.h +++ b/drivers/gpu/drm/apple/afk.h @@ -25,6 +25,8 @@ struct apple_dcp_afkep; struct epic_cmd_info { u16 tag; + u8 type; + size_t reply_length; void *rxbuf; void *txbuf; @@ -295,6 +297,8 @@ struct apple_dcp_afkep { u8 experimental_rx_logs; u32 endpoint; + /* Set before afk_start; protocol selection is per endpoint. */ + bool wire_v2; struct workqueue_struct *wq; struct completion started; @@ -325,6 +329,8 @@ struct apple_dcp_afkep { struct apple_dcp_afkep *afk_init(struct apple_dcp *dcp, u32 endpoint, const struct apple_epic_service_ops *ops); +int afk_v2_copy_owned(struct apple_epic_service *service, u64 dva, + void *buffer, size_t size, bool to_peer); int afk_start(struct apple_dcp_afkep *ep); void afk_shutdown(struct apple_dcp_afkep *ep); void afk_quiesce(struct apple_dcp_afkep *ep); diff --git a/drivers/gpu/drm/apple/afkv2-codec.h b/drivers/gpu/drm/apple/afkv2-codec.h new file mode 100644 index 0000000000000..cdd5d5ad20b22 --- /dev/null +++ b/drivers/gpu/drm/apple/afkv2-codec.h @@ -0,0 +1,270 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ +/* Copyright 2026 Aurora Silicon */ +/* Wire layout: public Linux PR254 and m1n1 AFKV2 implementations. */ +#ifndef AURORA_AFKV2_CODEC_H +#define AURORA_AFKV2_CODEC_H + +#include +#include +#include + +#define AFKV2_HEADER_SIZE 24U +#define AFKV2_COMMAND_SIZE 8U +#define AFKV2_OOB_SIZE 24U +#define AFKV2_MAX_PAYLOAD 1048576U +#define AFKV2_REPORT 0U +#define AFKV2_COMMAND 1U +#define AFKV2_RESPONSE 2U +#define AFKV2_OPEN 0x12U +#define AFKV2_CLOSE 0x13U + +struct afkv2_frame { + u8 sequence; + u8 transport_flags; + u16 interface; + u64 token; + u8 type; + u8 category; + u16 interface_flags; + u32 reserved; + const u8 *payload; + size_t payload_size; + size_t padding_size; +}; + +struct afkv2_command { + u8 id; + u32 value; + bool oob; + u64 rx_address; + u64 tx_address; + u32 rx_size; + u32 tx_size; + const u8 *inline_data; + size_t inline_size; +}; + +static inline u16 afkv2_get16(const u8 *p) +{ + return (u16)p[0] | (u16)p[1] << 8; +} + +static inline u32 afkv2_get32(const u8 *p) +{ + return (u32)p[0] | (u32)p[1] << 8 | (u32)p[2] << 16 | (u32)p[3] << 24; +} + +static inline u64 afkv2_get64(const u8 *p) +{ + return (u64)afkv2_get32(p) | (u64)afkv2_get32(p + 4) << 32; +} + +static inline void afkv2_put16(u8 *p, u16 value) +{ + p[0] = value; + p[1] = value >> 8; +} + +static inline void afkv2_put32(u8 *p, u32 value) +{ + p[0] = value; + p[1] = value >> 8; + p[2] = value >> 16; + p[3] = value >> 24; +} + +static inline void afkv2_put64(u8 *p, u64 value) +{ + afkv2_put32(p, value); + afkv2_put32(p + 4, value >> 32); +} + +/* Decode metadata only. Never follow an address supplied by the peer. */ +static inline int afkv2_decode_frame(const void *record, size_t size, + bool allow_padding, struct afkv2_frame *out) +{ + const u8 *p = record; + u32 length; + struct afkv2_frame result = {0}; + + if (!record || !out || size < AFKV2_HEADER_SIZE) + return -EINVAL; + if (size > AFKV2_HEADER_SIZE + AFKV2_MAX_PAYLOAD) + return -E2BIG; + length = afkv2_get32(p + 4); + if (length < 16 || length - 16 > AFKV2_MAX_PAYLOAD || length > size - 8) + return -EINVAL; + if (!allow_padding && length != size - 8) + return -EINVAL; + if (p[17] > AFKV2_RESPONSE) + return -EPROTO; + result.sequence = p[0]; + result.transport_flags = p[1]; + result.interface = afkv2_get16(p + 2); + result.token = afkv2_get64(p + 8); + result.type = p[16]; + result.category = p[17]; + result.interface_flags = afkv2_get16(p + 18); + result.reserved = afkv2_get32(p + 20); + result.payload = p + AFKV2_HEADER_SIZE; + result.payload_size = length - 16; + result.padding_size = size - 8 - length; + *out = result; + return 0; +} + +static inline int afkv2_encode_frame(void *record, size_t capacity, u8 sequence, + u16 interface, u64 token, u8 type, u8 category, + u16 flags, const void *payload, size_t payload_size, + size_t *written) +{ + u8 *p = record; + + if (!record || !written || !interface || category > AFKV2_RESPONSE || + (payload_size && !payload)) + return -EINVAL; + if (payload_size > AFKV2_MAX_PAYLOAD) + return -E2BIG; + if (capacity < AFKV2_HEADER_SIZE || payload_size > capacity - AFKV2_HEADER_SIZE) + return -ENOSPC; + /* Move first so an in-buffer payload is safe even when it overlaps. */ + if (payload_size) + memmove(p + AFKV2_HEADER_SIZE, payload, payload_size); + memset(p, 0, AFKV2_HEADER_SIZE); + p[0] = sequence; + afkv2_put16(p + 2, interface); + afkv2_put32(p + 4, payload_size + 16); + afkv2_put64(p + 8, token); + p[16] = type; + p[17] = category; + afkv2_put16(p + 18, flags); + *written = AFKV2_HEADER_SIZE + payload_size; + return 0; +} + +static inline int afkv2_encode_open(void *record, size_t capacity, u8 sequence, + u16 interface, size_t *written) +{ + return afkv2_encode_frame(record, capacity, sequence, interface, 0, + AFKV2_OPEN, AFKV2_REPORT, 1, NULL, 0, written); +} + +static inline bool afkv2_valid_span(u64 address, u32 size) +{ + if (!size) + return !address; + return address && size <= AFKV2_MAX_PAYLOAD && + address <= ~(u64)0 - (size - 1); +} + +static inline int afkv2_encode_oob_command(void *payload, size_t capacity, + u8 id, u64 rx_address, u32 rx_size, + u64 tx_address, u32 tx_size) +{ + u8 *p = payload; + + if (!payload || !afkv2_valid_span(rx_address, rx_size) || + !afkv2_valid_span(tx_address, tx_size)) + return -EINVAL; + if (capacity < AFKV2_COMMAND_SIZE + AFKV2_OOB_SIZE) + return -ENOSPC; + memset(p, 0, AFKV2_COMMAND_SIZE + AFKV2_OOB_SIZE); + p[0] = 1; + p[1] = id; + afkv2_put32(p + 4, rx_size); + afkv2_put64(p + 8, rx_address); + afkv2_put64(p + 16, tx_address); + afkv2_put32(p + 24, rx_size); + afkv2_put32(p + 28, tx_size); + return 0; +} + +static inline int afkv2_decode_command(const void *payload, size_t size, + struct afkv2_command *out) +{ + const u8 *p = payload; + struct afkv2_command result = {0}; + size_t header = AFKV2_COMMAND_SIZE; + + if (!payload || !out || size < header || p[0] & ~1U || afkv2_get16(p + 2)) + return -EINVAL; + result.id = p[1]; + result.value = afkv2_get32(p + 4); + result.oob = p[0] & 1; + if (result.oob) { + header += AFKV2_OOB_SIZE; + if (size < header) + return -EINVAL; + result.rx_address = afkv2_get64(p + 8); + result.tx_address = afkv2_get64(p + 16); + result.rx_size = afkv2_get32(p + 24); + result.tx_size = afkv2_get32(p + 28); + /* A failed response may retain addresses with zero lengths. */ + if ((result.rx_size && !afkv2_valid_span(result.rx_address, result.rx_size)) || + (result.tx_size && !afkv2_valid_span(result.tx_address, result.tx_size))) + return -EINVAL; + } + result.inline_data = p + header; + result.inline_size = size - header; + if (result.inline_size > AFKV2_MAX_PAYLOAD) + return -E2BIG; + *out = result; + return 0; +} + +/* Correlate only; no buffer is read, written or freed by this helper. */ +static inline int afkv2_check_response(const struct afkv2_frame *frame, + const struct afkv2_command *response, + u16 interface, u8 type, u8 id, + u64 rx_address, u32 rx_capacity, + u64 tx_address, u32 tx_capacity) +{ + if (!frame || !response || frame->category != AFKV2_RESPONSE || + frame->interface != interface || frame->type != type || response->id != id) + return -EPROTO; + if (response->value) + return 0; + if (!response->oob || response->inline_size || + response->rx_address != rx_address || response->tx_address != tx_address || + response->rx_size > rx_capacity || response->tx_size > tx_capacity) + return -EPROTO; + return 0; +} + +/* Reject a peer command without touching its advertised DMA buffers. */ +static inline int afkv2_encode_error_reply(void *record, size_t capacity, u8 sequence, + const struct afkv2_frame *request, + const struct afkv2_command *command, + u32 status, size_t *written) +{ + u8 payload[AFKV2_COMMAND_SIZE + AFKV2_OOB_SIZE] = {0}; + size_t size = AFKV2_COMMAND_SIZE; + + if (!request || !command || request->category != AFKV2_COMMAND || !status) + return -EINVAL; + payload[0] = command->oob ? 1 : 0; + payload[1] = command->id; + afkv2_put32(payload + 4, status); + if (command->oob) { + afkv2_put64(payload + 8, command->rx_address); + afkv2_put64(payload + 16, command->tx_address); + size += AFKV2_OOB_SIZE; + } + return afkv2_encode_frame(record, capacity, sequence, request->interface, + request->token, request->type, AFKV2_RESPONSE, 0, + payload, size, written); +} + +static inline u8 afkv2_command_id(u8 generation, u8 slot) +{ + return ((generation & 15) << 4) | (slot & 15); +} + +static inline bool afkv2_owned_span(u64 base, size_t length, u64 address, size_t size) +{ + if (!length || base > ~(u64)0 - (length - 1)) + return false; + return address >= base && size <= length && address - base <= length - size; +} + +#endif diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index 700512815c2af..883d4f9ce2e80 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -361,8 +361,6 @@ struct apple_dcp { struct apple_dcp_afkep *dptxep; struct apple_dcp_afkep *dptx_controller_ep; struct completion dptx_controller_ready; - struct apple_dcp_afkep *dptx_v2_probe_ep; - struct completion dptx_v2_probe_ready; struct dptx_port dptxport[2]; diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 2b5b6bdef87e1..7edee07d7bb10 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -1877,7 +1877,7 @@ static struct apple_dcp_afkep *dcp_afkep(struct apple_dcp *dcp, u8 endpoint) return dcp->dptxep; case DPTX_ENDPOINT_V2: return dcp->hw.iomfb_method_profile == - DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL ? dcp->dptx_v2_probe_ep : NULL; + DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL ? dcp->dptxep : NULL; case DPTX_CONTROLLER_ENDPOINT: return dcp->dptx_controller_ep; default: @@ -1964,7 +1964,7 @@ static void dcp_recv_msg(void *cookie, u8 endpoint, u64 message) ep = dcp->dptxep; break; case DPTX_ENDPOINT_V2: - ep = dcp->dptx_v2_probe_ep; + ep = dcp->dptxep; break; case DPTX_CONTROLLER_ENDPOINT: ep = dcp->dptx_controller_ep; @@ -3609,7 +3609,6 @@ static void dcp_comp_unbind(struct device *dev, struct device *main, void *data) afk_quiesce(dcp->avep); afk_quiesce(dcp->dptxep); afk_quiesce(dcp->dptx_controller_ep); - afk_quiesce(dcp->dptx_v2_probe_ep); afk_quiesce(dcp->ibootep); afk_quiesce(dcp->systemep); afk_quiesce(dcp->dcpavservep); @@ -3646,11 +3645,6 @@ static void dcp_comp_unbind(struct device *dev, struct device *main, void *data) dcp->dptxep = NULL; } - if (!IS_ERR_OR_NULL(dcp->dptx_v2_probe_ep)) { - afk_shutdown(dcp->dptx_v2_probe_ep); - dcp->dptx_v2_probe_ep = NULL; - } - if (!IS_ERR_OR_NULL(dcp->dptx_controller_ep)) { afk_shutdown(dcp->dptx_controller_ep); dcp->dptx_controller_ep = NULL; diff --git a/drivers/gpu/drm/apple/dptxep.c b/drivers/gpu/drm/apple/dptxep.c index e00c4cbcea171..8d605beb7faeb 100644 --- a/drivers/gpu/drm/apple/dptxep.c +++ b/drivers/gpu/drm/apple/dptxep.c @@ -812,27 +812,40 @@ static int dptxport_call(struct apple_epic_service *service, u32 idx, static void dptxport_init(struct apple_epic_service *service, const char *name, const char *class, s64 unit) { + struct apple_dcp *dcp = service->ep->dcp; + s64 port = unit; + if (!name || !class) + return; if (strcmp(name, "dcpdptx-port-epic")) return; if (strcmp(class, "AppleDCPDPTXRemotePort")) return; - trace_dptxport_init(service->ep->dcp, unit); + trace_dptxport_init(dcp, unit); + if (service->ep->wire_v2) { + /* The single external service reports its physical DCP unit. */ + if (dcp->hw.num_dptx_ports != 1 || unit != dcp->index) { + dev_err(dcp->dev, "Unexpected external DPTX unit %lld\n", unit); + return; + } + port = 0; + dev_info(dcp->dev, "DPTX physical unit %lld maps to logical port 0\n", unit); + } - switch (unit) { + switch (port) { case 0: case 1: - if (service->ep->dcp->dptxport[unit].enabled) { + if (service->ep->dcp->dptxport[port].enabled) { dev_err(service->ep->dcp->dev, "DPTXPort: unit %lld already exists\n", unit); return; } - service->ep->dcp->dptxport[unit].unit = unit; - service->ep->dcp->dptxport[unit].service = service; - service->ep->dcp->dptxport[unit].enabled = true; - service->cookie = (void *)&service->ep->dcp->dptxport[unit]; - complete(&service->ep->dcp->dptxport[unit].enable_completion); + service->ep->dcp->dptxport[port].unit = unit; + service->ep->dcp->dptxport[port].service = service; + service->ep->dcp->dptxport[port].enabled = true; + service->cookie = (void *)&service->ep->dcp->dptxport[port]; + complete(&service->ep->dcp->dptxport[port].enable_completion); break; default: dev_err(service->ep->dcp->dev, "DPTXPort: invalid unit %lld\n", @@ -884,61 +897,25 @@ int dptx_controller_init(struct apple_dcp *dcp) return 0; } -static void dptx_v2_probe_service(struct apple_epic_service *service, - const char *name, const char *class, s64 unit) -{ - dev_info(service->ep->dcp->dev, - "DPTX v2 discovery name=%s class=%s unit=%lld channel=%u\n", - name ?: "(none)", class ?: "(none)", unit, service->channel); - complete(&service->ep->dcp->dptx_v2_probe_ready); -} - -static const struct apple_epic_service_ops dptx_v2_probe_ops[] = { - { - .name = "AppleDCPDPTXRemotePort", - .init = dptx_v2_probe_service, - }, - {} -}; - -static int dptx_v2_discover(struct apple_dcp *dcp) -{ - struct apple_dcp_afkep *ep; - int ret; - - init_completion(&dcp->dptx_v2_probe_ready); - ep = afk_init(dcp, DPTX_ENDPOINT_V2, dptx_v2_probe_ops); - if (IS_ERR(ep)) - return PTR_ERR(ep); - dcp->dptx_v2_probe_ep = ep; - ret = afk_start(ep); - if (ret) - return ret; - if (!wait_for_completion_timeout(&dcp->dptx_v2_probe_ready, - msecs_to_jiffies(1000))) - return -ETIMEDOUT; - - /* Discovery does not enable port callbacks or transmit service commands. */ - return -EOPNOTSUPP; -} - int dptxep_init(struct apple_dcp *dcp) { + struct apple_dcp_afkep *ep; + bool wire_v2 = dcp->hw.iomfb_method_profile == + DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL; int ret; u32 port; unsigned long timeout = msecs_to_jiffies(1000); - if (dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) - return dptx_v2_discover(dcp); - init_completion(&dcp->dptxport[0].enable_completion); init_completion(&dcp->dptxport[1].enable_completion); init_completion(&dcp->dptxport[0].linkcfg_completion); init_completion(&dcp->dptxport[1].linkcfg_completion); - dcp->dptxep = afk_init(dcp, DPTX_ENDPOINT, dptxep_ops); - if (IS_ERR(dcp->dptxep)) - return PTR_ERR(dcp->dptxep); + ep = afk_init(dcp, wire_v2 ? DPTX_ENDPOINT_V2 : DPTX_ENDPOINT, dptxep_ops); + if (IS_ERR(ep)) + return PTR_ERR(ep); + ep->wire_v2 = wire_v2; + dcp->dptxep = ep; ret = afk_start(dcp->dptxep); if (ret) diff --git a/tools/testing/selftests/drivers/gpu/apple-afkv2/Makefile b/tools/testing/selftests/drivers/gpu/apple-afkv2/Makefile new file mode 100644 index 0000000000000..65780f3d5003f --- /dev/null +++ b/tools/testing/selftests/drivers/gpu/apple-afkv2/Makefile @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: GPL-2.0-only +HOSTCC ?= cc +export HOSTCC +export AFKV2_SOURCE_ROOT := $(abspath ../../../../../..) + +TEST_PROGS := run-afkv2-tests.sh +TEST_FILES := codec.c owned-prefix.c owned-suffix.c extract-owned.py host-include + +include ../../../lib.mk diff --git a/tools/testing/selftests/drivers/gpu/apple-afkv2/README b/tools/testing/selftests/drivers/gpu/apple-afkv2/README new file mode 100644 index 0000000000000..42eb152d716a2 --- /dev/null +++ b/tools/testing/selftests/drivers/gpu/apple-afkv2/README @@ -0,0 +1,24 @@ +AFKV2 host selftests +=================== + +Run ./run-afkv2-tests.sh from this directory, or use make run_tests in a +complete kernel source checkout. HOSTCC, CFLAGS and PYTHON select host tools +(standalone CC is also accepted). +AFKV2_SANITIZERS=1 additionally enables AddressSanitizer and UBSan. +AFKV2_SOURCE_ROOT can name the source checkout when running a copied test +package; the Makefile exports it automatically for separate output builds. + +The codec tests include the production header directly and compare invented, +independently specified golden bytes. They cover truncation, category ranges, +length/padding/overflow, unaligned input, OOB descriptor ownership, response +correlation, token preservation and command ID packing. + +The owned-copy test extracts the actual production function from afk.c, prints +its SHA256, and compiles it with synthetic command slots and CPU buffers. It +checks active/inactive/done slots, high device addresses, boundaries and copies. +Locks and DMA barriers are mocked: this test does not qualify concurrency, +DMA coherency, firmware, a graphics device or any hardware display behavior. + +All build products are temporary. No device node is opened and no GPU, firmware +or target machine is contacted. The extractor fails if production function +boundaries change, requiring review rather than silently testing a stale copy. diff --git a/tools/testing/selftests/drivers/gpu/apple-afkv2/codec.c b/tools/testing/selftests/drivers/gpu/apple-afkv2/codec.c new file mode 100644 index 0000000000000..de49360bdabea --- /dev/null +++ b/tools/testing/selftests/drivers/gpu/apple-afkv2/codec.c @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +/* Copyright 2026 Aurora Silicon */ +#undef NDEBUG +#include +#include +#include +#include +#include "afkv2-codec.h" + +static unsigned int assertions; +#define CHECK(x) \ + do { \ + assertions++; \ + assert(x); \ + } while (0) + +/* Invented fixtures with independently specified wire bytes; no device data. */ +static const u8 open_fixture[24] = { 0x7b, 0, 0x34, 0x12, 0x10, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0x12, 0, 1, 0, 0, 0, 0, 0 }; +static const u8 descriptor_fixture[32] = { + 1, 0xab, 0, 0, 0x40, 0, 0, 0, 0x88, 0x77, 0x66, + 0x55, 0x44, 0x33, 0x22, 0x11, 8, 7, 6, 5, 4, 3, + 2, 1, 0x40, 0, 0, 0, 0x10, 0, 0, 0 +}; + +int main(void) +{ + u8 data[128], command[32], unaligned[129], saved[128]; + struct afkv2_frame frame, reply; + struct afkv2_command parsed, response; + size_t written = 999; + int ret; + + memset(data, 0xa5, sizeof(data)); + CHECK(!afkv2_encode_open(data, sizeof(data), 0x7b, 0x1234, &written)); + CHECK(written == sizeof(open_fixture)); + CHECK(!memcmp(data, open_fixture, sizeof(open_fixture))); + CHECK(data[24] == 0xa5); + CHECK(!afkv2_decode_frame(data, written, false, &frame)); + CHECK(frame.interface == 0x1234 && frame.type == AFKV2_OPEN); + CHECK(frame.category == AFKV2_REPORT && frame.interface_flags == 1); + CHECK(frame.payload_size == 0 && frame.padding_size == 0); + memcpy(unaligned + 1, open_fixture, sizeof(open_fixture)); + CHECK(!afkv2_decode_frame(unaligned + 1, sizeof(open_fixture), false, + &frame)); + CHECK(afkv2_decode_frame(data, SIZE_MAX, true, &frame) == -E2BIG); + CHECK(afkv2_decode_frame(NULL, 24, false, &frame) == -EINVAL); + CHECK(afkv2_decode_frame(data, 24, false, NULL) == -EINVAL); + for (size_t i = 0; i < 24; i++) + CHECK(afkv2_decode_frame(data, i, false, &frame) == -EINVAL); + CHECK(afkv2_decode_frame(data, 32, false, &frame) == -EINVAL); + CHECK(!afkv2_decode_frame(data, 32, true, &frame) && + frame.padding_size == 8); + for (u32 i = 0; i < 16; i++) { + afkv2_put32(data + 4, i); + CHECK(afkv2_decode_frame(data, 24, false, &frame) == -EINVAL); + } + afkv2_put32(data + 4, 17); + CHECK(afkv2_decode_frame(data, 24, false, &frame) == -EINVAL); + afkv2_put32(data + 4, UINT32_MAX); + CHECK(afkv2_decode_frame(data, 24, false, &frame) == -EINVAL); + memcpy(data, open_fixture, sizeof(open_fixture)); + for (unsigned int category = 0; category < 256; category++) { + data[17] = category; + ret = afkv2_decode_frame(data, 24, false, &frame); + CHECK(category <= 2 ? ret == 0 : ret == -EPROTO); + } + CHECK(!afkv2_encode_oob_command(command, sizeof(command), 0xab, + 0x1122334455667788ULL, 64, + 0x0102030405060708ULL, 16)); + CHECK(!memcmp(command, descriptor_fixture, sizeof(command))); + CHECK(!afkv2_decode_command(command, sizeof(command), &parsed)); + CHECK(parsed.oob && parsed.id == 0xab && parsed.value == 64); + CHECK(parsed.rx_address == 0x1122334455667788ULL && + parsed.tx_size == 16); + CHECK(parsed.inline_size == 0); + for (size_t i = 0; i < sizeof(command); i++) + CHECK(afkv2_decode_command(command, i, &response) == -EINVAL); + command[0] = 2; + CHECK(afkv2_decode_command(command, sizeof(command), &response) == + -EINVAL); + memcpy(command, descriptor_fixture, sizeof(command)); + command[2] = 1; + CHECK(afkv2_decode_command(command, sizeof(command), &response) == + -EINVAL); + memcpy(command, descriptor_fixture, sizeof(command)); + CHECK(afkv2_encode_oob_command(command, 31, 0, 1, 1, 2, 1) == -ENOSPC); + CHECK(afkv2_encode_oob_command(command, 32, 0, UINT64_MAX, 2, 0, 0) == + -EINVAL); + CHECK(afkv2_encode_oob_command(command, 32, 0, 1, AFKV2_MAX_PAYLOAD + 1, + 0, 0) == -EINVAL); + CHECK(afkv2_encode_oob_command(command, 32, 0, 1, 0, 0, 0) == -EINVAL); + memcpy(command, descriptor_fixture, sizeof(command)); + CHECK(!afkv2_encode_frame(data, sizeof(data), 1, 0x1234, + 0xaabbccddeeff0011ULL, 0xc0, AFKV2_COMMAND, 0, + command, sizeof(command), &written)); + CHECK(written == 56 && afkv2_get32(data + 4) == 48); + CHECK(!afkv2_decode_frame(data, written, false, &frame)); + CHECK(frame.token == 0xaabbccddeeff0011ULL && + frame.category == AFKV2_COMMAND); + CHECK(!afkv2_encode_error_reply(unaligned, sizeof(unaligned), 2, &frame, + &parsed, 0xe00002c7U, &written)); + CHECK(!afkv2_decode_frame(unaligned, written, false, &reply)); + CHECK(reply.token == frame.token && reply.interface == frame.interface); + CHECK(reply.type == frame.type && reply.category == AFKV2_RESPONSE); + CHECK(!afkv2_decode_command(reply.payload, reply.payload_size, + &response)); + CHECK(response.value == 0xe00002c7U && response.rx_size == 0 && + response.tx_size == 0); + CHECK(response.rx_address == parsed.rx_address && + response.tx_address == parsed.tx_address); + CHECK(!afkv2_check_response(&reply, &response, 0x1234, 0xc0, 0xab, + parsed.rx_address, 64, parsed.tx_address, + 16)); + response.value = 0; + response.rx_size = 64; + response.tx_size = 16; + CHECK(!afkv2_check_response(&reply, &response, 0x1234, 0xc0, 0xab, + parsed.rx_address, 64, parsed.tx_address, + 16)); + response.rx_address++; + CHECK(afkv2_check_response(&reply, &response, 0x1234, 0xc0, 0xab, + parsed.rx_address, 64, parsed.tx_address, + 16) == -EPROTO); + response.rx_address--; + response.rx_size++; + CHECK(afkv2_check_response(&reply, &response, 0x1234, 0xc0, 0xab, + parsed.rx_address, 64, parsed.tx_address, + 16) == -EPROTO); + response.rx_size--; + CHECK(afkv2_check_response(&reply, &response, 0x1235, 0xc0, 0xab, + parsed.rx_address, 64, parsed.tx_address, + 16) == -EPROTO); + CHECK(afkv2_check_response(&reply, &response, 0x1234, 0xc1, 0xab, + parsed.rx_address, 64, parsed.tx_address, + 16) == -EPROTO); + CHECK(afkv2_check_response(&reply, &response, 0x1234, 0xc0, 0xaa, + parsed.rx_address, 64, parsed.tx_address, + 16) == -EPROTO); + memset(data, 0xa5, sizeof(data)); + memcpy(saved, data, sizeof(data)); + CHECK(afkv2_encode_open(data, 23, 0, 1, &written) == -ENOSPC); + CHECK(!memcmp(data, saved, sizeof(data))); + CHECK(afkv2_encode_open(data, sizeof(data), 0, 0, &written) == -EINVAL); + CHECK(!memcmp(data, saved, sizeof(data))); + CHECK(afkv2_encode_frame(data, sizeof(data), 0, 1, 0, 0, 0, 0, data, + SIZE_MAX, &written) == -E2BIG); + CHECK(!memcmp(data, saved, sizeof(data))); + u8 *large = malloc(AFKV2_HEADER_SIZE + AFKV2_MAX_PAYLOAD); + + CHECK(large); + memset(large, 0x5a, AFKV2_HEADER_SIZE + AFKV2_MAX_PAYLOAD); + CHECK(!afkv2_encode_frame(large, AFKV2_HEADER_SIZE + AFKV2_MAX_PAYLOAD, + 0, 1, 0, 0xc0, 1, 0, large, AFKV2_MAX_PAYLOAD, + &written)); + CHECK(!afkv2_decode_frame(large, written, false, &frame)); + CHECK(frame.payload_size == AFKV2_MAX_PAYLOAD && + frame.payload[0] == 0x5a); + free(large); + bool ids[256] = { 0 }; + + for (unsigned int generation = 0; generation < 16; generation++) { + for (unsigned int slot = 0; slot < 16; slot++) { + u8 id = afkv2_command_id(generation, slot); + + CHECK(!ids[id]); + ids[id] = true; + CHECK((id & 15) == slot); + } + } + CHECK(afkv2_command_id(16, 3) == afkv2_command_id(0, 3)); + CHECK(afkv2_owned_span(0x1000, 32, 0x1000, 32)); + CHECK(afkv2_owned_span(0x1000, 32, 0x101c, 4)); + CHECK(!afkv2_owned_span(0x1000, 32, 0x0fff, 1)); + CHECK(!afkv2_owned_span(0x1000, 32, 0x101d, 4)); + CHECK(!afkv2_owned_span(0x1000, 32, 0x1020, 1)); + CHECK(!afkv2_owned_span(0x1000, 0, 0x1000, 0)); + CHECK(afkv2_owned_span(UINT64_MAX - 15, 16, UINT64_MAX, 1)); + CHECK(!afkv2_owned_span(UINT64_MAX - 15, 17, UINT64_MAX, 1)); + printf("PASS %u assertions\n", assertions); + return 0; +} diff --git a/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-owned.py b/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-owned.py new file mode 100644 index 0000000000000..21fe63b6d3823 --- /dev/null +++ b/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-owned.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +"""Compile the production ownership checker with host-only synchronization mocks.""" +import hashlib +from pathlib import Path +import sys + + +def main(): + source = Path(sys.argv[1]).read_text(encoding="utf-8") + output = Path(sys.argv[2]) + first = "static int afk_v2_access_owned(" + last = "static void afk_v2_callback(" + if source.count(first) != 1 or source.count(last) != 1: + raise SystemExit("FAIL: ownership function boundaries changed; review extractor") + start = source.index(first) + end = source.index(last, start) + body = source[start:end] + if "int afk_v2_copy_owned(" not in body: + raise SystemExit("FAIL: production ownership wrapper missing") + directory = Path(__file__).resolve().parent + output.write_text((directory / "owned-prefix.c").read_text() + body + + (directory / "owned-suffix.c").read_text(), encoding="utf-8") + print("Production ownership function SHA256:", hashlib.sha256(body.encode()).hexdigest()) + + +if __name__ == "__main__": + main() diff --git a/tools/testing/selftests/drivers/gpu/apple-afkv2/host-include/linux/errno.h b/tools/testing/selftests/drivers/gpu/apple-afkv2/host-include/linux/errno.h new file mode 100644 index 0000000000000..a5c3a3f44a591 --- /dev/null +++ b/tools/testing/selftests/drivers/gpu/apple-afkv2/host-include/linux/errno.h @@ -0,0 +1,5 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ +#define EINVAL 22 +#define ENOSPC 28 +#define E2BIG 7 +#define EPROTO 71 diff --git a/tools/testing/selftests/drivers/gpu/apple-afkv2/host-include/linux/string.h b/tools/testing/selftests/drivers/gpu/apple-afkv2/host-include/linux/string.h new file mode 100644 index 0000000000000..e74214a66ed83 --- /dev/null +++ b/tools/testing/selftests/drivers/gpu/apple-afkv2/host-include/linux/string.h @@ -0,0 +1,2 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ +#include diff --git a/tools/testing/selftests/drivers/gpu/apple-afkv2/host-include/linux/types.h b/tools/testing/selftests/drivers/gpu/apple-afkv2/host-include/linux/types.h new file mode 100644 index 0000000000000..4d490ef828ccd --- /dev/null +++ b/tools/testing/selftests/drivers/gpu/apple-afkv2/host-include/linux/types.h @@ -0,0 +1,8 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ +#include +#include +#include +typedef uint8_t u8; +typedef uint16_t u16; +typedef uint32_t u32; +typedef uint64_t u64; diff --git a/tools/testing/selftests/drivers/gpu/apple-afkv2/owned-prefix.c b/tools/testing/selftests/drivers/gpu/apple-afkv2/owned-prefix.c new file mode 100644 index 0000000000000..8ff88ec1ec5b0 --- /dev/null +++ b/tools/testing/selftests/drivers/gpu/apple-afkv2/owned-prefix.c @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +/* Copyright 2026 Aurora Silicon */ +#undef NDEBUG +#include +#include +#include "afkv2-codec.h" +#define MAX_PENDING_CMDS 16 +#define SZ_1M 1048576U +#define EACCES 13 +#define spin_lock_irqsave(lock, flags) \ + do { \ + (void)(lock); \ + (flags) = 0; \ + } while (0) +#define spin_unlock_irqrestore(lock, flags) \ + do { \ + (void)(lock); \ + (void)(flags); \ + } while (0) +#define dma_rmb() \ + do { \ + } while (0) +#define dma_wmb() \ + do { \ + } while (0) +#define dev_warn_ratelimited(...) \ + do { \ + } while (0) +static int test_bit(unsigned int bit, const unsigned long *map) +{ + return !!(*map & (1UL << bit)); +} + +struct epic_cmd_info { + bool done; + void *rxbuf, *txbuf; + u64 rxbuf_dma, txbuf_dma; + size_t rxlen, txlen; +}; + +struct apple_dcp_afkep { + bool wire_v2; +}; + +struct apple_epic_service { + int lock; + struct apple_dcp_afkep *ep; + unsigned long cmd_map[1]; + struct epic_cmd_info cmds[MAX_PENDING_CMDS]; +}; diff --git a/tools/testing/selftests/drivers/gpu/apple-afkv2/owned-suffix.c b/tools/testing/selftests/drivers/gpu/apple-afkv2/owned-suffix.c new file mode 100644 index 0000000000000..770b16bc2fa94 --- /dev/null +++ b/tools/testing/selftests/drivers/gpu/apple-afkv2/owned-suffix.c @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +/* Copyright 2026 Aurora Silicon */ +int main(void) +{ + u8 rx[16], tx[16], output[16]; + struct apple_dcp_afkep ep = { .wire_v2 = true }; + struct apple_epic_service service = { .ep = &ep }; + struct epic_cmd_info *cmd = &service.cmds[0]; + + memset(rx, 0x12, sizeof(rx)); + memset(tx, 0x34, sizeof(tx)); + memset(output, 0x56, sizeof(output)); + cmd->rxbuf = rx; + cmd->rxbuf_dma = 0x10000001000ULL; + cmd->rxlen = sizeof(rx); + cmd->txbuf = tx; + cmd->txbuf_dma = 0x10000002000ULL; + cmd->txlen = sizeof(tx); + assert(afk_v2_copy_owned(&service, cmd->rxbuf_dma, output, 4, false) == + -EACCES); + service.cmd_map[0] = 1; + assert(afk_v2_copy_owned(&service, cmd->rxbuf_dma + 12, output, 4, + false) == 0); + assert(output[0] == 0x12 && output[3] == 0x12 && output[4] == 0x56); + assert(afk_v2_copy_owned(&service, cmd->txbuf_dma + 4, output, 4, + false) == 0); + assert(output[0] == 0x34 && output[4] == 0x56); + memset(output, 0x78, 4); + assert(afk_v2_copy_owned(&service, cmd->rxbuf_dma + 8, output, 4, + true) == 0); + assert(rx[7] == 0x12 && rx[8] == 0x78 && rx[11] == 0x78 && + rx[12] == 0x12); + assert(afk_v2_copy_owned(&service, cmd->rxbuf_dma - 1, output, 1, + true) == -EACCES); + assert(afk_v2_copy_owned(&service, cmd->rxbuf_dma + 13, output, 4, + true) == -EACCES); + assert(afk_v2_copy_owned(&service, cmd->txbuf_dma + 16, output, 1, + true) == -EACCES); + cmd->done = true; + assert(afk_v2_copy_owned(&service, cmd->rxbuf_dma, output, 1, false) == + -EACCES); + cmd->done = false; + ep.wire_v2 = false; + assert(afk_v2_copy_owned(&service, cmd->rxbuf_dma, output, 1, false) == + -EINVAL); + ep.wire_v2 = true; + assert(afk_v2_copy_owned(&service, cmd->rxbuf_dma, NULL, 1, false) == + -EINVAL); + assert(afk_v2_copy_owned(&service, 0, NULL, 0, false) == 0); + assert(afk_v2_access_owned(&service, cmd->rxbuf_dma + 1, NULL, 15, true, + false) == 0); + assert(afk_v2_access_owned(&service, cmd->rxbuf_dma + 1, NULL, 16, true, + false) == -EACCES); + puts("PASS owned-copy boundaries (actual extracted function; lock/barrier mocks)"); + return 0; +} diff --git a/tools/testing/selftests/drivers/gpu/apple-afkv2/run-afkv2-tests.sh b/tools/testing/selftests/drivers/gpu/apple-afkv2/run-afkv2-tests.sh new file mode 100755 index 0000000000000..4e13f6d05d986 --- /dev/null +++ b/tools/testing/selftests/drivers/gpu/apple-afkv2/run-afkv2-tests.sh @@ -0,0 +1,40 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0-only +set -eu + +afkv2_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +afkv2_root=${AFKV2_SOURCE_ROOT:-$(CDPATH= cd -- "$afkv2_dir/../../../../../.." && pwd)} +afkv2_cc=${HOSTCC:-${CC:-cc}} +afkv2_cc_command=${afkv2_cc%% *} +afkv2_python=${PYTHON:-python3} +if ! command -v "$afkv2_cc_command" >/dev/null 2>&1 || + ! command -v "$afkv2_python" >/dev/null 2>&1; then + echo "SKIP: a host C compiler and Python 3 are required" + exit 4 +fi +if [ ! -f "$afkv2_root/drivers/gpu/drm/apple/afkv2-codec.h" ]; then + echo "SKIP: run from a source tree containing the AFKV2 codec" + exit 4 +fi +afkv2_tmp=$(mktemp -d "${TMPDIR:-/tmp}/apple-afkv2.XXXXXX") +trap 'rm -rf -- "$afkv2_tmp"' EXIT +trap 'exit 1' HUP INT TERM +afkv2_sanitizers= +if [ "${AFKV2_SANITIZERS:-0}" = 1 ]; then + afkv2_sanitizers="-fsanitize=address,undefined -fno-omit-frame-pointer" +fi + +# Compiler arguments, CFLAGS and sanitizers use normal compiler word splitting. +# shellcheck disable=SC2086 +$afkv2_cc -std=c11 -Wall -Wextra -Werror -pedantic ${CFLAGS:-} $afkv2_sanitizers \ + -I"$afkv2_dir/host-include" -I"$afkv2_root/drivers/gpu/drm/apple" \ + "$afkv2_dir/codec.c" -o "$afkv2_tmp/codec" +"$afkv2_tmp/codec" +"$afkv2_python" "$afkv2_dir/extract-owned.py" \ + "$afkv2_root/drivers/gpu/drm/apple/afk.c" "$afkv2_tmp/owned.c" +# The extracted kernel function uses GNU C void-pointer arithmetic. +# shellcheck disable=SC2086 +$afkv2_cc -std=gnu11 -Wall -Wextra -Werror ${CFLAGS:-} $afkv2_sanitizers \ + -I"$afkv2_dir/host-include" -I"$afkv2_root/drivers/gpu/drm/apple" \ + "$afkv2_tmp/owned.c" -o "$afkv2_tmp/owned" +"$afkv2_tmp/owned" From b4926f7a70ed407ff316e8ab339623e7cb62d219 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 17:44:13 +1100 Subject: [PATCH 21/52] dt-bindings: phy: apple,atcphy: describe T8140 DP setup lists Document the three bounded calibration lists used for T8140 DisplayPort setup. Describe their register regions and ordering, and reject these properties on other PHY variants. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- .../devicetree/bindings/phy/apple,atcphy.yaml | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Documentation/devicetree/bindings/phy/apple,atcphy.yaml b/Documentation/devicetree/bindings/phy/apple,atcphy.yaml index 2484121870400..fef0605343e8f 100644 --- a/Documentation/devicetree/bindings/phy/apple,atcphy.yaml +++ b/Documentation/devicetree/bindings/phy/apple,atcphy.yaml @@ -50,6 +50,9 @@ allOf: reg-names: maxItems: 5 apple,tunable-usb2phy-reg-dflt: false + apple,tunable-dp-common-pre: false + apple,tunable-dp-lpdptx: false + apple,tunable-dp-common-post: false $defs: apple,tunable: @@ -179,6 +182,27 @@ properties: Note that lane here refers to a USB RX and TX pair re-used for DisplayPort and not to an individual DisplayPort differential lane. + apple,tunable-dp-common-pre: + $ref: "#/$defs/apple,tunable" + maxItems: 5461 + description: > + T8140 DisplayPort core setup applied before the LPDPTX list. The + bootloader supplies device-specific values for the core register region. + + apple,tunable-dp-lpdptx: + $ref: "#/$defs/apple,tunable" + maxItems: 5461 + description: > + T8140 DisplayPort setup for the lpdptx register region, applied between + the common-pre and common-post lists. + + apple,tunable-dp-common-post: + $ref: "#/$defs/apple,tunable" + maxItems: 5461 + description: > + T8140 DisplayPort core setup applied after the LPDPTX list. All three + DisplayPort lists are needed before selecting a DisplayPort PHY mode. + apple,tunable-usb2phy-reg-dflt: $ref: "#/$defs/apple,tunable" description: > From 7f67eba0ba5fe4a99b9ac9925dcdcf2a4a9e927b Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 17:44:13 +1100 Subject: [PATCH 22/52] phy: apple: apply the T8140 DisplayPort setup lists The T8140 variant currently returns from AUX setup without configuring it. The bootloader supplies three ordered DisplayPort setup lists, but the PHY driver does not read or apply them. Validate every list against its mapped register resource and apply the complete bundle at AUX setup. Reject DP selection if the bundle is missing or invalid, while keeping ordinary USB operation independent. Other PHY generations keep their existing setup paths. Drive-level training programming remains unchanged. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/phy/apple/atc-dp-tunables.h | 13 ++++++ drivers/phy/apple/atc.c | 68 ++++++++++++++++++++++++++++- 2 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 drivers/phy/apple/atc-dp-tunables.h diff --git a/drivers/phy/apple/atc-dp-tunables.h b/drivers/phy/apple/atc-dp-tunables.h new file mode 100644 index 0000000000000..6d0cb00158c71 --- /dev/null +++ b/drivers/phy/apple/atc-dp-tunables.h @@ -0,0 +1,13 @@ +/* SPDX-License-Identifier: GPL-2.0 OR BSD-2-Clause */ +/* Copyright 2026 Aurora Silicon */ +#ifndef APPLE_ATC_DP_TUNABLES_H +#define APPLE_ATC_DP_TUNABLES_H + +#include + +static inline bool atc_dp_list_length_valid(int length) +{ + return length >= 0 && length <= 65536 && length % 12 == 0; +} + +#endif diff --git a/drivers/phy/apple/atc.c b/drivers/phy/apple/atc.c index a5f8b7c0d5e5c..2fec5c7821388 100644 --- a/drivers/phy/apple/atc.c +++ b/drivers/phy/apple/atc.c @@ -46,6 +46,8 @@ #include #include +#include "atc-dp-tunables.h" + #define AUSPLL_FSM_CTRL 0x1014 #define AUSPLL_APB_CMD_OVERRIDE 0x2000 @@ -617,6 +619,7 @@ struct atcphy_mode_configuration { * @pipe_mux_clk: PIPE clock selection field mask * @pipe_mux_data: PIPE data selection field mask * @has_usb2phy_reg: The PHY has the secondary eUSB2 register bank (T8140) + * @has_t8140_dp: Uses the T8140 DisplayPort tunable bundle * @optional_tunables: The bootloader may leave out the common-a tunables (this * generation has none) and the SuperSpeed tunables; USB2 * still works without the latter @@ -630,6 +633,7 @@ struct atcphy_hw { u32 pipe_mux_data; bool has_usb2phy_reg; bool optional_tunables; + bool has_t8140_dp; }; /** @@ -642,9 +646,13 @@ struct atcphy_hw { * @tunables.lane_usb3: USB3 lane-specific tunables * @tunables.lane_dp: DisplayPort lane-specific tunables * @tunables.lane_usb4: USB4 lane-specific tunables + * @tunables.dp_common_pre: T8140 DisplayPort common setup + * @tunables.dp_lpdptx: T8140 DisplayPort LPDPTX setup + * @tunables.dp_common_post: T8140 DisplayPort common completion * @tunables.usb2phy_reg_dflt: Defaults for the secondary eUSB2 register bank * @hw: SoC-specific PHY description * @ss_tunables: The complete SuperSpeed tunable set was supplied + * @dp_tunables: The complete T8140 DisplayPort bundle was supplied * @fixed_usb2: The USB2 pairs go to a fixed hub on the USB controller, which * stays in host mode; the PHY provides USB2 from probe on * @typec_mode: Mode the Type-C mux last asked for; a fixed-hub port returns to @@ -671,6 +679,7 @@ struct atcphy_hw { * @regs.lpdptx: DisplayPort registers * @res: Resources for memory-mapped registers, used to verify that tunables aren't out of bounds * @res.core: Core register resource + * @res.lpdptx: DisplayPort register resource * @res.axi2af: AXI to Apple Fabric interface resource * @res.usb2phy_reg: Secondary eUSB2 register bank resource * @phys: PHY instances @@ -694,10 +703,14 @@ struct apple_atcphy { struct apple_tunable *lane_dp[2]; struct apple_tunable *lane_usb4[2]; struct apple_tunable *usb2phy_reg_dflt; + struct apple_tunable *dp_common_pre; + struct apple_tunable *dp_lpdptx; + struct apple_tunable *dp_common_post; } tunables; const struct atcphy_hw *hw; bool ss_tunables; + bool dp_tunables; bool fixed_usb2; enum atcphy_mode typec_mode; bool host_active; @@ -724,6 +737,7 @@ struct apple_atcphy { struct { struct resource *core; + struct resource *lpdptx; struct resource *axi2af; struct resource *usb2phy_reg; } res; @@ -1482,6 +1496,13 @@ static void atcphy_configure_lanes(struct apple_atcphy *atcphy, enum atcphy_mode static void atcphy_enable_dp_aux(struct apple_atcphy *atcphy) { + if (atcphy->hw->has_t8140_dp) { + apple_tunable_apply(atcphy->regs.core, atcphy->tunables.dp_common_pre); + apple_tunable_apply(atcphy->regs.lpdptx, atcphy->tunables.dp_lpdptx); + apple_tunable_apply(atcphy->regs.core, atcphy->tunables.dp_common_post); + return; + } + /* FIXME */ if (atcphy->hw->gen == ATCPHY_GENERATION_T8122) return; @@ -2523,6 +2544,9 @@ static int atcphy_configure(struct apple_atcphy *atcphy, enum atcphy_mode mode) u32 reg; lockdep_assert_held(&atcphy->lock); + if (atcphy->hw->has_t8140_dp && atcphy_modes[mode].enable_dp_aux && + !atcphy->dp_tunables) + return -EOPNOTSUPP; /* the mode change below may power the PHY down: stop the tunnel clock first */ atc_tunnel_stop_t8103(atcphy); atc_tunnel_restore(atcphy); @@ -3087,6 +3111,9 @@ static int atcphy_mux_set(struct typec_mux_dev *mux, struct typec_mux_state *sta case APPLE_ATCPHY_MODE_USB3: case APPLE_ATCPHY_MODE_USB3_DP: case APPLE_ATCPHY_MODE_DP: + if (atcphy->hw->has_t8140_dp && atcphy_modes[target_mode].enable_dp_aux && + !atcphy->dp_tunables) + return -EOPNOTSUPP; /* Without the lane tunables the SuperSpeed lanes are not calibrated */ if (!atcphy->ss_tunables) return -EOPNOTSUPP; @@ -3151,6 +3178,37 @@ static int atcphy_probe_mux(struct apple_atcphy *atcphy) return devm_add_action_or_reset(atcphy->dev, atcphy_typec_mux_unregister, mux); } +static int atcphy_load_t8140_dp(struct apple_atcphy *atcphy) +{ + struct { + const char *name; + struct resource *resource; + struct apple_tunable **out; + } lists[] = { + { "apple,tunable-dp-common-pre", atcphy->res.core, + &atcphy->tunables.dp_common_pre }, + { "apple,tunable-dp-lpdptx", atcphy->res.lpdptx, + &atcphy->tunables.dp_lpdptx }, + { "apple,tunable-dp-common-post", atcphy->res.core, + &atcphy->tunables.dp_common_post }, + }; + if (!of_machine_is_compatible("apple,j700")) + return -EOPNOTSUPP; + for (unsigned int i = 0; i < ARRAY_SIZE(lists); i++) { + struct property *property = of_find_property(atcphy->np, lists[i].name, NULL); + + if (!property || !atc_dp_list_length_valid(property->length)) + return -EINVAL; + *lists[i].out = devm_apple_tunable_parse(atcphy->dev, atcphy->np, + lists[i].name, lists[i].resource); + if (IS_ERR(*lists[i].out)) + return PTR_ERR(*lists[i].out); + } + + atcphy->dp_tunables = true; + return 0; +} + static int atcphy_load_tunables(struct apple_atcphy *atcphy) { size_t tunable_count; @@ -3215,6 +3273,13 @@ static int atcphy_load_tunables(struct apple_atcphy *atcphy) atcphy->tunables.usb2phy_reg_dflt = tunable; } + if (atcphy->hw->has_t8140_dp) { + int ret = atcphy_load_t8140_dp(atcphy); + + if (ret) + dev_warn(atcphy->dev, "DisplayPort tunable bundle unavailable: %d\n", ret); + } + atcphy->ss_tunables = !ss_missing; if (ss_missing) dev_warn(atcphy->dev, "SuperSpeed tunables missing, USB2 only\n"); @@ -3230,7 +3295,7 @@ static int atcphy_map_resources(struct platform_device *pdev, struct apple_atcph struct resource **res; } resources[] = { { "core", &atcphy->regs.core, &atcphy->res.core }, - { "lpdptx", &atcphy->regs.lpdptx, NULL }, + { "lpdptx", &atcphy->regs.lpdptx, &atcphy->res.lpdptx }, { "axi2af", &atcphy->regs.axi2af, &atcphy->res.axi2af }, { "usb2phy", &atcphy->regs.usb2phy, NULL }, { "pipehandler", &atcphy->regs.pipehandler, NULL }, @@ -3399,6 +3464,7 @@ static const struct atcphy_hw atcphy_hw_t8140 = { .pipe_mux_clk = PIPEHANDLER_MUX_CTRL_CLK_T8103, .pipe_mux_data = PIPEHANDLER_MUX_CTRL_DATA_T8103, .has_usb2phy_reg = true, + .has_t8140_dp = true, .optional_tunables = true, }; From f019831620ddbe8d8eea7173cab766c21154cc97 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 17:54:12 +1100 Subject: [PATCH 23/52] drm/apple: open the version-two DPTX controller interfaces The experimental external profile uses AFKV2 for its DPTX port, but the companion controller endpoint still uses the legacy path. Its interfaces are discovered without receiving the version-two OPEN handshake. Select AFKV2 for the controller before starting it, so controller publication performs the same OPEN-before-ready sequence as the port. Add a one-time PHY setup message to distinguish successful calibration application from controller activation during hardware qualification. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/dptxep.c | 3 +++ drivers/phy/apple/atc.c | 1 + 2 files changed, 4 insertions(+) diff --git a/drivers/gpu/drm/apple/dptxep.c b/drivers/gpu/drm/apple/dptxep.c index 8d605beb7faeb..856a9a75a389d 100644 --- a/drivers/gpu/drm/apple/dptxep.c +++ b/drivers/gpu/drm/apple/dptxep.c @@ -885,6 +885,9 @@ int dptx_controller_init(struct apple_dcp *dcp) ep = afk_init(dcp, DPTX_CONTROLLER_ENDPOINT, dptx_controller_ops); if (IS_ERR(ep)) return PTR_ERR(ep); + /* Controller interfaces require the same OPEN handshake as the port. */ + ep->wire_v2 = dcp->hw.iomfb_method_profile == + DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL; dcp->dptx_controller_ep = ep; ret = afk_start(ep); diff --git a/drivers/phy/apple/atc.c b/drivers/phy/apple/atc.c index 2fec5c7821388..6a2020e36eed2 100644 --- a/drivers/phy/apple/atc.c +++ b/drivers/phy/apple/atc.c @@ -1500,6 +1500,7 @@ static void atcphy_enable_dp_aux(struct apple_atcphy *atcphy) apple_tunable_apply(atcphy->regs.core, atcphy->tunables.dp_common_pre); apple_tunable_apply(atcphy->regs.lpdptx, atcphy->tunables.dp_lpdptx); apple_tunable_apply(atcphy->regs.core, atcphy->tunables.dp_common_post); + dev_info_once(atcphy->dev, "Applied T8140 DisplayPort setup lists\n"); return; } From 294891122c094263fa45fc23404064b553e0b8ed Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 17:59:36 +1100 Subject: [PATCH 24/52] usb: typec: expose mux callback results through dynamic debug A DisplayPort mode can reach the display route without the PHY setup function being called. Add default-off dynamic-debug messages for each mux callback result and for the ATC requested, current and target modes. This distinguishes callback dispatch, unavailable setup data and an unchanged PHY mode without accessing additional hardware registers. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/phy/apple/atc.c | 4 ++++ drivers/usb/typec/mux.c | 1 + 2 files changed, 5 insertions(+) diff --git a/drivers/phy/apple/atc.c b/drivers/phy/apple/atc.c index 6a2020e36eed2..e5e26712c5a4d 100644 --- a/drivers/phy/apple/atc.c +++ b/drivers/phy/apple/atc.c @@ -3040,6 +3040,9 @@ static int atcphy_mux_set(struct typec_mux_dev *mux, struct typec_mux_state *sta int ret; guard(mutex)(&atcphy->lock); + dev_dbg(atcphy->dev, "mux mode=%lu svid=%04x current=%u fixed=%u dp=%u ss=%u\n", + state->mode, state->alt ? state->alt->svid : 0, atcphy->mode, + atcphy->fixed_usb2, atcphy->dp_tunables, atcphy->ss_tunables); if (state->mode == TYPEC_STATE_SAFE) { target_mode = APPLE_ATCPHY_MODE_OFF; @@ -3100,6 +3103,7 @@ static int atcphy_mux_set(struct typec_mux_dev *mux, struct typec_mux_state *sta if (atcphy->fixed_usb2 && target_mode == APPLE_ATCPHY_MODE_OFF) target_mode = APPLE_ATCPHY_MODE_USB2; + dev_dbg(atcphy->dev, "mux target=%u\n", target_mode); if (atcphy->mode == target_mode) return 0; diff --git a/drivers/usb/typec/mux.c b/drivers/usb/typec/mux.c index 7e327118cbd71..dc763766012f7 100644 --- a/drivers/usb/typec/mux.c +++ b/drivers/usb/typec/mux.c @@ -388,6 +388,7 @@ int typec_mux_set(struct typec_mux *mux, struct typec_mux_state *state) mux_dev = mux->mux_devs[i]; ret = mux_dev->set(mux_dev, state); + dev_dbg(&mux_dev->dev, "mode=%lu callback result=%d\n", state->mode, ret); if (ret && ret != -EOPNOTSUPP) return ret; } From 0be54abf018f7b3a2deee17f8eec50536de3b063 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 18:05:27 +1100 Subject: [PATCH 25/52] tools: add bounded J700 ATC setup readback Add an explicit read-only diagnostic for the exact words selected by the T8140 DisplayPort setup lists. Validate the machine, mapped resource layout, list alignment and every offset before opening the memory device. Compose overlapping writes and report only mismatching masks and offsets, without exposing calibration values or writing registers. Four hardware-independent tests cover write composition, separate resources and malformed or out-of-range lists. Hardware readback remains an observation rather than a display qualification result. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- MAINTAINERS | 1 + tools/diagnostics/apple-atc-readback.py | 111 ++++++++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 tools/diagnostics/apple-atc-readback.py diff --git a/MAINTAINERS b/MAINTAINERS index 3f1523e9fdb31..f299bbe5c35b9 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -2636,6 +2636,7 @@ F: drivers/nvme/host/apple.c F: drivers/nvmem/apple-efuses.c F: drivers/nvmem/apple-spmi-nvmem.c F: drivers/phy/apple/ +F: tools/diagnostics/apple-atc-readback.py F: drivers/pinctrl/pinctrl-apple-gpio.c F: drivers/pmdomain/apple/ F: drivers/power/reset/macsmc-reboot.c diff --git a/tools/diagnostics/apple-atc-readback.py b/tools/diagnostics/apple-atc-readback.py new file mode 100644 index 0000000000000..e0443446fea8f --- /dev/null +++ b/tools/diagnostics/apple-atc-readback.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +# Copyright 2026 Aurora Silicon +"""Read back only the J700 ATC words selected by its DP setup lists. + +No register is written. Output excludes calibration values. Run --selftest +without hardware access; --read-hardware requires permission to read /dev/mem. +""" +import argparse +import json +import os +from pathlib import Path +import struct +import unittest + +ROOT = Path('/sys/firmware/devicetree/base') +PHY = ROOT / 'soc/phy@40b000000' +GROUPS = ( + ('apple,tunable-dp-common-pre', 'core'), + ('apple,tunable-dp-lpdptx', 'lpdptx'), + ('apple,tunable-dp-common-post', 'core'), +) + + +def decode(data, size): + if len(data) > 65536 or len(data) % 12 or size < 4: + raise ValueError('invalid setup list size') + result = list(struct.iter_unpack('>III', data)) + for offset, mask, value in result: + if offset % 4 or offset > size - 4: + raise ValueError('setup word outside resource') + return result + + +def compose(groups): + expected = {} + for base, words in groups: + for offset, mask, value in words: + address = base + offset + known, prior = expected.get(address, (0, 0)) + expected[address] = (known | mask | value, (prior & ~mask) | value) + if len(expected) > 256: + raise ValueError('readback word limit exceeded') + return expected + + +def acquire(): + if b'apple,j700' not in (ROOT / 'compatible').read_bytes().split(b'\0'): + raise ValueError('requires J700') + if b'apple,t8140-atcphy' not in (PHY / 'compatible').read_bytes().split(b'\0'): + raise ValueError('requires T8140 ATC') + names = (PHY / 'reg-names').read_bytes().rstrip(b'\0').decode().split('\0') + raw = (PHY / 'reg').read_bytes() + if len(raw) != 16 * len(names): + raise ValueError('unexpected resource layout') + regs = dict(zip(names, struct.iter_unpack('>QQ', raw))) + if regs.get('core') != (0x40b000000, 0x4c000): + raise ValueError('unexpected core resource') + if regs.get('lpdptx') != (0x40b050000, 0x4000): + raise ValueError('unexpected LPDPTX resource') + groups = [] + for prop, region in GROUPS: + base, size = regs[region] + groups.append((base, decode((PHY / prop).read_bytes(), size))) + expected = compose(groups) + failures = [] + fd = os.open('/dev/mem', os.O_RDONLY | os.O_CLOEXEC) + try: + for address, (mask, value) in sorted(expected.items()): + data = os.pread(fd, 4, address) + if len(data) != 4: + raise OSError('short register read') + mismatch = (int.from_bytes(data, 'little') ^ value) & mask + if mismatch: + region = 'core' if address < regs['lpdptx'][0] else 'lpdptx' + failures.append({'region': region, + 'offset': hex(address - regs[region][0]), + 'mismatch_mask': hex(mismatch)}) + finally: + os.close(fd) + print(json.dumps({'checked_words': len(expected), 'mismatches': failures})) + + +class CodecTests(unittest.TestCase): + def test_overlap(self): + self.assertEqual(compose([(16, [(0, 15, 5), (0, 3, 2)])]), {16: (15, 6)}) + + def test_set_outside_mask(self): + self.assertEqual(compose([(0, [(0, 1, 8)])]), {0: (9, 8)}) + + def test_regions(self): + self.assertEqual(len(compose([(0, [(0, 1, 1)]), (16, [(0, 1, 0)])])), 2) + + def test_bounds(self): + for data, size in [(b'x', 4), (struct.pack('>III', 4, 1, 1), 4), + (struct.pack('>III', 1, 1, 1), 8)]: + with self.assertRaises(ValueError): + decode(data, size) + self.assertEqual(decode(struct.pack('>III', 4, 1, 1), 8), [(4, 1, 1)]) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description=__doc__) + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument('--selftest', action='store_true') + group.add_argument('--read-hardware', action='store_true') + args = parser.parse_args() + if args.selftest: + unittest.main(argv=['apple-atc-readback'], exit=True) + else: + acquire() From 373e947ee1766c743283e43ecc5987863df4bd6f Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 18:08:39 +1100 Subject: [PATCH 26/52] tools: map ATC readback words as read-only MMIO Use a page-aligned read-only mapping for each selected MMIO word. The memory-device read syscall does not provide the device mapping required by this target. Preserve exact resource and offset admission and never create a writable mapping. Test the same mapped-word reader against a temporary file, including its alignment refusal, alongside the existing setup-list composition tests. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- tools/diagnostics/apple-atc-readback.py | 26 +++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/tools/diagnostics/apple-atc-readback.py b/tools/diagnostics/apple-atc-readback.py index e0443446fea8f..8eb3c49c508cc 100644 --- a/tools/diagnostics/apple-atc-readback.py +++ b/tools/diagnostics/apple-atc-readback.py @@ -8,9 +8,11 @@ """ import argparse import json +import mmap import os from pathlib import Path import struct +import tempfile import unittest ROOT = Path('/sys/firmware/devicetree/base') @@ -44,6 +46,15 @@ def compose(groups): return expected +def read_word(fd, address): + if address % 4: + raise ValueError('unaligned word address') + page = address - address % mmap.PAGESIZE + with mmap.mmap(fd, mmap.PAGESIZE, flags=mmap.MAP_SHARED, + prot=mmap.PROT_READ, offset=page) as view: + return struct.unpack_from(' Date: Mon, 12 Oct 2026 18:10:36 +1100 Subject: [PATCH 27/52] tools: report the J700 AUX power control state Report the analog shutdown, supply override and clamp control bits beside the setup-list readback. Keep the read-only resource checks and avoid printing complete register values or calibration data. Add a decoder test covering asserted and cleared states. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- tools/diagnostics/apple-atc-readback.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/tools/diagnostics/apple-atc-readback.py b/tools/diagnostics/apple-atc-readback.py index 8eb3c49c508cc..3e67f49cc1c85 100644 --- a/tools/diagnostics/apple-atc-readback.py +++ b/tools/diagnostics/apple-atc-readback.py @@ -55,6 +55,18 @@ def read_word(fd, address): return struct.unpack_from(' Date: Mon, 12 Oct 2026 18:12:31 +1100 Subject: [PATCH 28/52] phy: apple: sequence J700 AUX power separately from calibration J700 reaches DisplayPort mode with its setup-list values present, but readback shows AUX still clamped and both supply overrides unset. The calibration lists do not perform the AUX power transition. Request the small and large supplies in order, release the clamp, then release analog shutdown. Check each masked state with a bounded poll. Track incomplete startup so a later shutdown can unwind it, and refuse to stop the domain if AUX cleanup fails. Leave ordinary USB operation and other PHY generations on their existing paths. Add host tests of the production transition helpers with register mocks, covering phase ordering, unrelated bits and failure at every stage. These pass with AddressSanitizer and UBSan; hardware qualification is pending. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- MAINTAINERS | 1 + drivers/phy/apple/atc.c | 94 +++++++++++++++++++-- tools/diagnostics/test-apple-atc-aux.py | 106 ++++++++++++++++++++++++ 3 files changed, 194 insertions(+), 7 deletions(-) create mode 100644 tools/diagnostics/test-apple-atc-aux.py diff --git a/MAINTAINERS b/MAINTAINERS index f299bbe5c35b9..42369620cc9a2 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -2637,6 +2637,7 @@ F: drivers/nvmem/apple-efuses.c F: drivers/nvmem/apple-spmi-nvmem.c F: drivers/phy/apple/ F: tools/diagnostics/apple-atc-readback.py +F: tools/diagnostics/test-apple-atc-aux.py F: drivers/pinctrl/pinctrl-apple-gpio.c F: drivers/pmdomain/apple/ F: drivers/power/reset/macsmc-reboot.c diff --git a/drivers/phy/apple/atc.c b/drivers/phy/apple/atc.c index e5e26712c5a4d..013d2f36d4d3b 100644 --- a/drivers/phy/apple/atc.c +++ b/drivers/phy/apple/atc.c @@ -653,6 +653,7 @@ struct atcphy_hw { * @hw: SoC-specific PHY description * @ss_tunables: The complete SuperSpeed tunable set was supplied * @dp_tunables: The complete T8140 DisplayPort bundle was supplied + * @aux_power_requested: AUX power-up began and needs matching shutdown * @fixed_usb2: The USB2 pairs go to a fixed hub on the USB controller, which * stays in host mode; the PHY provides USB2 from probe on * @typec_mode: Mode the Type-C mux last asked for; a fixed-hub port returns to @@ -711,6 +712,7 @@ struct apple_atcphy { const struct atcphy_hw *hw; bool ss_tunables; bool dp_tunables; + bool aux_power_requested; bool fixed_usb2; enum atcphy_mode typec_mode; bool host_active; @@ -1494,19 +1496,81 @@ static void atcphy_configure_lanes(struct apple_atcphy *atcphy, enum atcphy_mode } } -static void atcphy_enable_dp_aux(struct apple_atcphy *atcphy) +#define J700_AUX_ANALOG_CONTROL 0x16000 +#define J700_AUX_SUPPLY_CONTROL 0x16400 +#define J700_AUX_ANALOG_OFF BIT(0) +#define J700_AUX_CLAMP_SELECT BIT(8) +#define J700_AUX_CLAMP_ENABLE BIT(9) + +static int j700_aux_update(struct apple_atcphy *atcphy, u32 offset, u32 mask, u32 value) +{ + u32 observed; + + core_mask32(atcphy, offset, mask, value); + usleep_range(10, 20); + return readl_poll_timeout_atomic(atcphy->regs.core + offset, observed, + (observed & mask) == value, 1, 100); +} + +static int j700_aux_enable(struct apple_atcphy *atcphy) +{ + u32 clamp = J700_AUX_CLAMP_SELECT | J700_AUX_CLAMP_ENABLE; + int ret; + + /* Even an incomplete power-up must be unwound before the domain is stopped. */ + atcphy->aux_power_requested = true; + for (unsigned int stage = 0; stage < 2; stage++) { + u32 fields = 3U << (2 * stage); + + ret = j700_aux_update(atcphy, J700_AUX_SUPPLY_CONTROL, fields, fields); + if (ret) + return ret; + } + ret = j700_aux_update(atcphy, J700_AUX_SUPPLY_CONTROL, clamp, J700_AUX_CLAMP_SELECT); + if (ret) + return ret; + + return j700_aux_update(atcphy, J700_AUX_ANALOG_CONTROL, J700_AUX_ANALOG_OFF, 0); +} + +static int j700_aux_disable(struct apple_atcphy *atcphy) +{ + u32 clamp = J700_AUX_CLAMP_SELECT | J700_AUX_CLAMP_ENABLE; + int ret; + + if (!atcphy->aux_power_requested) + return 0; + ret = j700_aux_update(atcphy, J700_AUX_ANALOG_CONTROL, + J700_AUX_ANALOG_OFF, J700_AUX_ANALOG_OFF); + if (ret) + return ret; + ret = j700_aux_update(atcphy, J700_AUX_SUPPLY_CONTROL, clamp, clamp); + if (ret) + return ret; + for (unsigned int remaining = 2; remaining; remaining--) { + u32 override = BIT(2 * remaining - 1); + + ret = j700_aux_update(atcphy, J700_AUX_SUPPLY_CONTROL, override, 0); + if (ret) + return ret; + } + atcphy->aux_power_requested = false; + return 0; +} + +static int atcphy_enable_dp_aux(struct apple_atcphy *atcphy) { if (atcphy->hw->has_t8140_dp) { apple_tunable_apply(atcphy->regs.core, atcphy->tunables.dp_common_pre); apple_tunable_apply(atcphy->regs.lpdptx, atcphy->tunables.dp_lpdptx); apple_tunable_apply(atcphy->regs.core, atcphy->tunables.dp_common_post); dev_info_once(atcphy->dev, "Applied T8140 DisplayPort setup lists\n"); - return; + return j700_aux_enable(atcphy); } /* FIXME */ if (atcphy->hw->gen == ATCPHY_GENERATION_T8122) - return; + return 0; core_set32(atcphy, ACIOPHY_LANE_DP_CFG_BLK_TX_DP_CTRL0, DPTXPHY_PMA_LANE_RESET_N); core_set32(atcphy, ACIOPHY_LANE_DP_CFG_BLK_TX_DP_CTRL0, DPTXPHY_PMA_LANE_RESET_N_OV); @@ -1556,10 +1620,14 @@ static void atcphy_enable_dp_aux(struct apple_atcphy *atcphy) FIELD_PREP(LPDPTX_AUX_MARGIN_RCAL_TXSWING, 12)); atcphy->dp_link_rate = -1; + return 0; } -static void atcphy_disable_dp_aux(struct apple_atcphy *atcphy) +static int atcphy_disable_dp_aux(struct apple_atcphy *atcphy) { + if (atcphy->hw->has_t8140_dp) + return j700_aux_disable(atcphy); + /* FIXME */ if (atcphy->hw->gen != ATCPHY_GENERATION_T8122) { set32(atcphy->regs.lpdptx + LPDPTX_AUX_CONTROL, LPDPTX_AUX_PWN_DOWN); @@ -1574,6 +1642,7 @@ static void atcphy_disable_dp_aux(struct apple_atcphy *atcphy) core_clear32(atcphy, ACIOPHY_LANE_DP_CFG_BLK_TX_DP_CTRL0, DPRX_PCLK_ENABLE); core_clear32(atcphy, ACIOPHY_LANE_DP_CFG_BLK_TX_DP_CTRL0, DPTX_PCLK1_ENABLE); core_clear32(atcphy, ACIOPHY_LANE_DP_CFG_BLK_TX_DP_CTRL0, DPTX_PCLK2_ENABLE); + return 0; } static int atcphy_dp_configure_lane(struct apple_atcphy *atcphy, enum atcphy_lane lane, @@ -1940,7 +2009,9 @@ static int atcphy_power_off(struct apple_atcphy *atcphy) u32 reg; int ret; - atcphy_disable_dp_aux(atcphy); + ret = atcphy_disable_dp_aux(atcphy); + if (ret) + return ret; /* Enable all reset lines */ core_clear32(atcphy, ATCPHY_POWER_CTRL, ATCPHY_POWER_PHY_RESET_N); @@ -2548,6 +2619,12 @@ static int atcphy_configure(struct apple_atcphy *atcphy, enum atcphy_mode mode) if (atcphy->hw->has_t8140_dp && atcphy_modes[mode].enable_dp_aux && !atcphy->dp_tunables) return -EOPNOTSUPP; + if (atcphy->hw->has_t8140_dp && !atcphy_modes[mode].enable_dp_aux && + atcphy->aux_power_requested) { + ret = j700_aux_disable(atcphy); + if (ret) + return ret; + } /* the mode change below may power the PHY down: stop the tunnel clock first */ atc_tunnel_stop_t8103(atcphy); atc_tunnel_restore(atcphy); @@ -2612,8 +2689,11 @@ static int atcphy_configure(struct apple_atcphy *atcphy, enum atcphy_mode mode) } /* Setup AUX channel if DP altmode is requested */ - if (atcphy_modes[mode].enable_dp_aux) - atcphy_enable_dp_aux(atcphy); + if (atcphy_modes[mode].enable_dp_aux) { + ret = atcphy_enable_dp_aux(atcphy); + if (ret) + return ret; + } /* Enable clocks and configure lanes */ if (atcphy->hw->gen == ATCPHY_GENERATION_T8103) { diff --git a/tools/diagnostics/test-apple-atc-aux.py b/tools/diagnostics/test-apple-atc-aux.py new file mode 100644 index 0000000000000..8442694fd44ff --- /dev/null +++ b/tools/diagnostics/test-apple-atc-aux.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +"""Compile the production J700 AUX transitions against a bounded register mock.""" +import hashlib +import os +from pathlib import Path +import shlex +import subprocess +import tempfile + +source = Path(__file__).resolve().parents[2] / 'drivers/phy/apple/atc.c' +text = source.read_text() +parts = [line for line in text.splitlines() if line.startswith('#define J700_AUX_')] +for name in ('j700_aux_update', 'j700_aux_enable', 'j700_aux_disable'): + start = text.index('static int ' + name + '(') + begin = text.index('{', start) + depth = 1 + end = begin + 1 + while depth: + depth += (text[end] == '{') - (text[end] == '}') + end += 1 + parts.append(text[start:end]) +production = '\n'.join(parts) +print('Production transition SHA256:', hashlib.sha256(production.encode()).hexdigest()) +prefix = r''' +#include +#include +#include +#include +#include +typedef uint32_t u32; +#define BIT(n) (1U << (n)) +struct apple_atcphy { struct { unsigned char *core; } regs; bool aux_power_requested; }; +struct event { u32 offset, mask, value; } events[32]; +static u32 registers[0x18000 / 4]; +static unsigned int writes, reject; +static void core_mask32(struct apple_atcphy *phy, u32 offset, u32 mask, u32 value) +{ + assert(offset < sizeof(registers) && offset % 4 == 0 && writes < 32); + assert(!(value & ~mask)); + events[writes] = (struct event){offset, mask, value}; + writes++; + if (writes != reject) + registers[offset / 4] = (registers[offset / 4] & ~mask) | value; + (void)phy; +} +static void usleep_range(unsigned int low, unsigned int high) +{ + assert(low && high >= low); +} +#define readl_poll_timeout_atomic(address, value, condition, interval, timeout) \ + ({ (value) = *(u32 *)(address); (condition) ? 0 : -110; }) +''' +suffix = r''' +static void initial(struct apple_atcphy *phy) +{ + memset(registers, 0, sizeof(registers)); + memset(events, 0, sizeof(events)); + registers[J700_AUX_SUPPLY_CONTROL / 4] = 0x80000200; + registers[J700_AUX_ANALOG_CONTROL / 4] = 0x40000001; + *phy = (struct apple_atcphy){ .regs.core = (unsigned char *)registers }; + writes = reject = 0; +} +int main(void) +{ + struct apple_atcphy phy; + initial(&phy); + assert(j700_aux_disable(&phy) == 0 && writes == 0); + assert(j700_aux_enable(&phy) == 0 && phy.aux_power_requested && writes == 4); + assert(registers[J700_AUX_SUPPLY_CONTROL / 4] == 0x8000010f); + assert(registers[J700_AUX_ANALOG_CONTROL / 4] == 0x40000000); + assert(events[0].mask == 3 && events[1].mask == 12); + assert(events[2].mask == 0x300 && events[2].value == 0x100); + assert(events[3].offset == J700_AUX_ANALOG_CONTROL); + writes = 0; + assert(j700_aux_disable(&phy) == 0 && !phy.aux_power_requested && writes == 4); + assert(events[0].offset == J700_AUX_ANALOG_CONTROL); + assert(events[1].mask == 0x300 && events[2].mask == 8 && events[3].mask == 2); + assert(registers[J700_AUX_SUPPLY_CONTROL / 4] == 0x80000305); + assert(registers[J700_AUX_ANALOG_CONTROL / 4] == 0x40000001); + for (unsigned int fault = 1; fault <= 4; fault++) { + initial(&phy); + reject = fault; + assert(j700_aux_enable(&phy) == -110 && phy.aux_power_requested); + reject = 0; + writes = 0; + assert(j700_aux_disable(&phy) == 0 && !phy.aux_power_requested); + initial(&phy); + assert(j700_aux_enable(&phy) == 0); + writes = 0; + reject = fault; + assert(j700_aux_disable(&phy) == -110 && phy.aux_power_requested); + } + puts("PASS: sequencing, unrelated bits, every failed phase, retained cleanup ownership"); + return 0; +} +''' +with tempfile.TemporaryDirectory(prefix='apple-atc-aux-') as directory: + path = Path(directory) + (path / 'test.c').write_text(prefix + production + suffix) + command = shlex.split(os.environ.get('CC', 'cc')) + command += ['-std=gnu11', '-Wall', '-Wextra', '-Werror', '-O1'] + if os.environ.get('ATC_SANITIZERS') == '1': + command += ['-fsanitize=address,undefined', '-fno-omit-frame-pointer'] + subprocess.run(command + [str(path / 'test.c'), '-o', str(path / 'test')], check=True) + subprocess.run([str(path / 'test')], check=True) From 39af890eb392cea1ff58cc0fc4e47ebff6e8f3db Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 18:18:18 +1100 Subject: [PATCH 29/52] tools: report J700 lane protocol and power controls Extend the read-only state report with both lane protocol selections, power-control nibbles and receive override bits. Keep the fixed resource admission and add a synthetic two-lane decoding test. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- tools/diagnostics/apple-atc-readback.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/tools/diagnostics/apple-atc-readback.py b/tools/diagnostics/apple-atc-readback.py index 3e67f49cc1c85..6929e38064e25 100644 --- a/tools/diagnostics/apple-atc-readback.py +++ b/tools/diagnostics/apple-atc-readback.py @@ -67,6 +67,16 @@ def aux_state(analog, supply): } +def lane_state(protocol, power, receiver): + return [{'lane': lane, + 'rx_protocol': (protocol >> (6 * lane)) & 7, + 'tx_protocol': (protocol >> (6 * lane + 3)) & 7, + 'power_control': (power >> (4 * lane)) & 15, + 'receiver_small_override': bool(receiver & (1 << (2 + lane))), + 'receiver_large_override': bool(receiver & (1 << (6 + lane)))} + for lane in range(2)] + + def acquire(): if b'apple,j700' not in (ROOT / 'compatible').read_bytes().split(b'\0'): raise ValueError('requires J700') @@ -88,6 +98,7 @@ def acquire(): expected = compose(groups) failures = [] power = None + lanes = None fd = os.open('/dev/mem', os.O_RDONLY | os.O_CLOEXEC) try: for address, (mask, value) in sorted(expected.items()): @@ -99,12 +110,23 @@ def acquire(): 'mismatch_mask': hex(mismatch)}) power = aux_state(read_word(fd, regs['core'][0] + 0x16000), read_word(fd, regs['core'][0] + 0x16400)) + lanes = lane_state(read_word(fd, regs['core'][0] + 0x60), + read_word(fd, regs['core'][0] + 0x200), + read_word(fd, regs['core'][0] + 0xc)) finally: os.close(fd) - print(json.dumps({'checked_words': len(expected), 'mismatches': failures, 'aux_state': power})) + print(json.dumps({'checked_words': len(expected), 'mismatches': failures, 'aux_state': power, 'lanes': lanes})) class CodecTests(unittest.TestCase): + def test_lane_state(self): + lanes = lane_state(0x489, 0x10, 0x88) + self.assertEqual((lanes[0]['rx_protocol'], lanes[0]['tx_protocol']), (1, 1)) + self.assertEqual((lanes[1]['rx_protocol'], lanes[1]['tx_protocol']), (2, 2)) + self.assertEqual(lanes[1]['power_control'], 1) + self.assertTrue(lanes[1]['receiver_small_override']) + self.assertTrue(lanes[1]['receiver_large_override']) + def test_aux_state(self): self.assertTrue(all(aux_state(1, 0x30f).values())) self.assertFalse(any(aux_state(0, 0).values())) From f19979190af80c37fdaea4b30ef13dca3852f037 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 18:19:18 +1100 Subject: [PATCH 30/52] phy: apple: park the J700 DP lane before link training The selected DP lane still reports the active power-control nibble after its protocol changes, with both receive overrides clear. The J700 path requires this lane to reach its quiescent state before link training. Assert only the selected lane's receive overrides, then sequence its power-control field into idle with bounded readback checks. Preserve the USB lane and unrelated bits. Return failures to the mux caller instead of publishing a completed mode transition. Host tests of the production helpers cover both lanes, invalid indices, unrelated-bit preservation and a failed write at every phase, under AddressSanitizer and UBSan. Hardware qualification remains pending. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/phy/apple/atc.c | 88 +++++++++++++++++++------ tools/diagnostics/test-apple-atc-aux.py | 23 ++++++- 2 files changed, 90 insertions(+), 21 deletions(-) diff --git a/drivers/phy/apple/atc.c b/drivers/phy/apple/atc.c index 013d2f36d4d3b..600a3f09dcecf 100644 --- a/drivers/phy/apple/atc.c +++ b/drivers/phy/apple/atc.c @@ -1440,7 +1440,50 @@ static void atcphy_setup_pipehandler(struct apple_atcphy *atcphy) atcphy_enable_dummy_phy(atcphy); } -static void atcphy_configure_lanes(struct apple_atcphy *atcphy, enum atcphy_mode mode) +#define J700_PMA_RECEIVER_CONTROL 0x000c +#define J700_PMA_POWER_CONTROL 0x0200 + +static int j700_field_update(struct apple_atcphy *atcphy, u32 offset, u32 mask, u32 value) +{ + u32 observed; + + core_mask32(atcphy, offset, mask, value); + usleep_range(10, 20); + return readl_poll_timeout_atomic(atcphy->regs.core + offset, observed, + (observed & mask) == value, 1, 100); +} + +static int j700_pma_idle(struct apple_atcphy *atcphy, unsigned int lane) +{ + u32 field; + int ret; + + if (lane >= 2) + return -EINVAL; + for (unsigned int supply = 0; supply < 2; supply++) { + field = BIT(2 + lane + 4 * supply); + ret = j700_field_update(atcphy, J700_PMA_RECEIVER_CONTROL, field, field); + if (ret) + return ret; + } + field = BIT(4 * lane + 3); + ret = j700_field_update(atcphy, J700_PMA_POWER_CONTROL, field, 0); + if (ret) + return ret; + field = BIT(4 * lane); + ret = j700_field_update(atcphy, J700_PMA_POWER_CONTROL, field, field); + if (ret) + return ret; + for (unsigned int control = 1; control <= 2; control++) { + field = BIT(4 * lane + control); + ret = j700_field_update(atcphy, J700_PMA_POWER_CONTROL, field, 0); + if (ret) + return ret; + } + return 0; +} + +static int atcphy_configure_lanes(struct apple_atcphy *atcphy, enum atcphy_mode mode) { const struct atcphy_mode_configuration *mode_cfg = atcphy_get_mode_config(atcphy, mode); @@ -1467,9 +1510,22 @@ static void atcphy_configure_lanes(struct apple_atcphy *atcphy, enum atcphy_mode else core_clear32(atcphy, atcphy->hw->aciophy_crossbar, ACIOPHY_CROSSBAR_DP_BOTH_PMA); + if (atcphy->hw->has_t8140_dp) { + for (unsigned int lane = 0; lane < 2; lane++) { + int ret; + + if (!mode_cfg->dp_lane[lane]) + continue; + ret = j700_pma_idle(atcphy, lane); + if (ret) + return ret; + } + return 0; + } + /* unclear if we need the remainder for t8122 */ if (atcphy->hw->gen == ATCPHY_GENERATION_T8122) - return; + return 0; if (mode_cfg->dp_lane[0]) { core_set32(atcphy, LN0_AUSPMA_RX_TOP + LN_AUSPMA_RX_TOP_PMAFSM, @@ -1494,6 +1550,7 @@ static void atcphy_configure_lanes(struct apple_atcphy *atcphy, enum atcphy_mode LN_AUSPMA_RX_TOP_PMAFSM_PCS_OV); udelay(10); } + return 0; } #define J700_AUX_ANALOG_CONTROL 0x16000 @@ -1502,15 +1559,6 @@ static void atcphy_configure_lanes(struct apple_atcphy *atcphy, enum atcphy_mode #define J700_AUX_CLAMP_SELECT BIT(8) #define J700_AUX_CLAMP_ENABLE BIT(9) -static int j700_aux_update(struct apple_atcphy *atcphy, u32 offset, u32 mask, u32 value) -{ - u32 observed; - - core_mask32(atcphy, offset, mask, value); - usleep_range(10, 20); - return readl_poll_timeout_atomic(atcphy->regs.core + offset, observed, - (observed & mask) == value, 1, 100); -} static int j700_aux_enable(struct apple_atcphy *atcphy) { @@ -1522,15 +1570,15 @@ static int j700_aux_enable(struct apple_atcphy *atcphy) for (unsigned int stage = 0; stage < 2; stage++) { u32 fields = 3U << (2 * stage); - ret = j700_aux_update(atcphy, J700_AUX_SUPPLY_CONTROL, fields, fields); + ret = j700_field_update(atcphy, J700_AUX_SUPPLY_CONTROL, fields, fields); if (ret) return ret; } - ret = j700_aux_update(atcphy, J700_AUX_SUPPLY_CONTROL, clamp, J700_AUX_CLAMP_SELECT); + ret = j700_field_update(atcphy, J700_AUX_SUPPLY_CONTROL, clamp, J700_AUX_CLAMP_SELECT); if (ret) return ret; - return j700_aux_update(atcphy, J700_AUX_ANALOG_CONTROL, J700_AUX_ANALOG_OFF, 0); + return j700_field_update(atcphy, J700_AUX_ANALOG_CONTROL, J700_AUX_ANALOG_OFF, 0); } static int j700_aux_disable(struct apple_atcphy *atcphy) @@ -1540,17 +1588,17 @@ static int j700_aux_disable(struct apple_atcphy *atcphy) if (!atcphy->aux_power_requested) return 0; - ret = j700_aux_update(atcphy, J700_AUX_ANALOG_CONTROL, - J700_AUX_ANALOG_OFF, J700_AUX_ANALOG_OFF); + ret = j700_field_update(atcphy, J700_AUX_ANALOG_CONTROL, + J700_AUX_ANALOG_OFF, J700_AUX_ANALOG_OFF); if (ret) return ret; - ret = j700_aux_update(atcphy, J700_AUX_SUPPLY_CONTROL, clamp, clamp); + ret = j700_field_update(atcphy, J700_AUX_SUPPLY_CONTROL, clamp, clamp); if (ret) return ret; for (unsigned int remaining = 2; remaining; remaining--) { u32 override = BIT(2 * remaining - 1); - ret = j700_aux_update(atcphy, J700_AUX_SUPPLY_CONTROL, override, 0); + ret = j700_field_update(atcphy, J700_AUX_SUPPLY_CONTROL, override, 0); if (ret) return ret; } @@ -2700,7 +2748,9 @@ static int atcphy_configure(struct apple_atcphy *atcphy, enum atcphy_mode mode) core_set32(atcphy, CIO3PLL_CLK_CTRL, CIO3PLL_CLK_PCLK_EN); core_set32(atcphy, CIO3PLL_CLK_CTRL, CIO3PLL_CLK_REFCLK_EN); } - atcphy_configure_lanes(atcphy, mode); + ret = atcphy_configure_lanes(atcphy, mode); + if (ret) + return ret; /* Take the USB3 PHY out of reset */ core_set32(atcphy, ATCPHY_POWER_CTRL, ATCPHY_POWER_PHY_RESET_N); diff --git a/tools/diagnostics/test-apple-atc-aux.py b/tools/diagnostics/test-apple-atc-aux.py index 8442694fd44ff..00f29b0a410b3 100644 --- a/tools/diagnostics/test-apple-atc-aux.py +++ b/tools/diagnostics/test-apple-atc-aux.py @@ -10,8 +10,8 @@ source = Path(__file__).resolve().parents[2] / 'drivers/phy/apple/atc.c' text = source.read_text() -parts = [line for line in text.splitlines() if line.startswith('#define J700_AUX_')] -for name in ('j700_aux_update', 'j700_aux_enable', 'j700_aux_disable'): +parts = [line for line in text.splitlines() if line.startswith(('#define J700_AUX_', '#define J700_PMA_'))] +for name in ('j700_field_update', 'j700_pma_idle', 'j700_aux_enable', 'j700_aux_disable'): start = text.index('static int ' + name + '(') begin = text.index('{', start) depth = 1 @@ -24,6 +24,7 @@ print('Production transition SHA256:', hashlib.sha256(production.encode()).hexdigest()) prefix = r''' #include +#include #include #include #include @@ -91,6 +92,24 @@ reject = fault; assert(j700_aux_disable(&phy) == -110 && phy.aux_power_requested); } + initial(&phy); + assert(j700_pma_idle(&phy, 2) == -EINVAL && writes == 0); + for (unsigned int lane = 0; lane < 2; lane++) { + initial(&phy); + registers[J700_PMA_POWER_CONTROL / 4] = 0x800000ee; + registers[J700_PMA_RECEIVER_CONTROL / 4] = 0x5000; + assert(j700_pma_idle(&phy, lane) == 0 && writes == 6); + assert(((registers[J700_PMA_POWER_CONTROL / 4] >> (4 * lane)) & 15) == 1); + assert(((registers[J700_PMA_POWER_CONTROL / 4] >> (4 * (1 - lane))) & 15) == 14); + assert(registers[J700_PMA_POWER_CONTROL / 4] & 0x80000000); + assert(registers[J700_PMA_RECEIVER_CONTROL / 4] == (0x5000U | (0x44U << lane))); + for (unsigned int fault = 1; fault <= 6; fault++) { + initial(&phy); + registers[J700_PMA_POWER_CONTROL / 4] = 0xee; + reject = fault; + assert(j700_pma_idle(&phy, lane) == -110); + } + } puts("PASS: sequencing, unrelated bits, every failed phase, retained cleanup ownership"); return 0; } From bc3b8aaa560f23e069a7ea2d774645ea5fec39cc Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 18:24:32 +1100 Subject: [PATCH 31/52] arm64: dts: apple: select the second external DCP source on J700 Route the test connector to dispext1,0, matching its external DCP index. The selector denotes the crossbar source and is independent of physical Type-C port zero. Hardware routing facts were audited with user authorization; this DT change is independently written. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/boot/dts/apple/t8140-j700-external-test.dts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arch/arm64/boot/dts/apple/t8140-j700-external-test.dts b/arch/arm64/boot/dts/apple/t8140-j700-external-test.dts index d3965ab2c5061..fbd8e83738f0c 100644 --- a/arch/arm64/boot/dts/apple/t8140-j700-external-test.dts +++ b/arch/arm64/boot/dts/apple/t8140-j700-external-test.dts @@ -14,7 +14,7 @@ phy-names = "typec0"; mux-controls = <&dpxbar0 0>; mux-control-names = "typec0"; - apple,typec-mux-indices = <0>; + apple,typec-mux-indices = <2>; apple,typec-dptx-phys = <0>; apple,dcp-index = <1>; }; From ea3d81e81f781c0f9f73748f5e7d405e232e0f39 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 18:34:02 +1100 Subject: [PATCH 32/52] dt-bindings: phy: apple: describe T8140 DisplayPort drive presets Document the firmware-provided fallback and per-rate drive tables. Each has sixteen entries indexed by voltage swing and pre-emphasis; other PHYs do not accept these properties. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- .../devicetree/bindings/phy/apple,atcphy.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Documentation/devicetree/bindings/phy/apple,atcphy.yaml b/Documentation/devicetree/bindings/phy/apple,atcphy.yaml index fef0605343e8f..3e36244ef4d5b 100644 --- a/Documentation/devicetree/bindings/phy/apple,atcphy.yaml +++ b/Documentation/devicetree/bindings/phy/apple,atcphy.yaml @@ -53,6 +53,8 @@ allOf: apple,tunable-dp-common-pre: false apple,tunable-dp-lpdptx: false apple,tunable-dp-common-post: false + patternProperties: + "^apple,dp-training-table(-rbr|-hbr|-hbr2|-hbr3)?$": false $defs: apple,tunable: @@ -210,6 +212,16 @@ properties: T8140, copied from Apple's Device Tree by the bootloader. Unlike the other tunables these are applied to the usb2phy-reg region. +patternProperties: + "^apple,dp-training-table(-rbr|-hbr|-hbr2|-hbr3)?$": + $ref: /schemas/types.yaml#/definitions/uint32-array + minItems: 16 + maxItems: 16 + description: > + Firmware-provided T8140 drive presets indexed by voltage swing times + four plus pre-emphasis. The unsuffixed table is the required fallback; + a link-rate suffix selects an optional rate-specific override. + required: - compatible - reg From 85d81b76238d5a3a4782d0ec443b7274cb1f72e0 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 18:34:02 +1100 Subject: [PATCH 33/52] phy: apple: implement bounded J700 DisplayPort main-link training After correcting the crossbar route, J700 reaches SET_LINK_RATE but the legacy PHY path waits for the wrong command-ready register. Add an isolated T8140 main-link sequence, firmware drive presets, and cleanup ownership that survives failed setup or shutdown. Preserve the other PMA used for USB. Handle the version-two group-one drive command with complete bounds and range checks before changing the PHY. Cache training values only after a successful PHY operation and retain the legacy endpoint behavior. Hardware register facts were audited with explicit user authorization. The implementation and tests are independently written. The copy audit matched only three hardware-field expressions and standard C test headers. No private implementation patch was applied. Validation: 29 PHY state scenarios, 112 callback boundary cases, 100 group routing cases, 905 AFK assertions and production owned-buffer/AUX tests under ASan/UBSan; schema validation and strict checkpatch. Hardware qualification remains pending. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- MAINTAINERS | 4 + drivers/gpu/drm/apple/afk.c | 17 +- drivers/gpu/drm/apple/afk.h | 2 + drivers/gpu/drm/apple/dptx-j700-training.h | 54 +++ drivers/gpu/drm/apple/dptxep.c | 30 +- drivers/gpu/drm/apple/dptxep.h | 1 + drivers/phy/apple/atc-j700-dp.h | 333 +++++++++++++++++++ drivers/phy/apple/atc.c | 45 ++- tools/diagnostics/apple-dptx-training-test.c | 131 ++++++++ tools/diagnostics/apple-j700-dp-test.c | 226 +++++++++++++ tools/diagnostics/test-apple-j700-dp.py | 26 ++ 11 files changed, 861 insertions(+), 8 deletions(-) create mode 100644 drivers/gpu/drm/apple/dptx-j700-training.h create mode 100644 drivers/phy/apple/atc-j700-dp.h create mode 100644 tools/diagnostics/apple-dptx-training-test.c create mode 100644 tools/diagnostics/apple-j700-dp-test.c create mode 100644 tools/diagnostics/test-apple-j700-dp.py diff --git a/MAINTAINERS b/MAINTAINERS index 42369620cc9a2..4db1e45c648ce 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -2006,6 +2006,7 @@ F: arch/arm64/include/asm/apple-idle.h F: arch/arm64/kernel/idle-apple.S F: drivers/hid/dockchannel-hid/dockchannel-hid-protocol* F: drivers/iio/common/aop_sensors/ +F: drivers/phy/apple/atc-j700-dp.h F: drivers/rtc/rtc-apple-pmu.c F: drivers/soc/apple/aop.rs F: drivers/soc/apple/mcc.c @@ -2015,6 +2016,9 @@ F: include/linux/soc/apple/pci-apple-piodma.h F: rust/kernel/iio/common/aop_sensors.rs F: rust/kernel/soc/apple/aop.rs F: sound/soc/apple/t8140_aop_audio.rs +F: tools/diagnostics/apple-dptx-training-test.c +F: tools/diagnostics/apple-j700-dp-test.c +F: tools/diagnostics/test-apple-j700-dp.py F: tools/testing/apple/ APPLETALK NETWORK LAYER diff --git a/drivers/gpu/drm/apple/afk.c b/drivers/gpu/drm/apple/afk.c index e7c76e2d8a610..b7c3905303eab 100644 --- a/drivers/gpu/drm/apple/afk.c +++ b/drivers/gpu/drm/apple/afk.c @@ -708,7 +708,8 @@ static void afk_v2_callback(struct apple_dcp_afkep *ep, (command->oob && request_size > command->rx_size)) goto reject; service = afk_service_get(afk_epic_find_service(ep, frame->interface)); - if (!service || READ_ONCE(service->torndown) || !service->ops->call) { + if (!service || READ_ONCE(service->torndown) || + (!service->ops->call && !service->ops->call_v2)) { if (service) afk_service_put(service); goto reject; @@ -732,7 +733,7 @@ static void afk_v2_callback(struct apple_dcp_afkep *ep, } call = (const void *)request; body_size = request_size - sizeof(*call); - if (le16_to_cpu(call->group) || + if ((le16_to_cpu(call->group) && !service->ops->call_v2) || le32_to_cpu(call->magic) != EPIC_SERVICE_CALL_MAGIC || le32_to_cpu(call->data_len) != body_size) goto release; @@ -743,9 +744,15 @@ static void afk_v2_callback(struct apple_dcp_afkep *ep, } reply[1] = command->id; memcpy(reply + AFKV2_COMMAND_SIZE, call, sizeof(*call)); - ret = service->ops->call(service, le32_to_cpu(call->command), - request + sizeof(*call), body_size, - reply + AFKV2_COMMAND_SIZE + sizeof(*call), body_size); + if (service->ops->call_v2) + ret = service->ops->call_v2(service, le16_to_cpu(call->group), + le32_to_cpu(call->command), + request + sizeof(*call), body_size, + reply + AFKV2_COMMAND_SIZE + sizeof(*call), body_size); + else + ret = service->ops->call(service, le32_to_cpu(call->command), + request + sizeof(*call), body_size, + reply + AFKV2_COMMAND_SIZE + sizeof(*call), body_size); if (ret) goto release; reply_size = AFKV2_COMMAND_SIZE + request_size; diff --git a/drivers/gpu/drm/apple/afk.h b/drivers/gpu/drm/apple/afk.h index a4de9090681c2..3d1f975d798a8 100644 --- a/drivers/gpu/drm/apple/afk.h +++ b/drivers/gpu/drm/apple/afk.h @@ -76,6 +76,8 @@ struct apple_epic_service_ops { int (*call)(struct apple_epic_service *service, u32 idx, const void *data, size_t data_size, void *reply, size_t reply_size); + int (*call_v2)(struct apple_epic_service *service, u16 group, u32 command, + const void *data, size_t size, void *reply, size_t room); int (*report)(struct apple_epic_service *service, enum epic_subtype type, const void *data, size_t data_size); void (*teardown)(struct apple_epic_service *service); diff --git a/drivers/gpu/drm/apple/dptx-j700-training.h b/drivers/gpu/drm/apple/dptx-j700-training.h new file mode 100644 index 0000000000000..09a5d1018a7fb --- /dev/null +++ b/drivers/gpu/drm/apple/dptx-j700-training.h @@ -0,0 +1,54 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ +/* Copyright 2026 Aurora Silicon */ +#ifndef APPLE_DPTX_J700_TRAINING_H +#define APPLE_DPTX_J700_TRAINING_H + +static int dptx_j700_training(struct apple_epic_service *service, bool write, + const void *data, size_t size, void *reply, size_t room) +{ + struct dptx_port *port = service->cookie; + union phy_configure_opts options = {}; + u32 count; + int ret; + + if (!port || !port->atcphy) + return -ENODEV; + if (size < 32 || room < 32) + return -EINVAL; + count = get_unaligned_le32(data + (write ? 16 : 0)); + if (count > 4 || (write && (!count || count == 3)) || room < 32 + 12 * count || + (write && size < 32 + 12 * count)) + return -EINVAL; + if (write) { + ret = phy_validate(port->atcphy, PHY_MODE_DP, 0, &options); + if (ret) + return ret; + if (count > options.dp.lanes) + return -EINVAL; + for (unsigned int i = 0; i < count; i++) { + const u8 *entry = data + 32 + 12 * i; + + if (get_unaligned_le32(entry) || get_unaligned_le32(entry + 4) > 3 || + get_unaligned_le32(entry + 8) > 3) + return -EINVAL; + options.dp.voltage[i] = get_unaligned_le32(entry + 4); + options.dp.pre[i] = get_unaligned_le32(entry + 8); + } + options.dp.lanes = count; + options.dp.set_voltages = true; + ret = phy_configure(port->atcphy, &options); + if (ret) + return ret; + memcpy(port->trained_voltage, options.dp.voltage, sizeof(port->trained_voltage)); + memcpy(port->trained_pre, options.dp.pre, sizeof(port->trained_pre)); + } + memset(reply, 0, room); + put_unaligned_le32(count, reply + (write ? 16 : 0)); + for (unsigned int i = 0; i < count; i++) { + put_unaligned_le32(port->trained_voltage[i], reply + 36 + 12 * i); + put_unaligned_le32(port->trained_pre[i], reply + 40 + 12 * i); + } + return 0; +} + +#endif diff --git a/drivers/gpu/drm/apple/dptxep.c b/drivers/gpu/drm/apple/dptxep.c index 856a9a75a389d..d2ab05ef9e57a 100644 --- a/drivers/gpu/drm/apple/dptxep.c +++ b/drivers/gpu/drm/apple/dptxep.c @@ -8,6 +8,7 @@ #include #include #include +#include #include @@ -17,6 +18,8 @@ #include "parser.h" #include "trace.h" +#include "dptx-j700-training.h" + struct dcpdptx_connection_cmd { __le32 unk; __le32 target; @@ -264,6 +267,13 @@ dptxport_call_get_max_drive_settings(struct apple_epic_service *service, return -EINVAL; reply->retcode = cpu_to_le32(0); + if (service->ep->wire_v2) { + memset(reply, 0, reply_size); + put_unaligned_le32(3, reply_ + 20); + put_unaligned_le32(3, reply_ + 24); + return 0; + } + reply->max_drive_settings[0] = cpu_to_le32(0x3); reply->max_drive_settings[1] = cpu_to_le32(0x3); @@ -405,6 +415,9 @@ static int dptxport_call_set_active_lane_count(struct apple_epic_service *servic u64 lane_count = le64_to_cpu(request->lane_count); + if (service->ep->wire_v2 && lane_count > dptx->lane_count) + return -EINVAL; + if (dptx->lane_count < lane_count) dev_err(dcp->dev, "set_active_lane_count: unexpected lane " "count:%llu phy: %d\n", lane_count, dptx->lane_count); @@ -415,7 +428,7 @@ static int dptxport_call_set_active_lane_count(struct apple_epic_service *servic dptx->phy_ops.dp.lanes = lane_count; /* USB4 DP IN has no ATC PHY; still accept the lane count. */ dptx->phy_ops.dp.set_lanes = - dcp_is_usb4_output(dcp) || (dcp->dptx_phy > 3); + service->ep->wire_v2 || dcp_is_usb4_output(dcp) || (dcp->dptx_phy > 3); break; default: dev_err(dcp->dev, "set_active_lane_count: invalid lane count:%llu\n", lane_count); @@ -853,11 +866,26 @@ static void dptxport_init(struct apple_epic_service *service, const char *name, } } +static int dptxport_group_call(struct apple_epic_service *service, u16 group, u32 command, + const void *data, size_t size, void *reply, size_t room) +{ + if (group == 1 && command == DPTX_APCALL_SET_DRIVE_SETTINGS) + return dptx_j700_training(service, true, data, size, reply, room); + if (group) + return -EOPNOTSUPP; + if (command == DPTX_APCALL_SET_DRIVE_SETTINGS) + return -EOPNOTSUPP; + if (command == DPTX_APCALL_GET_DRIVE_SETTINGS) + return dptx_j700_training(service, false, data, size, reply, room); + return dptxport_call(service, command, data, size, reply, room); +} + static const struct apple_epic_service_ops dptxep_ops[] = { { .name = "AppleDCPDPTXRemotePort", .init = dptxport_init, .call = dptxport_call, + .call_v2 = dptxport_group_call, }, {} }; diff --git a/drivers/gpu/drm/apple/dptxep.h b/drivers/gpu/drm/apple/dptxep.h index 885e3675f9db5..3eb71cf00f9ba 100644 --- a/drivers/gpu/drm/apple/dptxep.h +++ b/drivers/gpu/drm/apple/dptxep.h @@ -59,6 +59,7 @@ struct dptx_port { u32 lane_count; u32 link_rate, pending_link_rate; u32 drive_settings[2]; + u32 trained_voltage[4], trained_pre[4]; /* * Per-boot invocation counters for the validate/connect/request/ * release AFK calls, used together with the caller return address diff --git a/drivers/phy/apple/atc-j700-dp.h b/drivers/phy/apple/atc-j700-dp.h new file mode 100644 index 0000000000000..185551e5a589e --- /dev/null +++ b/drivers/phy/apple/atc-j700-dp.h @@ -0,0 +1,333 @@ +/* SPDX-License-Identifier: GPL-2.0 OR BSD-2-Clause */ +/* Copyright 2026 Aurora Silicon */ +#ifndef APPLE_ATC_J700_DP_H +#define APPLE_ATC_J700_DP_H + +/* Register facts from the authorized J700 hardware audit. */ +struct j700_dp_step { + u32 offset, mask, value; + u16 delay_ns; +}; + +static void j700_dp_steps(struct apple_atcphy *p, u32 base, + const struct j700_dp_step *steps, size_t count) +{ + for (size_t i = 0; i < count; i++) { + core_mask32(p, base + steps[i].offset, steps[i].mask, steps[i].value); + if (steps[i].delay_ns) + ndelay(steps[i].delay_ns); + } +} + +static int j700_dp_wait(struct apple_atcphy *p, u32 offset, u32 mask, bool set) +{ + u32 value; + + return readl_poll_timeout_atomic(p->regs.core + offset, value, + !!(value & mask) == set, 1, 100); +} + +static void j700_dp_power(struct apple_atcphy *p, unsigned int lane) +{ + unsigned int shift = 4 * lane; + const u32 bits[] = { BIT(2), BIT(1), BIT(0), BIT(3) }; + + for (unsigned int i = 0; i < ARRAY_SIZE(bits); i++) { + u32 mask = bits[i] << shift; + + core_mask32(p, 0x200, mask, i == 2 ? 0 : mask); + udelay(1); + } + for (unsigned int bank = 0; bank < 2; bank++) { + core_clear32(p, 0xc, BIT(6 - 4 * bank + lane)); + udelay(1); + } +} + +static void j700_dp_output_power(struct apple_atcphy *p, unsigned int lane, bool tx) +{ + u32 reg = tx ? 0x10c : 0xc; + + core_set32(p, reg, (BIT(4) | BIT(6)) << lane); + udelay(1); + core_set32(p, reg, (BIT(0) | BIT(2)) << lane); + udelay(1); + if (!tx) { + core_set32(p, 0x8, (BIT(10) | BIT(12)) << lane); + udelay(1); + core_set32(p, 0x8, (BIT(6) | BIT(8)) << lane); + udelay(1); + } + reg = tx ? 0x10c : 0x8; + core_clear32(p, reg, BIT(tx ? 8 : 14) << lane); + core_set32(p, reg, BIT(tx ? 10 : 16) << lane); + udelay(1); +} + +static void j700_dp_equalize(struct apple_atcphy *p, unsigned int lane, + bool tx, u32 preset) +{ + u32 base = (tx ? 0xc000 : 0x9000) + lane * 0x7000; + u32 swing = ((preset & 3) << 14) | ((preset >> 2) & 15); + u32 taps = ((preset >> 6) & 0x3ffff) * 2 + 1; + + if (tx) + core_set32(p, base + 0xd0, BIT(20)); + core_mask32(p, base + (tx ? 0x50 : 0x210), 0xc00f, swing); + core_set32(p, base + (tx ? 0xd4 : 0x248), BIT(28)); + core_mask32(p, base + (tx ? 0xd0 : 0x244), 0x7ffff, taps); +} + +static void j700_dp_transmitter(struct apple_atcphy *p, unsigned int lane, int rate) +{ + static const struct j700_dp_step start[] = { + { 0x44, ~0U, 0x0aafb800, 500 }, + { 0x44, 0x1000000, 0x1000000, 250 }, + { 0x44, 0x4000000, 0x4000000, 250 }, + { 0x44, 0x7c0, 0x300, 250 }, + { 0x44, 0x6c000, 0, 250 }, + }; + static const struct j700_dp_step divide[] = { + { 0x44, 0x100000, 0x100000, 250 }, + { 0x44, 0x400000, 0x400000, 0 }, + }; + static const struct j700_dp_step finish[] = { + { 0x04, 0x33000000, 0x32000000, 0 }, + { 0x10, 0x600, 0x600, 0 }, + { 0x1c, 0xc00000, 0x800000, 0 }, + }; + u32 base = 0xd000 + lane * 0x7000; + + j700_dp_output_power(p, lane, true); + j700_dp_steps(p, base, start, ARRAY_SIZE(start)); + j700_dp_equalize(p, lane, true, 0); + if (rate != ATCPHY_DP_LINK_RATE_HBR3) + j700_dp_steps(p, base, divide, ARRAY_SIZE(divide)); + j700_dp_steps(p, base, finish, ARRAY_SIZE(finish)); + core_mask32(p, base + 0xc, 0x7000000, + rate == ATCPHY_DP_LINK_RATE_RBR ? 0x5000000 : 0x4000000); +} + +static void j700_dp_receiver(struct apple_atcphy *p, unsigned int lane, int rate) +{ + static const struct j700_dp_step setup[] = { + { 0xa4, 3, 2, 0 }, { 0xa8, 0x18000, 0x10000, 0 }, + { 0x6c, 0x18000, 0x10000, 0 }, + { 0xd4, 0x1f1f80, 0x121180, 0 }, { 0x64, 0xfc00, 0xb800, 0 }, + { 0xd8, 7, 4, 0 }, { 0xdc, 0x18, 0, 0 }, + { 0xd4, 0xe000, 0xc000, 100 }, + }; + static const struct j700_dp_step settle[] = { + { 0x6c, 0x600, 0x400, 100 }, { 0xd4, 0xd0000, 0x50000, 0 }, + { 0xd8, 3, 1, 500 }, { 0xd4, 0xc0000, 0, 200 }, + { 0x68, 0x180, 0x180, 200 }, { 0x68, 0x60, 0x60, 200 }, + { 0xd8, 3, 3, 1000 }, { 0xd8, 3, 2, 1000 }, + { 0xd8, 3, 0, 750 }, { 0xa8, 0x60000, 0x60000, 1000 }, + { 0x6c, 0x6000, 0x4000, 0 }, + }; + u32 top = 0x9000 + lane * 0x7000; + u32 base = top + 0x2000; + + core_set32(p, top + 0x160, 1); + core_set32(p, top + 0x1dc, 0x18000); + j700_dp_steps(p, base, setup, ARRAY_SIZE(setup)); + j700_dp_output_power(p, lane, false); + core_set32(p, base + 0x6c, BIT(11)); + core_mask32(p, top + 0x1d4, 3, 1); + ndelay(10); + core_set32(p, top + 0x1d4, 2); + j700_dp_steps(p, base, settle, ARRAY_SIZE(settle)); + j700_dp_equalize(p, lane, false, 0x3f000); + core_mask32(p, base + 0xa8, 0x380000, + rate == ATCPHY_DP_LINK_RATE_RBR ? 0x280000 : 0x200000); + core_mask32(p, base + 0xa8, 0xc00000, 0x800000); + ndelay(50); + core_set32(p, base + 0x30, 0xc00); + ndelay(100); +} + +static void j700_dp_outputs_off(struct apple_atcphy *p, unsigned int lane) +{ + static const struct j700_dp_step tx[] = { + { 0x1c, 0xc00000, 0xc00000, 0 }, { 0x10, 0x600, 0x400, 0 }, + { 0x04, 0x30000000, 0x20000000, 250 }, + { 0x44, 0x100000, 0, 250 }, { 0x44, 0x400000, 0, 250 }, + { 0x44, 0x4000000, 0, 250 }, { 0x44, 0x1000000, 0, 250 }, + { 0x44, 0x1000, 0, 0 }, + }; + static const struct j700_dp_step rx[] = { + { 0x30, 0xc00, 0x800, 50 }, { 0xa8, 0xc00000, 0xc00000, 500 }, + { 0xa8, 0x60000, 0x40000, 0 }, { 0x68, 0x60, 0x40, 200 }, + { 0x68, 0x180, 0x100, 200 }, { 0xd4, 0x10000, 0, 100 }, + { 0x6c, 0x600, 0x600, 0 }, + }; + const u32 tx_bits[] = { BIT(8), BIT(0), BIT(4) }; + const u32 rx_bits[] = { BIT(14), BIT(6), BIT(10) }; + + j700_dp_steps(p, 0xd000 + lane * 0x7000, tx, ARRAY_SIZE(tx)); + for (unsigned int i = 0; i < ARRAY_SIZE(tx_bits); i++) { + core_mask32(p, 0x10c, tx_bits[i] << lane, i ? 0 : tx_bits[i] << lane); + udelay(1); + } + j700_dp_steps(p, 0xb000 + lane * 0x7000, rx, ARRAY_SIZE(rx)); + core_mask32(p, 0x91d4 + lane * 0x7000, 3, 1); + core_clear32(p, 0xb06c + lane * 0x7000, BIT(11)); + for (unsigned int i = 0; i < ARRAY_SIZE(rx_bits); i++) { + core_mask32(p, 0x8, rx_bits[i] << lane, i ? 0 : rx_bits[i] << lane); + udelay(1); + } + for (unsigned int i = 0; i < 2; i++) { + core_clear32(p, 0xc, BIT(4 * i + lane)); + udelay(1); + } +} + +static int j700_dp_stop(struct apple_atcphy *p) +{ + int ret; + + if (!p->dp_owned_pmas) + return 0; + core_set32(p, 0x7000, BIT(0)); + for (unsigned int i = 0; i < 2; i++) + if (p->dp_owned_pmas & BIT(i)) + j700_dp_outputs_off(p, i); + core_mask32(p, 0x7000, 0xc, 8); + core_mask32(p, 0x2000, 0x1ffffff9, 0x10000001); + ret = j700_dp_wait(p, 0x2000, BIT(1), true); + if (ret) + return ret; + core_mask32(p, 0x2000, 0x1ffffff9, 0x10000018); + ret = j700_dp_wait(p, 0x2000, BIT(1), false); + if (!ret) + ret = j700_dp_wait(p, 0x7034, BIT(3), false); + if (ret) + return ret; + core_clear32(p, 0x2200, BIT(3)); + for (unsigned int i = 0; i < 2; i++) { + if (!(p->dp_owned_pmas & BIT(i))) + continue; + ret = j700_pma_idle(p, i); + if (ret) + return ret; + } + p->dp_owned_pmas = 0; + p->dp_link_rate = -1; + return 0; +} + +static int j700_dp_start(struct apple_atcphy *p, unsigned int pmas, int rate, bool ssc) +{ + static const u32 parameters[4][4] = { + { 0x1e0e021c, 0, 0x156600, 0x15671b }, + { 0x1e0e01c2, 0x07fffffe, 0x155200, 0x155316 }, + { 0x1e0e01c2, 0x07fffffe, 0x554800, 0x554916 }, + { 0x1e0e02a3, 0x0bff7ffc, 0x554810, 0x564922 }, + }; + int ret; + + if (!pmas || (pmas & ~3U) || rate < 0 || (unsigned int)rate >= ARRAY_SIZE(parameters)) + return -EINVAL; + /* Claim before the first write: failed setup must still be cleaned up. */ + p->dp_owned_pmas = pmas; + p->dp_link_rate = -1; + for (unsigned int i = 0; i < 2; i++) + if (pmas & BIT(i)) + j700_dp_power(p, i); + core_clear32(p, 0x2234, 3); + core_mask32(p, 0x7000, 0x1ff0, 0x490); + for (unsigned int i = 0; i < 3; i++) + writel(parameters[rate][i == 2 ? 2 + ssc : i], p->regs.core + 0x2080 + 4 * i); + core_mask32(p, 0x2208, 0x1f0000, 0x70000); + core_set32(p, 0x2218, 1); + core_set32(p, 0x2200, BIT(3)); + core_mask32(p, 0x2000, 0x1ffffff9, 0x10000001); + ret = j700_dp_wait(p, 0x2000, BIT(1), true); + if (!ret) + ret = j700_dp_wait(p, 0x7034, BIT(3), true); + if (ret) + return ret; + core_mask32(p, 0x2000, 0x1ffffff9, 0x10014000); + ret = j700_dp_wait(p, 0x2000, BIT(1), false); + if (ret) + return ret; + for (unsigned int i = 0; i < 2; i++) + if (pmas & BIT(i)) + core_mask32(p, 0xb064 + i * 0x7000, 0xc, 8); + core_set32(p, 0x7000, 0xc); + udelay(1); + for (unsigned int i = 0; i < 2; i++) { + if (!(pmas & BIT(i))) + continue; + j700_dp_transmitter(p, i, rate); + j700_dp_receiver(p, i, rate); + } + core_clear32(p, 0x7000, 1); + p->dp_link_rate = rate; + p->dp_spread_spectrum = ssc; + return 0; +} + +static int j700_dp_configure(struct apple_atcphy *p, const struct phy_configure_opts_dp *o) +{ + const struct atcphy_mode_configuration *mode; + unsigned int pmas, capacity, first, table; + int rate = -1, ret; + + guard(mutex)(&p->lock); + if ((o->set_rate && !o->link_rate) || (o->set_lanes && !o->lanes)) + return o->set_voltages ? -EINVAL : j700_dp_stop(p); + if (p->mode != APPLE_ATCPHY_MODE_DP && p->mode != APPLE_ATCPHY_MODE_USB3_DP) + return -EINVAL; + mode = atcphy_get_mode_config(p, p->mode); + pmas = !!mode->dp_lane[0] | (!!mode->dp_lane[1] << 1); + capacity = 2 * hweight32(pmas); + if (o->lanes > capacity || (o->set_lanes && o->lanes != 1 && + o->lanes != 2 && o->lanes != 4)) + return -EINVAL; + if (!p->dp_presets_valid[0]) + return -ENOENT; + if (o->set_voltages) { + if (!o->lanes) + return -EINVAL; + for (unsigned int i = 0; i < o->lanes; i++) + if (o->voltage[i] > 3 || o->pre[i] > 3) + return -EINVAL; + } + if (o->set_rate) { + const unsigned int rates[] = { 1620, 2700, 5400, 8100 }; + + for (unsigned int i = 0; i < ARRAY_SIZE(rates); i++) + if (o->link_rate == rates[i]) + rate = i; + if (rate < 0) + return -EINVAL; + if (p->dp_link_rate != rate || p->dp_owned_pmas != pmas || + p->dp_spread_spectrum != o->ssc) { + ret = j700_dp_stop(p); + if (ret) + return ret; + ret = j700_dp_start(p, pmas, rate, o->ssc); + if (ret) { + int cleanup = j700_dp_stop(p); + + dev_err(p->dev, "DP setup failed: %d, cleanup: %d\n", ret, cleanup); + return ret; + } + } + } + if (!o->set_voltages) + return 0; + if (!p->dp_owned_pmas || p->dp_link_rate < 0) + return -EINVAL; + table = p->dp_link_rate + 1; + if (!p->dp_presets_valid[table]) + table = 0; + first = pmas == 3 ? !p->swap_lanes : (pmas == 2); + for (unsigned int i = 0; i < o->lanes; i++) + j700_dp_equalize(p, first ^ (i / 2), i % 2, + p->dp_presets[table][4 * o->voltage[i] + o->pre[i]]); + return 0; +} + +#endif diff --git a/drivers/phy/apple/atc.c b/drivers/phy/apple/atc.c index 600a3f09dcecf..4d5bba1db5c3d 100644 --- a/drivers/phy/apple/atc.c +++ b/drivers/phy/apple/atc.c @@ -654,6 +654,10 @@ struct atcphy_hw { * @ss_tunables: The complete SuperSpeed tunable set was supplied * @dp_tunables: The complete T8140 DisplayPort bundle was supplied * @aux_power_requested: AUX power-up began and needs matching shutdown + * @dp_owned_pmas: J700 PMAs requiring main-link cleanup + * @dp_spread_spectrum: J700 main-link spread-spectrum state + * @dp_presets: Firmware-derived per-rate drive presets + * @dp_presets_valid: Firmware supplied each drive-preset table * @fixed_usb2: The USB2 pairs go to a fixed hub on the USB controller, which * stays in host mode; the PHY provides USB2 from probe on * @typec_mode: Mode the Type-C mux last asked for; a fixed-hub port returns to @@ -713,6 +717,10 @@ struct apple_atcphy { bool ss_tunables; bool dp_tunables; bool aux_power_requested; + u8 dp_owned_pmas; + bool dp_spread_spectrum; + u32 dp_presets[5][16]; + bool dp_presets_valid[5]; bool fixed_usb2; enum atcphy_mode typec_mode; bool host_active; @@ -1483,6 +1491,8 @@ static int j700_pma_idle(struct apple_atcphy *atcphy, unsigned int lane) return 0; } +#include "atc-j700-dp.h" + static int atcphy_configure_lanes(struct apple_atcphy *atcphy, enum atcphy_mode mode) { const struct atcphy_mode_configuration *mode_cfg = atcphy_get_mode_config(atcphy, mode); @@ -1673,8 +1683,11 @@ static int atcphy_enable_dp_aux(struct apple_atcphy *atcphy) static int atcphy_disable_dp_aux(struct apple_atcphy *atcphy) { - if (atcphy->hw->has_t8140_dp) - return j700_aux_disable(atcphy); + if (atcphy->hw->has_t8140_dp) { + int ret = j700_dp_stop(atcphy); + + return ret ? ret : j700_aux_disable(atcphy); + } /* FIXME */ if (atcphy->hw->gen != ATCPHY_GENERATION_T8122) { @@ -2664,6 +2677,11 @@ static int atcphy_configure(struct apple_atcphy *atcphy, enum atcphy_mode mode) u32 reg; lockdep_assert_held(&atcphy->lock); + if (atcphy->hw->has_t8140_dp && atcphy->dp_owned_pmas && mode != atcphy->mode) { + ret = j700_dp_stop(atcphy); + if (ret) + return ret; + } if (atcphy->hw->has_t8140_dp && atcphy_modes[mode].enable_dp_aux && !atcphy->dp_tunables) return -EOPNOTSUPP; @@ -2909,6 +2927,9 @@ static int atcphy_dpphy_configure(struct phy *phy, union phy_configure_opts *opt struct apple_atcphy *atcphy = phy_get_drvdata(phy); enum atcphy_dp_link_rate link_rate; + if (atcphy->hw->has_t8140_dp) + return j700_dp_configure(atcphy, opts); + if (opts->set_voltages) return -EINVAL; if (opts->set_lanes) @@ -3340,6 +3361,26 @@ static int atcphy_load_t8140_dp(struct apple_atcphy *atcphy) return PTR_ERR(*lists[i].out); } + for (unsigned int i = 0; i < ARRAY_SIZE(atcphy->dp_presets); i++) { + static const char * const properties[] = { + "apple,dp-training-table", "apple,dp-training-table-rbr", + "apple,dp-training-table-hbr", "apple,dp-training-table-hbr2", + "apple,dp-training-table-hbr3", + }; + int count = of_property_count_u32_elems(atcphy->np, properties[i]); + int ret; + + if (i && !of_find_property(atcphy->np, properties[i], NULL)) + continue; + if (count != ARRAY_SIZE(atcphy->dp_presets[i])) + return -EINVAL; + ret = of_property_read_u32_array(atcphy->np, properties[i], + atcphy->dp_presets[i], count); + if (ret) + return ret; + atcphy->dp_presets_valid[i] = true; + } + atcphy->dp_tunables = true; return 0; } diff --git a/tools/diagnostics/apple-dptx-training-test.c b/tools/diagnostics/apple-dptx-training-test.c new file mode 100644 index 0000000000000..53ae4419f293d --- /dev/null +++ b/tools/diagnostics/apple-dptx-training-test.c @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* Copyright 2026 Aurora Silicon */ +#include +#include +#include +#include +#include +#include + +typedef uint8_t u8; +typedef uint32_t u32; +typedef uint16_t u16; +struct phy { int unused; }; +struct settings { + unsigned int lanes, voltage[4], pre[4]; + bool set_voltages; +}; + +union phy_configure_opts { struct settings dp; }; +struct dptx_port { + struct phy *atcphy; + u32 trained_voltage[4], trained_pre[4]; +}; + +struct apple_epic_service { struct dptx_port *cookie; }; +static unsigned int calls; +static int failure; +#define PHY_MODE_DP 1 + +static u32 get_unaligned_le32(const void *p) +{ + u32 v; + + memcpy(&v, p, sizeof(v)); + return v; +} + +static void put_unaligned_le32(u32 v, void *p) +{ + memcpy(p, &v, sizeof(v)); +} + +static int phy_validate(struct phy *p, int mode, int sub, union phy_configure_opts *o) +{ + assert(p && mode == PHY_MODE_DP && !sub); + o->dp.lanes = 2; + return 0; +} + +static int phy_configure(struct phy *p, union phy_configure_opts *o) +{ + assert(p && o->dp.set_voltages && o->dp.lanes <= 2); + calls++; + return failure; +} + +#include "drivers/gpu/drm/apple/dptx-j700-training.h" + +#ifdef DPTX_GROUP_TEST +#define DPTX_APCALL_SET_DRIVE_SETTINGS 3 +#define DPTX_APCALL_GET_DRIVE_SETTINGS 4 +static int dptxport_call(struct apple_epic_service *service, u32 command, + const void *data, size_t size, void *reply, size_t room) +{ + (void)service; + (void)command; + (void)data; + (void)size; + (void)reply; + (void)room; + return 7; +} + +#include "group-call.h" +#endif + +int main(void) +{ + struct phy phy = {}; + struct dptx_port port = { .atcphy = &phy }; + struct apple_epic_service service = { .cookie = &port }; + u8 input[81] = {}, output[81]; + u8 *request = input + 1, *reply = output + 1; + unsigned int cases = 0; + + put_unaligned_le32(2, request + 16); + put_unaligned_le32(1, request + 36); + put_unaligned_le32(2, request + 52); + for (unsigned int size = 0; size < 56; size++) { + assert(dptx_j700_training(&service, true, request, size, reply, 80) == -EINVAL); + assert(dptx_j700_training(&service, true, request, 80, reply, size) == -EINVAL); + assert(!calls); + cases += 2; + } + put_unaligned_le32(9, request + 52); + assert(dptx_j700_training(&service, true, request, 80, reply, 80) == -EINVAL); + assert(!calls && !port.trained_voltage[0]); + put_unaligned_le32(2, request + 52); + failure = -EIO; + assert(dptx_j700_training(&service, true, request, 80, reply, 80) == -EIO); + assert(!port.trained_voltage[0]); + failure = 0; + assert(!dptx_j700_training(&service, true, request, 80, reply, 80)); + assert(port.trained_voltage[0] == 1 && port.trained_pre[1] == 2); + assert(get_unaligned_le32(reply + 16) == 2 && !get_unaligned_le32(reply)); + put_unaligned_le32(4, request); + assert(!dptx_j700_training(&service, false, request, 32, reply, 80)); + assert(get_unaligned_le32(reply) == 4 && !get_unaligned_le32(reply + 16)); + assert(get_unaligned_le32(reply + 36) == 1 && get_unaligned_le32(reply + 52) == 2); + assert(!get_unaligned_le32(reply + 76)); + +#ifdef DPTX_GROUP_TEST + for (unsigned int group = 0; group < 4; group++) { + for (unsigned int command = 0; command < 25; command++) { + int expected = group ? -EOPNOTSUPP : 7; + + if (group == 0 && command == 3) + expected = -EOPNOTSUPP; + if ((group == 1 && command == 3) || (group == 0 && command == 4)) + expected = 0; + assert(dptxport_group_call(&service, group, command, + request, 80, reply, 80) == expected); + } + } +#endif + port.atcphy = NULL; + assert(dptx_j700_training(&service, true, request, 80, reply, 80) == -ENODEV); + printf("DPTX training: %u boundary cases plus failure/cache/readback checks passed\n", + cases); + return 0; +} diff --git a/tools/diagnostics/apple-j700-dp-test.c b/tools/diagnostics/apple-j700-dp-test.c new file mode 100644 index 0000000000000..d6a6219fe9ad1 --- /dev/null +++ b/tools/diagnostics/apple-j700-dp-test.c @@ -0,0 +1,226 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* Copyright 2026 Aurora Silicon */ +#include +#include +#include +#include +#include +#include + +typedef uint32_t u32; +typedef uint16_t u16; +#define BIT(n) (1U << (n)) +#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) +#define hweight32(v) __builtin_popcount(v) +static void test_guard_mutex(void *p) { (void)p; } +#define guard(kind) test_guard_##kind +static void report(void *p, const char *s, ...) { (void)p; (void)s; } +#define dev_err report +#define ndelay(n) ((void)(n)) +#define udelay(n) ((void)(n)) + +enum { + ATCPHY_DP_LINK_RATE_RBR, ATCPHY_DP_LINK_RATE_HBR, + ATCPHY_DP_LINK_RATE_HBR2, ATCPHY_DP_LINK_RATE_HBR3, +}; + +enum { APPLE_ATCPHY_MODE_OFF, APPLE_ATCPHY_MODE_DP, APPLE_ATCPHY_MODE_USB3_DP }; +struct phy_configure_opts_dp { + unsigned int link_rate, lanes, voltage[4], pre[4]; + bool set_rate, set_lanes, set_voltages, ssc; +}; + +struct atcphy_mode_configuration { bool dp_lane[2]; }; +struct apple_atcphy { + struct { void *core; } regs; + void *dev; + int lock, mode, dp_link_rate; + unsigned int dp_owned_pmas; + bool dp_spread_spectrum, swap_lanes, dp_presets_valid[5]; + u32 dp_presets[5][16]; +}; + +static u32 registers[0x20000 / 4]; +static unsigned int writes, polls, fail_poll, pin_mask = 2; +static bool fail_all; + +static void core_mask32(struct apple_atcphy *p, u32 off, u32 mask, u32 value) +{ + (void)p; + assert(!(off % 4) && off / 4 < ARRAY_SIZE(registers)); + assert(!(value & ~mask)); + registers[off / 4] = (registers[off / 4] & ~mask) | value; + writes++; + if (off == 0x2000) { + if (value == 0x10000001) { + registers[off / 4] |= BIT(1); + registers[0x7034 / 4] |= BIT(3); + } else { + registers[off / 4] &= ~BIT(1); + if (value == 0x10000018) + registers[0x7034 / 4] &= ~BIT(3); + } + } +} + +static void core_set32(struct apple_atcphy *p, u32 o, u32 v) +{ + core_mask32(p, o, v, v); +} + +static void core_clear32(struct apple_atcphy *p, u32 o, u32 v) +{ + core_mask32(p, o, v, 0); +} + +static void writel(u32 value, void *ptr) +{ + u32 off = (char *)ptr - (char *)registers; + + core_mask32(NULL, off, ~0U, value); +} + +static u32 readl(void *ptr) { return *(u32 *)ptr; } +#define readl_poll_timeout_atomic(addr, val, condition, delay, timeout) \ + ({ \ + int rc; \ + (void)(delay); \ + (void)(timeout); \ + polls++; \ + val = readl(addr); \ + rc = fail_all || polls == fail_poll || !(condition) ? -ETIMEDOUT : 0; \ + rc; \ + }) + +static int j700_pma_idle(struct apple_atcphy *p, unsigned int lane) +{ + core_set32(p, 0xc, BIT(2 + lane) | BIT(6 + lane)); + core_mask32(p, 0x200, 15U << (4 * lane), 1U << (4 * lane)); + return 0; +} + +static const struct atcphy_mode_configuration * +atcphy_get_mode_config(struct apple_atcphy *p, int mode) +{ + static struct atcphy_mode_configuration config; + + (void)p; + (void)mode; + config.dp_lane[0] = pin_mask & 1; + config.dp_lane[1] = pin_mask & 2; + return &config; +} + +#include "drivers/phy/apple/atc-j700-dp.h" + +static struct apple_atcphy device(void) +{ + struct apple_atcphy p = { + .regs.core = registers, .mode = APPLE_ATCPHY_MODE_USB3_DP, + .dp_link_rate = -1, + }; + + memset(registers, 0xa5, sizeof(registers)); + for (unsigned int i = 0; i < ARRAY_SIZE(p.dp_presets_valid); i++) + p.dp_presets_valid[i] = true; + writes = 0; + polls = 0; + fail_poll = 0; + fail_all = false; + return p; +} + +int main(void) +{ + const unsigned int rates[] = { 1620, 2700, 5400, 8100 }; + unsigned int cases = 0; + + for (pin_mask = 1; pin_mask <= 3; pin_mask++) { + for (unsigned int rate = 0; rate < ARRAY_SIZE(rates); rate++) { + for (unsigned int ssc = 0; ssc < 2; ssc++) { + struct apple_atcphy p = device(); + struct phy_configure_opts_dp o = { + .set_rate = true, .link_rate = rates[rate], .ssc = ssc, + }; + u32 before = registers[0x200 / 4]; + + assert(!j700_dp_configure(&p, &o)); + assert(p.dp_owned_pmas == pin_mask && p.dp_link_rate == (int)rate); + if (pin_mask != 3) { + u32 other = pin_mask == 1 ? 0xf0 : 0xf; + + assert(!((registers[0x200 / 4] ^ before) & other)); + } + unsigned int first_writes = writes; + + assert(!j700_dp_configure(&p, &o) && writes == first_writes); + struct phy_configure_opts_dp voltage = { + .set_voltages = true, .lanes = 2 * hweight32(pin_mask), + }; + + for (unsigned int lane = 0; lane < voltage.lanes; lane++) { + unsigned int index = 4 * lane + 1; + + voltage.voltage[lane] = lane; + voltage.pre[lane] = 1; + p.dp_presets[rate + 1][index] = 0x12340 + lane; + } + assert(!j700_dp_configure(&p, &voltage)); + for (unsigned int lane = 0; lane < voltage.lanes; lane++) { + unsigned int first = pin_mask == 1 ? 0 : 1; + u32 base = (lane % 2 ? 0xc000 : 0x9000) + + (first ^ (lane / 2)) * 0x7000; + u32 offset = lane % 2 ? 0x50 : 0x210; + u32 preset = 0x12340 + lane; + + assert((registers[(base + offset) / 4] & 0xc00f) == + (((preset & 3) << 14) | ((preset >> 2) & 15))); + } + + assert(!j700_dp_stop(&p)); + assert(!p.dp_owned_pmas && p.dp_link_rate == -1); + cases++; + } + } + } + pin_mask = 2; + for (unsigned int fault = 1; fault <= 3; fault++) { + struct apple_atcphy p = device(); + struct phy_configure_opts_dp o = { .set_rate = true, .link_rate = 8100 }; + + fail_poll = fault; + assert(j700_dp_configure(&p, &o) == -ETIMEDOUT); + assert(!p.dp_owned_pmas && p.dp_link_rate == -1); + cases++; + } + { + struct apple_atcphy p = device(); + struct phy_configure_opts_dp o = { .set_rate = true, .link_rate = 8100 }; + + fail_all = true; + assert(j700_dp_configure(&p, &o) == -ETIMEDOUT && p.dp_owned_pmas == 2); + fail_all = false; + assert(!j700_dp_stop(&p) && !p.dp_owned_pmas); + cases++; + } + { + struct apple_atcphy p = device(); + struct phy_configure_opts_dp o = { .set_rate = true, .link_rate = 1234 }; + + assert(j700_dp_configure(&p, &o) == -EINVAL && !writes); + o.link_rate = 8100; + o.lanes = 3; + assert(j700_dp_configure(&p, &o) == -EINVAL && !writes); + o.lanes = 0; + p.dp_presets_valid[0] = false; + assert(j700_dp_configure(&p, &o) == -ENOENT && !writes); + p.dp_presets_valid[0] = true; + o.set_voltages = true; + o.lanes = 2; + o.voltage[1] = 4; + assert(j700_dp_configure(&p, &o) == -EINVAL && !writes); + cases++; + } + printf("J700 DP: %u register/state cases passed\n", cases); + return 0; +} diff --git a/tools/diagnostics/test-apple-j700-dp.py b/tools/diagnostics/test-apple-j700-dp.py new file mode 100644 index 0000000000000..41836939f6eae --- /dev/null +++ b/tools/diagnostics/test-apple-j700-dp.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +"""Compile the production J700 helpers against bounded host register models.""" +import os +from pathlib import Path +import shlex +import subprocess +import tempfile + + +root = Path(__file__).resolve().parents[2] +source = (root / "drivers/gpu/drm/apple/dptxep.c").read_text() +start = source.index("static int dptxport_group_call(") +end = source.index("\n}\n", start) + 3 +with tempfile.TemporaryDirectory(prefix="j700-dp-") as work: + work = Path(work) + (work / "group-call.h").write_text(source[start:end]) + for name in ("apple-j700-dp-test", "apple-dptx-training-test"): + command = shlex.split(os.environ.get("CC", "clang")) + command += ["-std=gnu11", "-O1", "-g", "-Wall", "-Wextra", "-Werror"] + command += ["-fsanitize=address,undefined", "-fno-omit-frame-pointer"] + command += ["-I" + str(root), "-I" + str(work), "-DDPTX_GROUP_TEST"] + command += [str(root / "tools/diagnostics" / (name + ".c"))] + command += ["-o", str(work / name)] + subprocess.run(command, check=True) + subprocess.run([str(work / name)], check=True) From a004aa36383b8b39ad522fab3ed22d31f769fdab Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 18:36:12 +1100 Subject: [PATCH 34/52] drm: apple: honor separate AFK V2 callback reply capacity A drive-settings read has a short request and a larger response. Validate both owned-buffer lengths before dispatch, allocate the response capacity, and report its body length without extending the input read. Keep the full response range ownership check before any callback side effect. The 917 codec assertions now include unequal request/response lengths, undersized buffers, limits, and invalid output pointers. Production owned copy tests and all J700 PHY/training/group tests pass under sanitizers. Strict checkpatch and the opaque copy audit are clean. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/afk.c | 27 ++++++++++--------- drivers/gpu/drm/apple/afkv2-codec.h | 19 +++++++++++++ .../selftests/drivers/gpu/apple-afkv2/codec.c | 24 +++++++++++++++++ 3 files changed, 58 insertions(+), 12 deletions(-) diff --git a/drivers/gpu/drm/apple/afk.c b/drivers/gpu/drm/apple/afk.c index b7c3905303eab..8bcdb8ec24033 100644 --- a/drivers/gpu/drm/apple/afk.c +++ b/drivers/gpu/drm/apple/afk.c @@ -696,16 +696,15 @@ static void afk_v2_callback(struct apple_dcp_afkep *ep, const struct epic_service_call *call; const u8 *request = command->inline_data; u8 *owned_request = NULL, *reply = NULL; - size_t request_size = command->oob ? command->tx_size : command->inline_size; + size_t request_size, response_size, response_body; size_t body_size, reply_size; u32 status = 0xe00002c7; int ret; if (frame->type != EPIC_SUBTYPE_STD_SERVICE || - request_size < sizeof(*call) + sizeof(u32) || - request_size > SZ_1M - AFKV2_COMMAND_SIZE || - request_size > command->value || - (command->oob && request_size > command->rx_size)) + afkv2_callback_lengths(command, sizeof(*call) + sizeof(u32), + SZ_1M - AFKV2_COMMAND_SIZE, + &request_size, &response_size)) goto reject; service = afk_service_get(afk_epic_find_service(ep, frame->interface)); if (!service || READ_ONCE(service->torndown) || @@ -717,7 +716,7 @@ static void afk_v2_callback(struct apple_dcp_afkep *ep, if (command->oob) { /* Reject an unowned response destination before any callback side effect. */ ret = afk_v2_access_owned(service, command->rx_address, NULL, - request_size, true, false); + response_size, true, false); if (ret) goto release; owned_request = kmalloc(request_size, GFP_KERNEL); @@ -737,32 +736,36 @@ static void afk_v2_callback(struct apple_dcp_afkep *ep, le32_to_cpu(call->magic) != EPIC_SERVICE_CALL_MAGIC || le32_to_cpu(call->data_len) != body_size) goto release; - reply = kzalloc(AFKV2_COMMAND_SIZE + request_size, GFP_KERNEL); + response_body = response_size - sizeof(*call); + reply = kzalloc(AFKV2_COMMAND_SIZE + response_size, GFP_KERNEL); if (!reply) { status = 0xe00002bd; goto release; } reply[1] = command->id; memcpy(reply + AFKV2_COMMAND_SIZE, call, sizeof(*call)); + ((struct epic_service_call *)(reply + AFKV2_COMMAND_SIZE))->data_len = + cpu_to_le32(response_body); if (service->ops->call_v2) ret = service->ops->call_v2(service, le16_to_cpu(call->group), le32_to_cpu(call->command), request + sizeof(*call), body_size, - reply + AFKV2_COMMAND_SIZE + sizeof(*call), body_size); + reply + AFKV2_COMMAND_SIZE + sizeof(*call), + response_body); else ret = service->ops->call(service, le32_to_cpu(call->command), request + sizeof(*call), body_size, - reply + AFKV2_COMMAND_SIZE + sizeof(*call), body_size); + reply + AFKV2_COMMAND_SIZE + sizeof(*call), response_body); if (ret) goto release; - reply_size = AFKV2_COMMAND_SIZE + request_size; + reply_size = AFKV2_COMMAND_SIZE + response_size; if (command->oob) { ret = afk_v2_copy_owned(service, command->rx_address, - reply + AFKV2_COMMAND_SIZE, request_size, true); + reply + AFKV2_COMMAND_SIZE, response_size, true); if (ret) goto release; ret = afkv2_encode_oob_command(reply, reply_size, command->id, - command->rx_address, request_size, + command->rx_address, response_size, command->tx_address, command->tx_size); if (ret) goto release; diff --git a/drivers/gpu/drm/apple/afkv2-codec.h b/drivers/gpu/drm/apple/afkv2-codec.h index cdd5d5ad20b22..8990182275354 100644 --- a/drivers/gpu/drm/apple/afkv2-codec.h +++ b/drivers/gpu/drm/apple/afkv2-codec.h @@ -212,6 +212,25 @@ static inline int afkv2_decode_command(const void *payload, size_t size, return 0; } +/* Requests and responses may have different owned-buffer capacities. */ +static inline int afkv2_callback_lengths(const struct afkv2_command *command, + size_t minimum, size_t limit, + size_t *request, size_t *response) +{ + size_t in, out; + + if (!command || !request || !response || minimum > limit) + return -EINVAL; + in = command->oob ? command->tx_size : command->inline_size; + out = command->oob ? command->rx_size : command->inline_size; + if (in < minimum || out < minimum || in > limit || out > limit || + in > command->value || out > command->value) + return -EINVAL; + *request = in; + *response = out; + return 0; +} + /* Correlate only; no buffer is read, written or freed by this helper. */ static inline int afkv2_check_response(const struct afkv2_frame *frame, const struct afkv2_command *response, diff --git a/tools/testing/selftests/drivers/gpu/apple-afkv2/codec.c b/tools/testing/selftests/drivers/gpu/apple-afkv2/codec.c index de49360bdabea..79598033c084a 100644 --- a/tools/testing/selftests/drivers/gpu/apple-afkv2/codec.c +++ b/tools/testing/selftests/drivers/gpu/apple-afkv2/codec.c @@ -178,6 +178,30 @@ int main(void) CHECK(!afkv2_owned_span(0x1000, 0, 0x1000, 0)); CHECK(afkv2_owned_span(UINT64_MAX - 15, 16, UINT64_MAX, 1)); CHECK(!afkv2_owned_span(UINT64_MAX - 15, 17, UINT64_MAX, 1)); + struct afkv2_command callback = { .oob = true, .tx_size = 96, + .rx_size = 144, .value = 144 }; + size_t input = 0, output = 0; + + CHECK(!afkv2_callback_lengths(&callback, 68, 1024, &input, &output)); + CHECK(input == 96 && output == 144); + callback.value = 143; + CHECK(afkv2_callback_lengths(&callback, 68, 1024, &input, &output)); + callback.value = 2048; + callback.rx_size = 2048; + CHECK(afkv2_callback_lengths(&callback, 68, 1024, &input, &output)); + callback.rx_size = 67; + CHECK(afkv2_callback_lengths(&callback, 68, 1024, &input, &output)); + callback.rx_size = 144; + callback.tx_size = 67; + CHECK(afkv2_callback_lengths(&callback, 68, 1024, &input, &output)); + callback.oob = false; + callback.inline_size = 96; + CHECK(!afkv2_callback_lengths(&callback, 68, 1024, &input, &output)); + CHECK(input == 96 && output == 96); + CHECK(afkv2_callback_lengths(NULL, 68, 1024, &input, &output)); + CHECK(afkv2_callback_lengths(&callback, 68, 1024, NULL, &output)); + CHECK(afkv2_callback_lengths(&callback, 68, 1024, &input, NULL)); + CHECK(afkv2_callback_lengths(&callback, 1025, 1024, &input, &output)); printf("PASS %u assertions\n", assertions); return 0; } From d863129e9cf58169827cb04ba21a3411898a134a Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 18:42:34 +1100 Subject: [PATCH 35/52] phy: apple: preserve J700 lane-routing state during DP setup Use the audited T8140 routing masks for USB3/DP and four-lane DP. The generic sequence changes the miscellaneous lane-swap control and clears routing state that this generation preserves. Update the low protocol fields in order and idle only the PMAs assigned to DisplayPort. Add opt-in callback and PHY completion diagnostics to distinguish firmware waiting from a blocked PHY operation. No calibration values are logged. The routing values are hardware facts from the user-authorized audit; this implementation is independently written. Copy audit clean. Host checks pass: 33 PHY routing/state cases, 112 training bounds, 100 group routing cases, 917 codec assertions, owned-copy and AUX transition tests. Strict checkpatch passes. External-display qualification remains pending. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/afk.c | 6 ++++++ drivers/phy/apple/atc-j700-dp.h | 27 ++++++++++++++++++++++++++ drivers/phy/apple/atc.c | 17 +++++----------- tools/diagnostics/apple-j700-dp-test.c | 19 ++++++++++++++++++ 4 files changed, 57 insertions(+), 12 deletions(-) diff --git a/drivers/gpu/drm/apple/afk.c b/drivers/gpu/drm/apple/afk.c index 8bcdb8ec24033..38b44c2d6d785 100644 --- a/drivers/gpu/drm/apple/afk.c +++ b/drivers/gpu/drm/apple/afk.c @@ -701,6 +701,8 @@ static void afk_v2_callback(struct apple_dcp_afkep *ep, u32 status = 0xe00002c7; int ret; + dev_dbg(ep->dcp->dev, "AFKV2 callback sizes: tx=%u rx=%u value=%u inline=%zu\n", + command->tx_size, command->rx_size, command->value, command->inline_size); if (frame->type != EPIC_SUBTYPE_STD_SERVICE || afkv2_callback_lengths(command, sizeof(*call) + sizeof(u32), SZ_1M - AFKV2_COMMAND_SIZE, @@ -746,6 +748,8 @@ static void afk_v2_callback(struct apple_dcp_afkep *ep, memcpy(reply + AFKV2_COMMAND_SIZE, call, sizeof(*call)); ((struct epic_service_call *)(reply + AFKV2_COMMAND_SIZE))->data_len = cpu_to_le32(response_body); + dev_dbg(ep->dcp->dev, "AFKV2 callback begin: group=%u command=%u in=%zu out=%zu\n", + le16_to_cpu(call->group), le32_to_cpu(call->command), body_size, response_body); if (service->ops->call_v2) ret = service->ops->call_v2(service, le16_to_cpu(call->group), le32_to_cpu(call->command), @@ -756,6 +760,8 @@ static void afk_v2_callback(struct apple_dcp_afkep *ep, ret = service->ops->call(service, le32_to_cpu(call->command), request + sizeof(*call), body_size, reply + AFKV2_COMMAND_SIZE + sizeof(*call), response_body); + dev_dbg(ep->dcp->dev, "AFKV2 callback end: group=%u command=%u result=%d\n", + le16_to_cpu(call->group), le32_to_cpu(call->command), ret); if (ret) goto release; reply_size = AFKV2_COMMAND_SIZE + response_size; diff --git a/drivers/phy/apple/atc-j700-dp.h b/drivers/phy/apple/atc-j700-dp.h index 185551e5a589e..5ce78d085159d 100644 --- a/drivers/phy/apple/atc-j700-dp.h +++ b/drivers/phy/apple/atc-j700-dp.h @@ -3,6 +3,29 @@ #ifndef APPLE_ATC_J700_DP_H #define APPLE_ATC_J700_DP_H +/* Preserve the upper routing state established by the platform firmware. */ +static int j700_dp_route(struct apple_atcphy *p, bool four_lanes) +{ + u32 protocols = four_lanes ? 0x492 : (p->swap_lanes ? 0x252 : 0x489); + int ret; + + core_mask32(p, 0x60, 0xfff, protocols); + if (four_lanes) { + core_mask32(p, 0x64, 0x1f, 0x14); + core_mask32(p, 0x64, 0x3ffe0, p->swap_lanes ? 0x100 : 0x22000); + } else { + core_mask32(p, 0x64, 0x1ffff, 0x110 | p->swap_lanes); + } + for (unsigned int lane = 0; lane < 2; lane++) { + if (!four_lanes && lane != !p->swap_lanes) + continue; + ret = j700_pma_idle(p, lane); + if (ret) + return ret; + } + return 0; +} + /* Register facts from the authorized J700 hardware audit. */ struct j700_dp_step { u32 offset, mask, value; @@ -213,6 +236,7 @@ static int j700_dp_stop(struct apple_atcphy *p) } p->dp_owned_pmas = 0; p->dp_link_rate = -1; + dev_dbg(p->dev, "J700 DP main stopped\n"); return 0; } @@ -265,6 +289,7 @@ static int j700_dp_start(struct apple_atcphy *p, unsigned int pmas, int rate, bo core_clear32(p, 0x7000, 1); p->dp_link_rate = rate; p->dp_spread_spectrum = ssc; + dev_dbg(p->dev, "J700 DP main ready: PMAs=%u rate=%d ssc=%u\n", pmas, rate, ssc); return 0; } @@ -274,7 +299,9 @@ static int j700_dp_configure(struct apple_atcphy *p, const struct phy_configure_ unsigned int pmas, capacity, first, table; int rate = -1, ret; + dev_dbg(p->dev, "J700 DP configure waiting for PHY lock\n"); guard(mutex)(&p->lock); + dev_dbg(p->dev, "J700 DP configure acquired PHY lock\n"); if ((o->set_rate && !o->link_rate) || (o->set_lanes && !o->lanes)) return o->set_voltages ? -EINVAL : j700_dp_stop(p); if (p->mode != APPLE_ATCPHY_MODE_DP && p->mode != APPLE_ATCPHY_MODE_USB3_DP) diff --git a/drivers/phy/apple/atc.c b/drivers/phy/apple/atc.c index 4d5bba1db5c3d..0e2d74959b41c 100644 --- a/drivers/phy/apple/atc.c +++ b/drivers/phy/apple/atc.c @@ -1497,6 +1497,11 @@ static int atcphy_configure_lanes(struct apple_atcphy *atcphy, enum atcphy_mode { const struct atcphy_mode_configuration *mode_cfg = atcphy_get_mode_config(atcphy, mode); + if (atcphy->hw->has_t8140_dp && + (mode == APPLE_ATCPHY_MODE_USB3 || mode == APPLE_ATCPHY_MODE_USB3_DP || + mode == APPLE_ATCPHY_MODE_DP)) + return j700_dp_route(atcphy, mode == APPLE_ATCPHY_MODE_DP); + core_mask32(atcphy, atcphy->hw->aciophy_lane_mode, ACIOPHY_LANE_MODE_RX0, FIELD_PREP(ACIOPHY_LANE_MODE_RX0, mode_cfg->lane_mode[0])); core_mask32(atcphy, atcphy->hw->aciophy_lane_mode, ACIOPHY_LANE_MODE_TX0, @@ -1520,18 +1525,6 @@ static int atcphy_configure_lanes(struct apple_atcphy *atcphy, enum atcphy_mode else core_clear32(atcphy, atcphy->hw->aciophy_crossbar, ACIOPHY_CROSSBAR_DP_BOTH_PMA); - if (atcphy->hw->has_t8140_dp) { - for (unsigned int lane = 0; lane < 2; lane++) { - int ret; - - if (!mode_cfg->dp_lane[lane]) - continue; - ret = j700_pma_idle(atcphy, lane); - if (ret) - return ret; - } - return 0; - } /* unclear if we need the remainder for t8122 */ if (atcphy->hw->gen == ATCPHY_GENERATION_T8122) diff --git a/tools/diagnostics/apple-j700-dp-test.c b/tools/diagnostics/apple-j700-dp-test.c index d6a6219fe9ad1..0b3e24067ac05 100644 --- a/tools/diagnostics/apple-j700-dp-test.c +++ b/tools/diagnostics/apple-j700-dp-test.c @@ -16,6 +16,7 @@ static void test_guard_mutex(void *p) { (void)p; } #define guard(kind) test_guard_##kind static void report(void *p, const char *s, ...) { (void)p; (void)s; } #define dev_err report +#define dev_dbg report #define ndelay(n) ((void)(n)) #define udelay(n) ((void)(n)) @@ -183,6 +184,24 @@ int main(void) } } } + for (unsigned int full = 0; full < 2; full++) { + for (unsigned int swap = 0; swap < 2; swap++) { + struct apple_atcphy p = device(); + u32 old_selector = registers[0x64 / 4]; + u32 old_misc = registers[4 / 4]; + u32 mask = full ? 0x3ffff : 0x1ffff; + + p.swap_lanes = swap; + assert(!j700_dp_route(&p, full)); + assert(!((registers[0x64 / 4] ^ old_selector) & ~mask)); + assert(registers[4 / 4] == old_misc); + assert((registers[0x60 / 4] & 0xfff) == + (full ? 0x492 : (swap ? 0x252 : 0x489))); + assert((registers[0x64 / 4] & mask) == + (full ? (swap ? 0x114 : 0x22014) : (0x110 | swap))); + cases++; + } + } pin_mask = 2; for (unsigned int fault = 1; fault <= 3; fault++) { struct apple_atcphy p = device(); From 7a294dd592be90d4b6322ead8a6a5a241a095a63 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 18:45:36 +1100 Subject: [PATCH 36/52] mux: apple: retain the physical J700 crossbar gate sequence Keep the newer T602x DP IN clock controls for existing users, but exclude them from the J700 physical DP output. That output uses the original T602x gate sequence and read-clock reset location. The previous generic path wrote the additional source/output gate registers on J700 too. This small condition is derived from audited hardware facts; no private implementation is imported. The production change has no copy-audit matches. Shared generic test-extractor/header boilerplate was reviewed. Validate the actual production mux function for every source on all three outputs, both with and without the J700 condition: 54 select/disconnect scenarios pass under ASan/UBSan. Existing PHY, training and AFK checks pass. Strict checkpatch passes; external hardware qualification remains pending. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- MAINTAINERS | 1 + drivers/mux/apple-display-crossbar.c | 17 ++- tools/diagnostics/test-apple-j700-crossbar.py | 119 ++++++++++++++++++ 3 files changed, 132 insertions(+), 5 deletions(-) create mode 100644 tools/diagnostics/test-apple-j700-crossbar.py diff --git a/MAINTAINERS b/MAINTAINERS index 4db1e45c648ce..c157347692c6f 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -2018,6 +2018,7 @@ F: rust/kernel/soc/apple/aop.rs F: sound/soc/apple/t8140_aop_audio.rs F: tools/diagnostics/apple-dptx-training-test.c F: tools/diagnostics/apple-j700-dp-test.c +F: tools/diagnostics/test-apple-j700-crossbar.py F: tools/diagnostics/test-apple-j700-dp.py F: tools/testing/apple/ diff --git a/drivers/mux/apple-display-crossbar.c b/drivers/mux/apple-display-crossbar.c index f49977b846f04..a0a7ac57ccf00 100644 --- a/drivers/mux/apple-display-crossbar.c +++ b/drivers/mux/apple-display-crossbar.c @@ -210,6 +210,8 @@ static int apple_dpxbar_set_t602x(struct mux_control *mux, int state) unsigned int dispext_bit_en; u32 atc_bit, mux_mask, mux_val; bool enable; + /* J700 physical DP retains the original T602x clock-gate sequence. */ + bool j700_physical = index == MUX_DPPHY && of_machine_is_compatible("apple,j700"); int ret = 0; if (index >= MUX_MAX) @@ -271,11 +273,14 @@ static int apple_dpxbar_set_t602x(struct mux_control *mux, int state) dpxbar_set32(dpxbar, T602X_REG_014, 0x4); /* Native T602x DPIN0 teardown restores the read reset at +0x24. */ - dpxbar_set32(dpxbar, T602X_FIFO_RD_PCLK2_EN, atc_bit); + dpxbar_set32(dpxbar, j700_physical ? FIFO_RD_PCLK1_EN : + T602X_FIFO_RD_PCLK2_EN, atc_bit); dpxbar_clear32(dpxbar, T602X_REG_034, atc_bit); - dpxbar_clear32(dpxbar, CROSSBAR_ATC_EN, atc_bit); - dpxbar_clear32(dpxbar, CROSSBAR_DISPEXT_EN, prev_dispext_bit); + if (!j700_physical) { + dpxbar_clear32(dpxbar, CROSSBAR_ATC_EN, atc_bit); + dpxbar_clear32(dpxbar, CROSSBAR_DISPEXT_EN, prev_dispext_bit); + } dpxbar_mask32(dpxbar, T602X_REG_030, mux_mask, 0); dpxbar->selected_dispext[index] = -1; @@ -320,13 +325,15 @@ static int apple_dpxbar_set_t602x(struct mux_control *mux, int state) index == MUX_DPIN1 ? FIFO_RD_PCLK1_EN : T602X_REG_01C, atc_bit); dpxbar_set32(dpxbar, T602X_REG_034, atc_bit); - dpxbar_set32(dpxbar, CROSSBAR_ATC_EN, atc_bit); + if (!j700_physical) + dpxbar_set32(dpxbar, CROSSBAR_ATC_EN, atc_bit); /* * t8103 enables the dispext source at 0x050. T602x left * that register at 0, so dpin0 was selected with no * source clock into the ACIO analog PHY. */ - dpxbar_set32(dpxbar, CROSSBAR_DISPEXT_EN, dispext_bit); + if (!j700_physical) + dpxbar_set32(dpxbar, CROSSBAR_DISPEXT_EN, dispext_bit); dpxbar_set32(dpxbar, T602X_FIFO_RD_UNK_EN, dispext_bit); diff --git a/tools/diagnostics/test-apple-j700-crossbar.py b/tools/diagnostics/test-apple-j700-crossbar.py new file mode 100644 index 0000000000000..0bc7577127196 --- /dev/null +++ b/tools/diagnostics/test-apple-j700-crossbar.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +"""Exercise the actual T602x mux function for J700 and the other outputs.""" +from pathlib import Path +import os +import re +import shlex +import subprocess +import tempfile + +root = Path(__file__).resolve().parents[2] +source = (root / "drivers/mux/apple-display-crossbar.c").read_text() + + +def function(name): + start = source.index("static ", source.index(name) - 30) + opening = source.index("{", start) + depth = 1 + end = opening + 1 + while depth: + depth += (source[end] == "{") - (source[end] == "}") + end += 1 + return source[start:end] + + +prefix = r''' +#include +#include +#include +#include +#include +#include +typedef uint32_t u32; +#define BIT(n) (1U << (n)) +#define GENMASK(h, l) ((~0U << (l)) & (~0U >> (31 - (h)))) +#define FIELD_PREP(mask, value) (((u32)(value) << __builtin_ctz(mask)) & (mask)) +#define MUX_IDLE_DISCONNECT (-2) +#define dev_info(...) ((void)0) +#define dev_dbg(...) ((void)0) +#define spin_lock_irqsave(lock, flags) do { (void)(lock); (flags) = 0; } while (0) +#define spin_unlock_irqrestore(lock, flags) do { (void)(lock); (void)(flags); } while (0) +#define udelay(value) ((void)(value)) +#define t602x_dump(...) ((void)0) +struct apple_dpxbar { int selected_dispext[3], lock; }; +struct mux_chip { struct apple_dpxbar *data; }; +struct mux_control { struct mux_chip *chip; unsigned int index; }; +static struct apple_dpxbar *mux_chip_priv(struct mux_chip *chip) { return chip->data; } +static unsigned int mux_control_get_index(struct mux_control *mux) { return mux->index; } +static bool j700; +static u32 regs[1024], writes[1024]; +static bool of_machine_is_compatible(const char *name) +{ + assert(!strcmp(name, "apple,j700")); + return j700; +} +static void dpxbar_mask32(struct apple_dpxbar *x, u32 reg, u32 mask, u32 value) +{ + (void)x; + assert(reg < sizeof(regs) && !(reg % 4) && !(value & ~mask)); + regs[reg / 4] = (regs[reg / 4] & ~mask) | value; + writes[reg / 4]++; +} +static void dpxbar_set32(struct apple_dpxbar *x, u32 reg, u32 value) +{ + dpxbar_mask32(x, reg, value, value); +} +static void dpxbar_clear32(struct apple_dpxbar *x, u32 reg, u32 value) +{ + dpxbar_mask32(x, reg, value, 0); +} +''' +definitions = "\n".join(re.findall(r"^#define (?:T602X_|CROSSBAR_|ATC_|FIFO_).*$", + source, re.M)) +enum = re.search(r"enum \{ MUX_DPPHY.*?;", source).group() +body = "\n".join(function(name) for name in ( + "t602x_atc_bit(", "t602x_mux_mask(", "t602x_mux_set(", + "apple_dpxbar_set_t602x(")) +suffix = r''' +int main(void) +{ + unsigned int cases = 0; + for (unsigned int machine = 0; machine < 2; machine++) { + for (unsigned int output = 0; output < 3; output++) { + for (int state = 0; state < 9; state++) { + struct apple_dpxbar data = { .selected_dispext = { -1, -1, -1 } }; + struct mux_chip chip = { .data = &data }; + struct mux_control mux = { .chip = &chip, .index = output }; + j700 = machine; + memset(regs, 0xa5, sizeof(regs)); + memset(writes, 0, sizeof(writes)); + assert(!apple_dpxbar_set_t602x(&mux, state)); + assert(data.selected_dispext[output] == state); + assert(!!writes[0x50 / 4] == !(j700 && output == 0)); + assert(!!writes[0x70 / 4] == !(j700 && output == 0)); + memset(writes, 0, sizeof(writes)); + assert(!apple_dpxbar_set_t602x(&mux, MUX_IDLE_DISCONNECT)); + assert(data.selected_dispext[output] == -1); + assert(!!writes[0x50 / 4] == !(j700 && output == 0)); + assert(!!writes[0x70 / 4] == !(j700 && output == 0)); + assert(writes[(j700 && output == 0 ? 0x20 : 0x24) / 4]); + assert(apple_dpxbar_set_t602x(&mux, 9) == -EINVAL); + cases++; + } + } + } + printf("PASS %u actual crossbar select/disconnect scenarios\n", cases); + return 0; +} +''' +with tempfile.TemporaryDirectory(prefix="j700-crossbar-") as work: + work = Path(work) + unit = work / "test.c" + unit.write_text(prefix + definitions + "\n" + enum + "\n" + body + suffix) + command = shlex.split(os.environ.get("CC", "clang")) + # The existing mux implementation compares an int loop index with an unsigned port. + command += ["-std=gnu11", "-Wall", "-Wextra", "-Werror", "-Wno-sign-compare", "-O1", "-g"] + command += ["-fsanitize=address,undefined", str(unit), "-o", str(work / "test")] + subprocess.run(command, check=True) + subprocess.run([str(work / "test")], check=True) From 011471c6d6dbfd7268497e7bb6c65abaf474e51a Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 18:50:55 +1100 Subject: [PATCH 37/52] drm: apple: validate J700 active lanes against PHY capacity Firmware resets the active lane count to zero before setting its link rate. Comparing the following two-lane request against that cached zero rejected a valid activation with EINVAL and prevented link completion. Query the PHY capacity for each request instead. Validate impossible counts before changing state, and propagate validation failures. Cover the zero-to-two reset sequence, unsupported counts and missing PHY. Validation: J700 PHY/training tests, 917 AFK assertions and owned-buffer checks pass under sanitizers; strict checkpatch and copy audit clean. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/dptx-j700-training.h | 16 ++++++++++++++++ drivers/gpu/drm/apple/dptxep.c | 7 +++++-- tools/diagnostics/apple-dptx-training-test.c | 10 ++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/drivers/gpu/drm/apple/dptx-j700-training.h b/drivers/gpu/drm/apple/dptx-j700-training.h index 09a5d1018a7fb..a4632cd363abd 100644 --- a/drivers/gpu/drm/apple/dptx-j700-training.h +++ b/drivers/gpu/drm/apple/dptx-j700-training.h @@ -3,6 +3,22 @@ #ifndef APPLE_DPTX_J700_TRAINING_H #define APPLE_DPTX_J700_TRAINING_H +/* Active lanes may be zero during the firmware's training reset. */ +static int dptx_j700_validate_lanes(struct dptx_port *port, u64 requested) +{ + union phy_configure_opts capacity = {}; + int ret; + + if (!port || !port->atcphy) + return -ENODEV; + if (requested > 4 || requested == 3) + return -EINVAL; + ret = phy_validate(port->atcphy, PHY_MODE_DP, 0, &capacity); + if (ret) + return ret; + return requested <= capacity.dp.lanes ? 0 : -EINVAL; +} + static int dptx_j700_training(struct apple_epic_service *service, bool write, const void *data, size_t size, void *reply, size_t room) { diff --git a/drivers/gpu/drm/apple/dptxep.c b/drivers/gpu/drm/apple/dptxep.c index d2ab05ef9e57a..eaa3a3b26bf61 100644 --- a/drivers/gpu/drm/apple/dptxep.c +++ b/drivers/gpu/drm/apple/dptxep.c @@ -415,8 +415,11 @@ static int dptxport_call_set_active_lane_count(struct apple_epic_service *servic u64 lane_count = le64_to_cpu(request->lane_count); - if (service->ep->wire_v2 && lane_count > dptx->lane_count) - return -EINVAL; + if (service->ep->wire_v2) { + ret = dptx_j700_validate_lanes(dptx, lane_count); + if (ret) + return ret; + } if (dptx->lane_count < lane_count) dev_err(dcp->dev, "set_active_lane_count: unexpected lane " diff --git a/tools/diagnostics/apple-dptx-training-test.c b/tools/diagnostics/apple-dptx-training-test.c index 53ae4419f293d..72d682820363d 100644 --- a/tools/diagnostics/apple-dptx-training-test.c +++ b/tools/diagnostics/apple-dptx-training-test.c @@ -10,6 +10,7 @@ typedef uint8_t u8; typedef uint32_t u32; typedef uint16_t u16; +typedef uint64_t u64; struct phy { int unused; }; struct settings { unsigned int lanes, voltage[4], pre[4]; @@ -26,6 +27,7 @@ struct apple_epic_service { struct dptx_port *cookie; }; static unsigned int calls; static int failure; #define PHY_MODE_DP 1 +#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) static u32 get_unaligned_le32(const void *p) { @@ -123,7 +125,15 @@ int main(void) } } #endif + const unsigned int transitions[] = { 0, 2, 0, 2, 1, 2 }; + + for (unsigned int i = 0; i < ARRAY_SIZE(transitions); i++) + assert(!dptx_j700_validate_lanes(&port, transitions[i])); + assert(dptx_j700_validate_lanes(&port, 3) == -EINVAL); + assert(dptx_j700_validate_lanes(&port, 4) == -EINVAL); + assert(dptx_j700_validate_lanes(&port, UINT64_MAX) == -EINVAL); port.atcphy = NULL; + assert(dptx_j700_validate_lanes(&port, 2) == -ENODEV); assert(dptx_j700_training(&service, true, request, 80, reply, 80) == -ENODEV); printf("DPTX training: %u boundary cases plus failure/cache/readback checks passed\n", cases); From ea06f4a6afef7aa15ec85c772a2da8ac290e4334 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 18:52:49 +1100 Subject: [PATCH 38/52] drm: apple: avoid stale active-lane warnings after V2 validation The V2 request has already passed the physical-capacity check. Restrict the older cached-count warning to the legacy path so a valid zero-to-two transition does not emit an incorrect error. No state transition changes. Validation: J700 host regression tests, strict checkpatch and copy audit. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/dptxep.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/drivers/gpu/drm/apple/dptxep.c b/drivers/gpu/drm/apple/dptxep.c index eaa3a3b26bf61..4935539bd2920 100644 --- a/drivers/gpu/drm/apple/dptxep.c +++ b/drivers/gpu/drm/apple/dptxep.c @@ -419,9 +419,7 @@ static int dptxport_call_set_active_lane_count(struct apple_epic_service *servic ret = dptx_j700_validate_lanes(dptx, lane_count); if (ret) return ret; - } - - if (dptx->lane_count < lane_count) + } else if (dptx->lane_count < lane_count) dev_err(dcp->dev, "set_active_lane_count: unexpected lane " "count:%llu phy: %d\n", lane_count, dptx->lane_count); From 09f9dd589378484133747489f2e8f34a424001db Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 18:57:03 +1100 Subject: [PATCH 39/52] phy: apple: preserve J700 transmitter programming order Keep the separate supply request/override writes and program the link-rate field before the final transmitter output controls. Combining those writes and moving the rate field after output setup did not preserve the audited hardware sequence. The host test now checks the actual production register transaction order, including supply writes. It rejects the previous implementation and passes the corrected sequence for both PMAs and every supported rate/SSC pair. Strict checkpatch and the expanded copy audit pass. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/phy/apple/atc-j700-dp.h | 24 +++++++++++-------- tools/diagnostics/apple-j700-dp-test.c | 33 +++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/drivers/phy/apple/atc-j700-dp.h b/drivers/phy/apple/atc-j700-dp.h index 5ce78d085159d..11d7b32003748 100644 --- a/drivers/phy/apple/atc-j700-dp.h +++ b/drivers/phy/apple/atc-j700-dp.h @@ -71,16 +71,18 @@ static void j700_dp_output_power(struct apple_atcphy *p, unsigned int lane, bool { u32 reg = tx ? 0x10c : 0xc; - core_set32(p, reg, (BIT(4) | BIT(6)) << lane); - udelay(1); - core_set32(p, reg, (BIT(0) | BIT(2)) << lane); - udelay(1); - if (!tx) { - core_set32(p, 0x8, (BIT(10) | BIT(12)) << lane); - udelay(1); - core_set32(p, 0x8, (BIT(6) | BIT(8)) << lane); + for (unsigned int pair = 0; pair < 2; pair++) { + core_set32(p, reg, BIT(4 - 4 * pair + lane)); + core_set32(p, reg, BIT(6 - 4 * pair + lane)); udelay(1); } + if (!tx) { + for (unsigned int pair = 0; pair < 2; pair++) { + core_set32(p, 0x8, BIT(10 - 4 * pair + lane)); + core_set32(p, 0x8, BIT(12 - 4 * pair + lane)); + udelay(1); + } + } reg = tx ? 0x10c : 0x8; core_clear32(p, reg, BIT(tx ? 8 : 14) << lane); core_set32(p, reg, BIT(tx ? 10 : 16) << lane); @@ -115,7 +117,8 @@ static void j700_dp_transmitter(struct apple_atcphy *p, unsigned int lane, int r { 0x44, 0x400000, 0x400000, 0 }, }; static const struct j700_dp_step finish[] = { - { 0x04, 0x33000000, 0x32000000, 0 }, + { 0x04, 0x03000000, 0x02000000, 0 }, + { 0x04, 0x30000000, 0x30000000, 0 }, { 0x10, 0x600, 0x600, 0 }, { 0x1c, 0xc00000, 0x800000, 0 }, }; @@ -126,9 +129,10 @@ static void j700_dp_transmitter(struct apple_atcphy *p, unsigned int lane, int r j700_dp_equalize(p, lane, true, 0); if (rate != ATCPHY_DP_LINK_RATE_HBR3) j700_dp_steps(p, base, divide, ARRAY_SIZE(divide)); - j700_dp_steps(p, base, finish, ARRAY_SIZE(finish)); + j700_dp_steps(p, base, finish, 2); core_mask32(p, base + 0xc, 0x7000000, rate == ATCPHY_DP_LINK_RATE_RBR ? 0x5000000 : 0x4000000); + j700_dp_steps(p, base, finish + 2, ARRAY_SIZE(finish) - 2); } static void j700_dp_receiver(struct apple_atcphy *p, unsigned int lane, int rate) diff --git a/tools/diagnostics/apple-j700-dp-test.c b/tools/diagnostics/apple-j700-dp-test.c index 0b3e24067ac05..d8e565491f96e 100644 --- a/tools/diagnostics/apple-j700-dp-test.c +++ b/tools/diagnostics/apple-j700-dp-test.c @@ -43,6 +43,7 @@ struct apple_atcphy { static u32 registers[0x20000 / 4]; static unsigned int writes, polls, fail_poll, pin_mask = 2; +static u32 transactions[2048], transaction_masks[2048]; static bool fail_all; static void core_mask32(struct apple_atcphy *p, u32 off, u32 mask, u32 value) @@ -51,7 +52,9 @@ static void core_mask32(struct apple_atcphy *p, u32 off, u32 mask, u32 value) assert(!(off % 4) && off / 4 < ARRAY_SIZE(registers)); assert(!(value & ~mask)); registers[off / 4] = (registers[off / 4] & ~mask) | value; - writes++; + assert(writes < ARRAY_SIZE(transactions)); + transaction_masks[writes] = mask; + transactions[writes++] = off; if (off == 0x2000) { if (value == 0x10000001) { registers[off / 4] |= BIT(1); @@ -131,6 +134,33 @@ static struct apple_atcphy device(void) return p; } +static void check_transmitter_order(unsigned int pmas) +{ + for (unsigned int lane = 0; lane < 2; lane++) { + u32 base = 0xd000 + lane * 0x7000; + bool found_rate = false, found_power = false; + + if (!(pmas & BIT(lane))) + continue; + for (unsigned int t = 0; t + 4 < writes; t++) { + if (transactions[t] == base + 4 && transactions[t + 1] == base + 4) { + assert(transactions[t + 2] == base + 0xc); + assert(transactions[t + 3] == base + 0x10); + assert(transactions[t + 4] == base + 0x1c); + found_rate = true; + } + if (transactions[t] == 0x10c && transaction_masks[t] == BIT(4 + lane)) { + assert(transactions[t + 1] == 0x10c); + assert(transaction_masks[t + 1] == BIT(6 + lane)); + assert(transaction_masks[t + 2] == BIT(lane)); + assert(transaction_masks[t + 3] == BIT(2 + lane)); + found_power = true; + } + } + assert(found_rate && found_power); + } +} + int main(void) { const unsigned int rates[] = { 1620, 2700, 5400, 8100 }; @@ -147,6 +177,7 @@ int main(void) assert(!j700_dp_configure(&p, &o)); assert(p.dp_owned_pmas == pin_mask && p.dp_link_rate == (int)rate); + check_transmitter_order(pin_mask); if (pin_mask != 3) { u32 other = pin_mask == 1 ? 0xf0 : 0xf; From b96ac1e25ab65e229dc817546b9a3f8b79502259 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 18:57:03 +1100 Subject: [PATCH 40/52] drm: apple: answer the V2 physical-device mode query The external firmware asks whether the connection is a virtual display. Return a bounded, zero-initialized physical-device answer instead of unsupported, matching the public m1n1 V2 DPTX protocol. Keep other unknown commands rejected and retain the callback group restrictions. Validation: query size/group checks, J700 training tests, 54 crossbar scenarios, 917 codec assertions and owned-buffer tests; checkpatch and copy audit pass. Hardware qualification remains pending. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/dptxep.c | 6 ++++++ drivers/gpu/drm/apple/dptxep.h | 1 + tools/diagnostics/apple-dptx-training-test.c | 11 +++++++++-- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/drivers/gpu/drm/apple/dptxep.c b/drivers/gpu/drm/apple/dptxep.c index 4935539bd2920..bae72b7dc7dfb 100644 --- a/drivers/gpu/drm/apple/dptxep.c +++ b/drivers/gpu/drm/apple/dptxep.c @@ -876,6 +876,12 @@ static int dptxport_group_call(struct apple_epic_service *service, u16 group, u3 return -EOPNOTSUPP; if (command == DPTX_APCALL_SET_DRIVE_SETTINGS) return -EOPNOTSUPP; + if (command == DPTX_APCALL_GET_VIRTUAL_DEVICE_MODE) { + if (room < sizeof(struct dptxport_apcall_get_support)) + return -EINVAL; + memset(reply, 0, room); + return 0; + } if (command == DPTX_APCALL_GET_DRIVE_SETTINGS) return dptx_j700_training(service, false, data, size, reply, room); return dptxport_call(service, command, data, size, reply, room); diff --git a/drivers/gpu/drm/apple/dptxep.h b/drivers/gpu/drm/apple/dptxep.h index 3eb71cf00f9ba..14530c99567b1 100644 --- a/drivers/gpu/drm/apple/dptxep.h +++ b/drivers/gpu/drm/apple/dptxep.h @@ -30,6 +30,7 @@ enum dptx_apcall { DPTX_APCALL_DEVICE_NOT_RESPONDING = 22, DPTX_APCALL_DEVICE_BUSY_TIMEOUT = 23, DPTX_APCALL_DEVICE_NOT_STARTED = 24, + DPTX_APCALL_GET_VIRTUAL_DEVICE_MODE = 26, }; #define DCPDPTX_REMOTE_PORT_CORE GENMASK(3, 0) diff --git a/tools/diagnostics/apple-dptx-training-test.c b/tools/diagnostics/apple-dptx-training-test.c index 72d682820363d..cb1cea55a6452 100644 --- a/tools/diagnostics/apple-dptx-training-test.c +++ b/tools/diagnostics/apple-dptx-training-test.c @@ -61,6 +61,8 @@ static int phy_configure(struct phy *p, union phy_configure_opts *o) #ifdef DPTX_GROUP_TEST #define DPTX_APCALL_SET_DRIVE_SETTINGS 3 #define DPTX_APCALL_GET_DRIVE_SETTINGS 4 +#define DPTX_APCALL_GET_VIRTUAL_DEVICE_MODE 26 +struct dptxport_apcall_get_support { u8 bytes[32]; }; static int dptxport_call(struct apple_epic_service *service, u32 command, const void *data, size_t size, void *reply, size_t room) { @@ -113,17 +115,22 @@ int main(void) #ifdef DPTX_GROUP_TEST for (unsigned int group = 0; group < 4; group++) { - for (unsigned int command = 0; command < 25; command++) { + for (unsigned int command = 0; command < 27; command++) { int expected = group ? -EOPNOTSUPP : 7; if (group == 0 && command == 3) expected = -EOPNOTSUPP; - if ((group == 1 && command == 3) || (group == 0 && command == 4)) + if ((group == 1 && command == 3) || + (group == 0 && (command == 4 || command == 26))) expected = 0; assert(dptxport_group_call(&service, group, command, request, 80, reply, 80) == expected); } } + assert(dptxport_group_call(&service, 0, 26, request, 80, reply, 31) == -EINVAL); + memset(reply, 0xff, 80); + assert(!dptxport_group_call(&service, 0, 26, request, 80, reply, 80)); + assert(!get_unaligned_le32(reply + 16)); #endif const unsigned int transitions[] = { 0, 2, 0, 2, 1, 2 }; From 29ebc07b7e66e05462dcccf9b6771c864655f7f8 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 19:02:26 +1100 Subject: [PATCH 41/52] phy: apple: apply J700 DP calibration before common power-up Move the firmware DP calibration bundle into the tunable-application phase, before the common supply/clamp sequence. Applying it later from AUX enable preserved the final register values but not the required initialization order. Full DP uses its own common bundle; shared USB/DP keeps the USB lane calibration and then applies the DP bundle. The phase ordering follows the authorized hardware audit. Existing public tunable operations are reused; no private implementation is imported. Actual production-dispatch tests cover full/shared/reversed DP, the legacy path and fixed USB2. Existing PHY, codec and crossbar tests pass. Strict checkpatch passes; only generic test scaffolding matched in the reviewed copy audit. Hardware qualification remains pending. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- MAINTAINERS | 1 + drivers/phy/apple/atc.c | 19 +++- tools/diagnostics/test-apple-j700-tuning.py | 103 ++++++++++++++++++++ 3 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 tools/diagnostics/test-apple-j700-tuning.py diff --git a/MAINTAINERS b/MAINTAINERS index c157347692c6f..e68f9960b6e67 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -2020,6 +2020,7 @@ F: tools/diagnostics/apple-dptx-training-test.c F: tools/diagnostics/apple-j700-dp-test.c F: tools/diagnostics/test-apple-j700-crossbar.py F: tools/diagnostics/test-apple-j700-dp.py +F: tools/diagnostics/test-apple-j700-tuning.py F: tools/testing/apple/ APPLETALK NETWORK LAYER diff --git a/drivers/phy/apple/atc.c b/drivers/phy/apple/atc.c index 0e2d74959b41c..2ec021c2e2c79 100644 --- a/drivers/phy/apple/atc.c +++ b/drivers/phy/apple/atc.c @@ -1007,6 +1007,14 @@ static const struct atcphy_mode_configuration *atcphy_get_mode_config(struct app return &atcphy_modes[mode].normal; } +static void j700_dp_tunables(struct apple_atcphy *atcphy) +{ + apple_tunable_apply(atcphy->regs.axi2af, atcphy->tunables.axi2af); + apple_tunable_apply(atcphy->regs.core, atcphy->tunables.dp_common_pre); + apple_tunable_apply(atcphy->regs.lpdptx, atcphy->tunables.dp_lpdptx); + apple_tunable_apply(atcphy->regs.core, atcphy->tunables.dp_common_post); +} + static void atcphy_apply_tunables(struct apple_atcphy *atcphy, enum atcphy_mode mode) { const int lane0 = atcphy->swap_lanes ? 1 : 0; @@ -1021,6 +1029,11 @@ static void atcphy_apply_tunables(struct apple_atcphy *atcphy, enum atcphy_mode if (mode == APPLE_ATCPHY_MODE_USB2 && atcphy->fixed_usb2) return; + if (atcphy->hw->has_t8140_dp && mode == APPLE_ATCPHY_MODE_DP) { + j700_dp_tunables(atcphy); + return; + } + apple_tunable_apply(atcphy->regs.core, atcphy->tunables.common[0]); apple_tunable_apply(atcphy->regs.axi2af, atcphy->tunables.axi2af); apple_tunable_apply(atcphy->regs.core, atcphy->tunables.common[1]); @@ -1057,6 +1070,9 @@ static void atcphy_apply_tunables(struct apple_atcphy *atcphy, enum atcphy_mode case APPLE_ATCPHY_MODE_USB2: break; } + if (atcphy->hw->has_t8140_dp && mode == APPLE_ATCPHY_MODE_USB3_DP) + j700_dp_tunables(atcphy); + } static int atcphy_pipehandler_lock(struct apple_atcphy *atcphy) @@ -1612,9 +1628,6 @@ static int j700_aux_disable(struct apple_atcphy *atcphy) static int atcphy_enable_dp_aux(struct apple_atcphy *atcphy) { if (atcphy->hw->has_t8140_dp) { - apple_tunable_apply(atcphy->regs.core, atcphy->tunables.dp_common_pre); - apple_tunable_apply(atcphy->regs.lpdptx, atcphy->tunables.dp_lpdptx); - apple_tunable_apply(atcphy->regs.core, atcphy->tunables.dp_common_post); dev_info_once(atcphy->dev, "Applied T8140 DisplayPort setup lists\n"); return j700_aux_enable(atcphy); } diff --git a/tools/diagnostics/test-apple-j700-tuning.py b/tools/diagnostics/test-apple-j700-tuning.py new file mode 100644 index 0000000000000..b6dc336eb71f6 --- /dev/null +++ b/tools/diagnostics/test-apple-j700-tuning.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +"""Check the production calibration dispatch before common-power setup.""" +from pathlib import Path +import os +import shlex +import subprocess +import tempfile + +root = Path(__file__).resolve().parents[2] +source = (root / "drivers/phy/apple/atc.c").read_text() + + +def function(name): + start = source.index("static void " + name + "(") + brace = source.index("{", start) + end, depth = brace + 1, 1 + while depth: + depth += (source[end] == "{") - (source[end] == "}") + end += 1 + return source[start:end] + + +prefix = r''' +#include +#include +#include +#include +#include +enum atcphy_mode { + APPLE_ATCPHY_MODE_OFF, APPLE_ATCPHY_MODE_USB2, APPLE_ATCPHY_MODE_USB3, + APPLE_ATCPHY_MODE_USB3_DP, APPLE_ATCPHY_MODE_DP, + APPLE_ATCPHY_MODE_TBT, APPLE_ATCPHY_MODE_USB4, +}; +struct apple_atcphy { + bool swap_lanes, fixed_usb2; + struct { bool has_t8140_dp; } *hw; + struct { void *core, *axi2af, *lpdptx; } regs; + struct { + int *common[2], *axi2af, *lane_usb3[2], *lane_dp[2], *lane_usb4[2]; + int *dp_common_pre, *dp_lpdptx, *dp_common_post; + } tunables; +}; +static unsigned int count; +static int events[20]; +static void apple_tunable_apply(void *base, int *table) +{ + (void)base; + if (table) + events[count++] = *table; +} +''' +suffix = r''' +static void check(struct apple_atcphy *p, enum atcphy_mode mode, + const int *expected, unsigned int size) +{ + count = 0; + atcphy_apply_tunables(p, mode); + assert(count == size); + if (size) + assert(!memcmp(events, expected, sizeof(int) * size)); +} +int main(void) +{ + int ids[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 }; + struct apple_atcphy p = {}; + typeof(*p.hw) hw = { .has_t8140_dp = true }; + p.hw = &hw; + p.tunables.common[0] = &ids[0]; p.tunables.common[1] = &ids[1]; + p.tunables.axi2af = &ids[2]; + p.tunables.lane_usb3[0] = &ids[3]; p.tunables.lane_usb3[1] = &ids[4]; + p.tunables.lane_dp[0] = &ids[5]; p.tunables.lane_dp[1] = &ids[6]; + p.tunables.dp_common_pre = &ids[7]; p.tunables.dp_lpdptx = &ids[8]; + p.tunables.dp_common_post = &ids[9]; + p.tunables.lane_usb4[0] = &ids[10]; p.tunables.lane_usb4[1] = &ids[11]; + const int dp[] = { 3, 8, 9, 10 }; + const int shared[] = { 1, 3, 2, 4, 7, 3, 8, 9, 10 }; + const int reversed[] = { 1, 3, 2, 5, 6, 3, 8, 9, 10 }; + const int legacy[] = { 1, 3, 2, 6, 7 }; + check(&p, APPLE_ATCPHY_MODE_DP, dp, 4); + check(&p, APPLE_ATCPHY_MODE_USB3_DP, shared, 9); + p.swap_lanes = true; + check(&p, APPLE_ATCPHY_MODE_USB3_DP, reversed, 9); + p.swap_lanes = false; + hw.has_t8140_dp = false; + check(&p, APPLE_ATCPHY_MODE_DP, legacy, 5); + p.fixed_usb2 = true; + check(&p, APPLE_ATCPHY_MODE_USB2, NULL, 0); + puts("PASS calibration order: full DP, shared, reverse, legacy and fixed USB2"); + return 0; +} +''' +with tempfile.TemporaryDirectory(prefix="j700-tuning-") as work: + work = Path(work) + test = work / "test.c" + test.write_text(prefix + function("j700_dp_tunables") + + function("atcphy_apply_tunables") + suffix) + command = shlex.split(os.environ.get("CC", "clang")) + command += ["-std=gnu11", "-O1", "-g", "-Wall", "-Wextra", "-Werror"] + command += ["-fsanitize=address,undefined", str(test), "-o", str(work / "test")] + subprocess.run(command, check=True) + subprocess.run([str(work / "test")], check=True, + env={**os.environ, "UBSAN_OPTIONS": "halt_on_error=1"}) From dc41df1a19d970134fa28ff0ee4f8ef3fc994c42 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 19:12:35 +1100 Subject: [PATCH 42/52] drm: apple: allow the qualified J700 provider fallback on DCPEXT Missing power-LUT properties on the external controller took the provider failure path and set dcp->crashed before display modes could be published. The existing J700 compatibility payloads were accepted only on the internal controller. Allow those same validated payloads for the explicitly experimental J700 external profile. Keep all request, capacity and property-length checks, and keep malformed properties and other machines/profiles fail-closed. The five existing public payloads match the audited working handoff; no new calibration data is added. Validation: actual provider-callback host tests cover the six known properties, internal compatibility, experimental external compatibility, other profiles/machines, malformed lengths and unknown requests under ASan/UBSan. The old implementation rejects the external regression case. Strict checkpatch and copy audit pass; hardware qualification is pending. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- MAINTAINERS | 1 + drivers/gpu/drm/apple/iomfb_h17p.c | 4 +- tools/diagnostics/test-apple-j700-provider.py | 123 ++++++++++++++++++ 3 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 tools/diagnostics/test-apple-j700-provider.py diff --git a/MAINTAINERS b/MAINTAINERS index e68f9960b6e67..0ceb4b28e0fef 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -2020,6 +2020,7 @@ F: tools/diagnostics/apple-dptx-training-test.c F: tools/diagnostics/apple-j700-dp-test.c F: tools/diagnostics/test-apple-j700-crossbar.py F: tools/diagnostics/test-apple-j700-dp.py +F: tools/diagnostics/test-apple-j700-provider.py F: tools/diagnostics/test-apple-j700-tuning.py F: tools/testing/apple/ diff --git a/drivers/gpu/drm/apple/iomfb_h17p.c b/drivers/gpu/drm/apple/iomfb_h17p.c index 682a92879f8ef..49fcabaeb7c66 100644 --- a/drivers/gpu/drm/apple/iomfb_h17p.c +++ b/drivers/gpu/drm/apple/iomfb_h17p.c @@ -445,7 +445,9 @@ static bool trampoline_provider_property_h17p(struct apple_dcp *dcp, int tag, goto fail; } else if (!of_property_present(node, property->dt_name) && of_machine_is_compatible("apple,j700") && - of_device_is_compatible(node, "apple,t8140-dcp")) { + (of_device_is_compatible(node, "apple,t8140-dcp") || + (dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL && + of_device_is_compatible(node, "apple,t8140-dcpext")))) { /* Qualified older loaders lack the newly described table properties. */ memset(reply, 0, sizeof(*reply)); memcpy(reply->data, property->legacy_data, property->length); diff --git a/tools/diagnostics/test-apple-j700-provider.py b/tools/diagnostics/test-apple-j700-provider.py new file mode 100644 index 0000000000000..9872c6d55be3b --- /dev/null +++ b/tools/diagnostics/test-apple-j700-provider.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +"""Run the actual provider callback against missing and malformed properties.""" +from pathlib import Path +import os +import shlex +import subprocess +import tempfile + +root = Path(__file__).resolve().parents[2] +source = (root / "drivers/gpu/drm/apple/iomfb_h17p.c").read_text() +body = source[source.index("struct dcp_h17p_provider_request {"): + source.index("static bool trampoline_luma_span_h17p")] +prefix = r''' +#include +#include +#include +#include +#include +#include +#include +typedef uint8_t u8; +typedef uint32_t u32; +typedef uint32_t __le32; +#define __packed __attribute__((packed)) +#undef static_assert +#define static_assert(cond) _Static_assert(cond, "") +#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) +#define sizeof_field(t, f) sizeof(((t *)0)->f) +#define le32_to_cpu(x) (x) +#define cpu_to_le32(x) (x) +#define WRITE_ONCE(x, v) ((x) = (v)) +#define DCP_IOMFB_METHODS_H17G 1 +#define DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL 2 +#define trace_iomfb_callback(d, t, n) ((void)(d), (void)(t)) +#define dev_err(...) ((void)0) +#define dev_warn_once(...) ((void)0) +struct device_node { bool internal, external, present; int length; }; +struct device { struct device_node *of_node; }; +struct apple_dcp { struct { int iomfb_method_profile; } hw; struct device *dev; bool crashed; }; +static bool j700 = true; +static bool of_machine_is_compatible(const char *name) +{ + assert(!strcmp(name, "apple,j700")); + return j700; +} +static bool of_device_is_compatible(struct device_node *n, const char *name) +{ + return (!strcmp(name, "apple,t8140-dcp") && n->internal) || + (!strcmp(name, "apple,t8140-dcpext") && n->external); +} +static bool of_property_present(struct device_node *n, const char *name) +{ + (void)name; + return n->present; +} +static int of_property_count_u8_elems(struct device_node *n, const char *name) +{ + (void)name; + return n->present ? n->length : -EINVAL; +} +static int of_property_read_u8_array(struct device_node *n, const char *name, void *p, unsigned int len) +{ + (void)n; (void)name; + memset(p, 0x5a, len); + return 0; +} +static const void *memchr_inv(const void *p, int value, size_t size) +{ + const u8 *b = p; + for (size_t i = 0; i < size; i++) + if (b[i] != value) + return b + i; + return NULL; +} +''' +suffix = r''' +int main(void) +{ + struct device_node node = { .external = true }; + struct device dev = { .of_node = &node }; + struct apple_dcp dcp = { .dev = &dev, .hw.iomfb_method_profile = 2 }; + struct dcp_h17p_provider_request req = { .scope = { 'V', 'O', 'R', 'P' }, .capacity = 0xc00 }; + struct dcp_h17p_provider_reply reply; + unsigned int cases = 0; + for (unsigned int i = 0; i < ARRAY_SIZE(provider_properties); i++) { + memset(&reply, 0xff, sizeof(reply)); + strcpy(req.name, provider_properties[i].name); + assert(trampoline_provider_property_h17p(&dcp, 400, &reply, &req)); + assert(!dcp.crashed && reply.length == provider_properties[i].length); + if (reply.length) + assert(!memcmp(reply.data, provider_properties[i].legacy_data, reply.length)); + cases++; + } + strcpy(req.name, "power-lut-data-x"); + j700 = false; + assert(!trampoline_provider_property_h17p(&dcp, 400, &reply, &req) && dcp.crashed); + j700 = true; dcp.crashed = false; dcp.hw.iomfb_method_profile = 3; + assert(!trampoline_provider_property_h17p(&dcp, 400, &reply, &req) && dcp.crashed); + node.external = false; node.internal = true; dcp.crashed = false; + assert(trampoline_provider_property_h17p(&dcp, 400, &reply, &req) && !dcp.crashed); + node.internal = false; node.external = true; node.present = true; node.length = 3; + dcp.hw.iomfb_method_profile = 2; dcp.crashed = false; + assert(!trampoline_provider_property_h17p(&dcp, 400, &reply, &req) && dcp.crashed); + node.length = 4; dcp.crashed = false; + assert(trampoline_provider_property_h17p(&dcp, 400, &reply, &req) && !dcp.crashed); + assert(reply.length == 4 && reply.data[0] == 0x5a); + strcpy(req.name, "unqualified-property"); + assert(!trampoline_provider_property_h17p(&dcp, 400, &reply, &req)); + printf("PASS %u provider payloads and scope/length rejection cases\n", cases); + return 0; +} +''' +with tempfile.TemporaryDirectory(prefix="j700-provider-") as work: + work = Path(work) + test = work / "test.c" + test.write_text(prefix + body + suffix) + command = shlex.split(os.environ.get("CC", "clang")) + command += ["-std=gnu11", "-O1", "-g", "-Wall", "-Wextra", "-Werror"] + command += ["-fsanitize=address,undefined", str(test), "-o", str(work / "test")] + subprocess.run(command, check=True) + subprocess.run([str(work / "test")], check=True, + env={**os.environ, "UBSAN_OPTIONS": "halt_on_error=1"}) From e8fe156f2d3dcc0058228abc0ebb9f7c1c4e3300 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 19:19:22 +1100 Subject: [PATCH 43/52] drm/apple: add scoped J700 link-rate diagnostic ceiling Preserve HBR3 by default and on other outputs. Permit known lower DP rates only for the experimental J700 physical output so training failures can be isolated without changing PHY tables. Exercise the actual callback over all ceilings and scope combinations. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/dptxep.c | 19 ++++- .../diagnostics/test-apple-j700-link-rate.py | 70 +++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 tools/diagnostics/test-apple-j700-link-rate.py diff --git a/drivers/gpu/drm/apple/dptxep.c b/drivers/gpu/drm/apple/dptxep.c index bae72b7dc7dfb..964f2896b6373 100644 --- a/drivers/gpu/drm/apple/dptxep.c +++ b/drivers/gpu/drm/apple/dptxep.c @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -20,6 +21,12 @@ #include "dptx-j700-training.h" +/* Diagnostic ceiling for the experimental physical J700 output only. */ +static unsigned int j700_dp_max_link_rate = LINK_RATE_HBR3; +module_param(j700_dp_max_link_rate, uint, 0600); +MODULE_PARM_DESC(j700_dp_max_link_rate, + "J700 external DP ceiling: 6=RBR, 10=HBR, 20=HBR2, 30=HBR3"); + struct dcpdptx_connection_cmd { __le32 unk; __le32 target; @@ -343,12 +350,22 @@ static int dptxport_call_get_max_link_rate(struct apple_epic_service *service, void *reply_, size_t reply_size) { struct dptxport_apcall_link_rate *reply = reply_; + struct apple_dcp *dcp = service->ep->dcp; + u32 ceiling = READ_ONCE(j700_dp_max_link_rate); if (reply_size < sizeof(*reply)) return -EINVAL; reply->retcode = cpu_to_le32(0); - reply->link_rate = cpu_to_le32(LINK_RATE_HBR3); + if (service->cookie && ((struct dptx_port *)service->cookie)->atcphy && + dcp->hw.iomfb_method_profile == + DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL && + of_machine_is_compatible("apple,j700") && + (ceiling == LINK_RATE_RBR || ceiling == LINK_RATE_HBR || + ceiling == LINK_RATE_HBR2 || ceiling == LINK_RATE_HBR3)) + reply->link_rate = cpu_to_le32(ceiling); + else + reply->link_rate = cpu_to_le32(LINK_RATE_HBR3); return 0; } diff --git a/tools/diagnostics/test-apple-j700-link-rate.py b/tools/diagnostics/test-apple-j700-link-rate.py new file mode 100644 index 0000000000000..7bef0cc936918 --- /dev/null +++ b/tools/diagnostics/test-apple-j700-link-rate.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +"""Exercise the production link ceiling callback and its isolation boundaries.""" +from pathlib import Path +import subprocess +import tempfile + +root = Path(__file__).resolve().parents[2] +source = (root / "drivers/gpu/drm/apple/dptxep.c").read_text() +start = source.index("static int dptxport_call_get_max_link_rate(") +end = source.index("\n}\n", start) + 3 +stub = r""" +#include +#include +#include +#include +#include +typedef uint32_t u32; +#define LINK_RATE_RBR 6 +#define LINK_RATE_HBR 10 +#define LINK_RATE_HBR2 20 +#define LINK_RATE_HBR3 30 +#define DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL 7 +#define READ_ONCE(x) (x) +#define cpu_to_le32(x) (x) +struct apple_dcp { struct { int iomfb_method_profile; } hw; }; +struct apple_epic_endpoint { struct apple_dcp *dcp; }; +struct apple_epic_service { struct apple_epic_endpoint *ep; void *cookie; }; +struct dptx_port { void *atcphy; }; +struct dptxport_apcall_link_rate { u32 retcode; char pad[12]; u32 link_rate; char tail[12]; }; +static u32 j700_dp_max_link_rate; +static bool j700; +static bool of_machine_is_compatible(const char *name) { (void)name; return j700; } +""" +main = r""" +int main(void) { + struct apple_dcp dcp = {}; + struct apple_epic_endpoint ep = { &dcp }; + struct dptx_port port = {}; + struct apple_epic_service service = { &ep, &port }; + struct dptxport_apcall_link_rate reply; + unsigned cases = 0; + for (unsigned rate = 0; rate <= 40; rate++) + for (unsigned scope = 0; scope < 8; scope++) { + j700 = scope & 1; + port.atcphy = (scope & 2) ? &port : NULL; + dcp.hw.iomfb_method_profile = (scope & 4) ? 7 : 0; + j700_dp_max_link_rate = rate; + assert(dptxport_call_get_max_link_rate(&service, &reply, sizeof(reply)) == 0); + bool valid = rate == 6 || rate == 10 || rate == 20 || rate == 30; + assert(reply.link_rate == ((scope == 7 && valid) ? rate : 30)); + assert(reply.retcode == 0); + cases++; + } + service.cookie = NULL; + assert(!dptxport_call_get_max_link_rate(&service, &reply, sizeof(reply))); + assert(reply.link_rate == 30); + for (unsigned size = 0; size < sizeof(reply); size++) + assert(dptxport_call_get_max_link_rate(&service, &reply, size) == -EINVAL); + return cases != 328; +} +""" +with tempfile.TemporaryDirectory(prefix="j700-rate-") as tmp: + c = Path(tmp) / "test.c" + exe = Path(tmp) / "test" + c.write_text(stub + source[start:end] + main) + subprocess.run(["clang", "-Wall", "-Wextra", "-Werror", + "-fsanitize=address,undefined", str(c), "-o", str(exe)], check=True) + subprocess.run([str(exe)], check=True) +print("328 link ceilings and scope cases, null port and all short replies: PASS") From f7ced60fe8b08c9b45c439c5c3b6abff25bcbe62 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 19:20:51 +1100 Subject: [PATCH 44/52] drm/apple: report unknown callback bounds for diagnosis Include validated input/output lengths and channel context in the existing warning. Retain the external fail-closed behavior while identifying the D134 callback observed after link setup. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/iomfb.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index 1950599effd74..5bd735f9299c4 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -818,8 +818,9 @@ static void dcpep_handle_cb(struct apple_dcp *dcp, enum dcp_context_id context, if (!handled) { if (!iomfb_uses_queue(dcp) || !test_and_set_bit(tag, dcp->unknown_callbacks)) - dev_warn(dev, "received unknown callback %c%c%c%c\n", - hdr->tag[3], hdr->tag[2], hdr->tag[1], hdr->tag[0]); + dev_warn(dev, "received unknown callback %c%c%c%c (%u, %u) context=%u\n", + hdr->tag[3], hdr->tag[2], hdr->tag[1], hdr->tag[0], + hdr->in_len, hdr->out_len, context); if (dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) { /* Retain the failed session without inventing a successful reply. */ From db547bff7d79d0df818afac3d668c10280e78145 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 19:25:59 +1100 Subject: [PATCH 45/52] drm/apple: answer the measured J700 external D134 record The external controller reaches video-interface publication, then sends D134 on the async channel with no input and a four-byte output. Its unanswered callback marks the strict external session crashed. Permit the existing zero reply only for that exact record on J700 T8140 DCPEXT. Retain fail-closed behavior for every other unknown external callback and preserve legacy handling. Test the production dispatcher under ASan/UBSan across 15552 tag, size, context and device/profile combinations, including output bounds and unchanged legacy replies. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/iomfb.c | 28 +++++- tools/diagnostics/test-apple-j700-d134.py | 111 ++++++++++++++++++++++ 2 files changed, 135 insertions(+), 4 deletions(-) create mode 100644 tools/diagnostics/test-apple-j700-d134.py diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index 5bd735f9299c4..5fe53684795fc 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -770,6 +770,23 @@ void dcp_hotplug(struct work_struct *work) drm_kms_helper_connector_hotplug_event(&connector->base); } +/* + * J700/25G83 asks D134 after publishing its external video interface. + * The observed async record has no input and a four-byte result. Use the + * existing zero reply for this record only; its feature remains unsupported. + */ +static bool dcp_external_zero_reply(struct apple_dcp *dcp, int tag, + enum dcp_context_id context, + u32 in_len, u32 out_len) +{ + return tag == 134 && context == DCP_CONTEXT_ASYNC && + in_len == 0 && out_len == sizeof(u32) && + dcp->hw.iomfb_method_profile == + DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL && + of_machine_is_compatible("apple,j700") && + of_device_is_compatible(dcp->dev->of_node, "apple,t8140-dcpext"); +} + static void dcpep_handle_cb(struct apple_dcp *dcp, enum dcp_context_id context, void *data, u32 length, u16 offset) { @@ -779,7 +796,7 @@ static void dcpep_handle_cb(struct apple_dcp *dcp, enum dcp_context_id context, int tag; struct dcp_channel *ch = dcp_get_channel(dcp, context); u8 depth; - bool handled; + bool handled, zero_reply; if (dcp->fw_compat == DCP_FIRMWARE_H17P && (length < sizeof(*hdr) || !ch || ch->depth >= DCP_MAX_CALL_DEPTH || @@ -816,13 +833,16 @@ static void dcpep_handle_cb(struct apple_dcp *dcp, enum dcp_context_id context, out = in + hdr->in_len; if (!handled) { - if (!iomfb_uses_queue(dcp) || - !test_and_set_bit(tag, dcp->unknown_callbacks)) + zero_reply = dcp_external_zero_reply(dcp, tag, context, + hdr->in_len, hdr->out_len); + if (!zero_reply && + (!iomfb_uses_queue(dcp) || + !test_and_set_bit(tag, dcp->unknown_callbacks))) dev_warn(dev, "received unknown callback %c%c%c%c (%u, %u) context=%u\n", hdr->tag[3], hdr->tag[2], hdr->tag[1], hdr->tag[0], hdr->in_len, hdr->out_len, context); if (dcp->hw.iomfb_method_profile == - DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) { + DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL && !zero_reply) { /* Retain the failed session without inventing a successful reply. */ dcp->crashed = true; complete(&dcp->start_done); diff --git a/tools/diagnostics/test-apple-j700-d134.py b/tools/diagnostics/test-apple-j700-d134.py new file mode 100644 index 0000000000000..d29debcd45634 --- /dev/null +++ b/tools/diagnostics/test-apple-j700-d134.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +"""Compile the production dispatcher and check the sole D134 reply exception.""" +from pathlib import Path +import subprocess +import tempfile + +root = Path(__file__).resolve().parents[2] +source = (root / "drivers/gpu/drm/apple/iomfb.c").read_text() +start = source.index("static bool dcp_external_zero_reply(") +end = source.index("static void dcpep_handle_ack(", start) +prefix = r""" +#include +#include +#include +#include +#include +typedef uint8_t u8; +typedef uint16_t u16; +typedef uint32_t u32; +typedef uint64_t u64; +#define DCP_FIRMWARE_H17P 1 +#define DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL 2 +#define DCP_IOMFB_METHODS_H17G 3 +#define DCP_MAX_CALL_DEPTH 8 +#define IOMFB_MAX_CB 600 +#define DCP_PACKET_ALIGNMENT 64 +#define ALIGN(n, a) (((n) + (a) - 1) & ~((a) - 1)) +#define dev_err(...) ((void)dev) +#define dev_info(...) ((void)dev) +#define dev_warn(...) ((void)dev) +enum dcp_context_id { DCP_CONTEXT_CB = 0, DCP_CONTEXT_ASYNC = 3 }; +struct device_node { bool external; }; +struct device { struct device_node *of_node; }; +struct dcp_packet_header { char tag[4]; u32 in_len, out_len; }; +struct dcp_channel { unsigned depth; void *output[8]; unsigned end[8]; }; +struct apple_dcp { + struct { int iomfb_method_profile; } hw; + struct device *dev; + int fw_compat; + unsigned experimental_callback_logs; + bool crashed; + bool (**cb_handlers)(struct apple_dcp *, int, void *, void *); + unsigned long unknown_callbacks[16]; + int start_done; + struct dcp_channel channel; +}; +static bool machine; +static unsigned acks, case_tag; +static struct dcp_channel *dcp_get_channel(struct apple_dcp *d, enum dcp_context_id c) +{ (void)c; return &d->channel; } +static int dcp_parse_tag(char *p) { (void)p; return case_tag; } +static bool of_machine_is_compatible(const char *n) { (void)n; return machine; } +static bool of_device_is_compatible(struct device_node *n, const char *s) +{ (void)s; return n->external; } +static bool iomfb_check_callback_h17p(struct apple_dcp *d, int t, u32 i, u32 o) +{ (void)d; (void)t; (void)i; (void)o; return true; } +static bool iomfb_uses_queue(struct apple_dcp *d) { (void)d; return false; } +static bool test_and_set_bit(int t, unsigned long *b) { (void)t; (void)b; return false; } +static void complete(int *p) { *p = 1; } +static u8 dcp_push_depth(unsigned *d) { return (*d)++; } +static void dcp_ack(struct apple_dcp *d, enum dcp_context_id c) +{ (void)c; acks++; d->channel.depth--; } +""" +suffix = r""" +int main(void) +{ + struct device_node node; + struct device dev = { &node }; + bool (*handlers[IOMFB_MAX_CB])(struct apple_dcp *, int, void *, void *) = {}; + unsigned cases = 0; + for (unsigned scope = 0; scope < 8; scope++) + for (unsigned tag = 133; tag <= 135; tag++) + for (unsigned context = 0; context < 8; context++) + for (unsigned in = 0; in <= 8; in++) + for (unsigned out = 0; out <= 8; out++) { + struct apple_dcp d = { + .dev = &dev, .fw_compat = 1, .experimental_callback_logs = 64, + .cb_handlers = handlers, .hw.iomfb_method_profile = 2, + }; + union { uint64_t align; unsigned char data[64]; } packet; + struct dcp_packet_header *h = (void *)packet.data; + memset(&packet, 0xa5, sizeof(packet)); + h->in_len = in; h->out_len = out; + machine = scope & 1; node.external = scope & 2; + if (!(scope & 4)) + d.hw.iomfb_method_profile = 0; + case_tag = tag; acks = 0; + dcpep_handle_cb(&d, context, h, sizeof(*h) + in + out, 0); + bool accepted = scope == 7 && tag == 134 && context == 3 && !in && out == 4; + if (scope & 4) { + assert(acks == accepted && d.crashed != accepted); + for (unsigned i = 0; i < out; i++) + assert(packet.data[sizeof(*h) + in + i] == (accepted ? 0 : 0xa5)); + } else { + assert(acks == 1 && !d.crashed); /* Existing legacy behavior. */ + } + assert(packet.data[sizeof(*h) + in + out] == 0xa5); + cases++; + } + return cases != 15552; +} +""" +with tempfile.TemporaryDirectory(prefix="j700-d134-") as tmp: + c = Path(tmp) / "test.c" + exe = Path(tmp) / "test" + c.write_text(prefix + source[start:end] + suffix) + subprocess.run(["clang", "-std=gnu11", "-Wall", "-Wextra", "-Werror", + "-fsanitize=address,undefined", str(c), "-o", str(exe)], check=True) + subprocess.run([str(exe)], check=True) +print("15552 dispatcher cases: D134 bounds/scope, rejection and legacy replies PASS") From 1a317d5142ea6ff0e563403d51a0b738ce01927c Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 19:30:46 +1100 Subject: [PATCH 46/52] drm/apple: route native training readback during modeset R24 reaches external mode export and a visible but frozen image. Firmware uses group 1 command 4 to read training settings during modeset; route it to the bounded native readback helper instead of rejecting it. Test returned cached lane values, no PHY writes and short-reply rejection. Include pending command and present-state information in the existing transaction timeout diagnostic. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/dptxep.c | 2 ++ drivers/gpu/drm/apple/iomfb.c | 14 +++++++++++++- tools/diagnostics/apple-dptx-training-test.c | 14 +++++++++++++- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/drivers/gpu/drm/apple/dptxep.c b/drivers/gpu/drm/apple/dptxep.c index 964f2896b6373..34c49d078533e 100644 --- a/drivers/gpu/drm/apple/dptxep.c +++ b/drivers/gpu/drm/apple/dptxep.c @@ -889,6 +889,8 @@ static int dptxport_group_call(struct apple_epic_service *service, u16 group, u3 { if (group == 1 && command == DPTX_APCALL_SET_DRIVE_SETTINGS) return dptx_j700_training(service, true, data, size, reply, room); + if (group == 1 && command == DPTX_APCALL_GET_DRIVE_SETTINGS) + return dptx_j700_training(service, false, data, size, reply, room); if (group) return -EOPNOTSUPP; if (command == DPTX_APCALL_SET_DRIVE_SETTINGS) diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index 5fe53684795fc..8d1fecfec763f 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -312,7 +312,19 @@ static void iomfb_queue_timeout(struct work_struct *work) dcp->iomfb.deadline - jiffies); } else if (dcp->iomfb.active) { WRITE_ONCE(dcp->crashed, true); - dev_err(dcp->dev, "IOMFB transaction timed out\n"); + dev_err(dcp->dev, + "IOMFB transaction timed out: cmd=%u oob=%u present=%u accepted=%u swap=%u\n", + dcp->ch_cmd.depth, dcp->ch_oobcmd.depth, + dcp->present_state_h17p.pending, dcp->present_state_h17p.accepted, + dcp->present_state_h17p.swap_id); + if (dcp->ch_cmd.depth && dcp->ch_cmd.depth <= DCP_MAX_CALL_DEPTH) { + const struct dcp_packet_header *h = + &dcp->ch_cmd.header[dcp->ch_cmd.depth - 1]; + + dev_err(dcp->dev, "pending IOMFB %c%c%c%c in=%u out=%u\n", + h->tag[3], h->tag[2], h->tag[1], h->tag[0], + h->in_len, h->out_len); + } iomfb_discard_pending(dcp); schedule_work(&dcp->vblank_wq); } diff --git a/tools/diagnostics/apple-dptx-training-test.c b/tools/diagnostics/apple-dptx-training-test.c index cb1cea55a6452..7e3a7f3b59790 100644 --- a/tools/diagnostics/apple-dptx-training-test.c +++ b/tools/diagnostics/apple-dptx-training-test.c @@ -120,13 +120,25 @@ int main(void) if (group == 0 && command == 3) expected = -EOPNOTSUPP; - if ((group == 1 && command == 3) || + if ((group == 1 && (command == 3 || command == 4)) || (group == 0 && (command == 4 || command == 26))) expected = 0; assert(dptxport_group_call(&service, group, command, request, 80, reply, 80) == expected); } } + put_unaligned_le32(2, request); + unsigned int before_readback = calls; + + memset(reply, 0xff, 80); + assert(!dptxport_group_call(&service, 1, 4, request, 56, reply, 56)); + assert(calls == before_readback); + assert(get_unaligned_le32(reply) == 2 && !get_unaligned_le32(reply + 16)); + assert(get_unaligned_le32(reply + 36) == port.trained_voltage[0]); + assert(get_unaligned_le32(reply + 52) == port.trained_pre[1]); + assert(reply[56] == 0xff); + assert(dptxport_group_call(&service, 1, 4, request, 56, reply, 55) == -EINVAL); + assert(calls == before_readback); assert(dptxport_group_call(&service, 0, 26, request, 80, reply, 31) == -EINVAL); memset(reply, 0xff, 80); assert(!dptxport_group_call(&service, 0, 26, request, 80, reply, 80)); From 4b0a450bef3a82e3453e9ee28c817d03e98743d9 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 19:35:59 +1100 Subject: [PATCH 47/52] drm/apple: keep opaque X property off experimental DCPEXT The external controller rejects the post-first-present enableGPAlphaDiv property with status 4. Treating that panel workaround as required marks the external session crashed and freezes a visible image. Leave the experimental external controller on its native presentation path while preserving the existing internal-panel property queue. Cover 396 production-queue scope/state/capacity cases and allocation failure under ASan/UBSan. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/iomfb.c | 4 + .../test-apple-j700-opaque-scope.py | 83 +++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 tools/diagnostics/test-apple-j700-opaque-scope.py diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index 8d1fecfec763f..e7eefa6abf91a 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -193,6 +193,10 @@ static int iomfb_enqueue_opaque_x(struct apple_dcp *dcp) struct iomfb_opaque_x_transaction *opaque; lockdep_assert_held(&dcp->iomfb.lock); + /* DCPEXT rejects enableGPAlphaDiv; retain the panel-only workaround. */ + if (dcp->hw.iomfb_method_profile == + DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) + return 0; if (atomic_read(&dcp->iomfb.opaque_x_state) != IOMFB_OPAQUE_X_NEEDED) return 0; if (dcp->iomfb.queued >= 32) diff --git a/tools/diagnostics/test-apple-j700-opaque-scope.py b/tools/diagnostics/test-apple-j700-opaque-scope.py new file mode 100644 index 0000000000000..d9827134366b2 --- /dev/null +++ b/tools/diagnostics/test-apple-j700-opaque-scope.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +"""Check that the actual property queue preserves panel behavior, excludes DCPEXT.""" +from pathlib import Path +import subprocess +import tempfile + +root = Path(__file__).resolve().parents[2] +source = (root / "drivers/gpu/drm/apple/iomfb.c").read_text() +start = source.index("static int iomfb_enqueue_opaque_x(") +end = source.index("\n}\n", start) + 3 +prefix = r""" +#include +#include +#include +#define DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL 2 +enum { IOMFB_OPAQUE_X_WAITING, IOMFB_OPAQUE_X_NEEDED, + IOMFB_OPAQUE_X_QUEUED, IOMFB_OPAQUE_X_READY }; +struct list_head { unsigned count; }; +struct apple_dcp; +struct iomfb_transaction { + void (*start)(struct apple_dcp *, struct iomfb_transaction *); + void (*release)(struct iomfb_transaction *); + struct list_head link; +}; +struct iomfb_opaque_x_transaction { struct iomfb_transaction transaction; }; +struct apple_dcp { + struct { int iomfb_method_profile; } hw; + struct { int lock, opaque_x_state; unsigned queued; struct list_head pending; } iomfb; +}; +static unsigned allocations; +static int fail_alloc; +static void *last_allocation; +static void *allocate(size_t n) { allocations++; return last_allocation = fail_alloc ? NULL : calloc(1, n); } +#define kzalloc_obj(x) allocate(sizeof(x)) +#define kfree(x) free(x) +#define lockdep_assert_held(x) ((void)(x)) +#define atomic_read(x) (*(x)) +static int atomic_cmpxchg(int *p, int from, int to) +{ int old = *p; if (old == from) *p = to; return old; } +static void list_add(struct list_head *n, struct list_head *h) +{ (void)n; h->count++; } +static void iomfb_opaque_x_start(struct apple_dcp *d, struct iomfb_transaction *t) +{ (void)d; (void)t; } +static void iomfb_opaque_x_release(struct iomfb_transaction *t) { (void)t; } +""" +suffix = r""" +int main(void) +{ + for (int state = 0; state < 4; state++) + for (unsigned queued = 0; queued <= 32; queued++) + for (int profile = 0; profile < 3; profile++) { + struct apple_dcp d = { .hw.iomfb_method_profile = profile, + .iomfb = { .opaque_x_state = state, .queued = queued } }; + last_allocation = NULL; + unsigned before = allocations; + int ret = iomfb_enqueue_opaque_x(&d); + if (profile == 2 || state != IOMFB_OPAQUE_X_NEEDED) { + assert(!ret && allocations == before); + assert(d.iomfb.queued == queued && d.iomfb.opaque_x_state == state); + } else if (queued == 32) { + assert(ret == -EBUSY && allocations == before); + } else { + assert(!ret && allocations == before + 1); + assert(d.iomfb.queued == queued + 1 && d.iomfb.pending.count == 1); + assert(d.iomfb.opaque_x_state == IOMFB_OPAQUE_X_QUEUED); + } + free(last_allocation); + } + struct apple_dcp d = { .iomfb.opaque_x_state = IOMFB_OPAQUE_X_NEEDED }; + fail_alloc = 1; + assert(iomfb_enqueue_opaque_x(&d) == -ENOMEM); + return 0; +} +""" +with tempfile.TemporaryDirectory(prefix="j700-property-") as tmp: + c = Path(tmp) / "test.c" + exe = Path(tmp) / "test" + c.write_text(prefix + source[start:end] + suffix) + subprocess.run(["clang", "-std=gnu11", "-Wall", "-Wextra", "-Werror", + "-fsanitize=address,undefined", str(c), "-o", str(exe)], check=True) + subprocess.run([str(exe)], check=True) +print("396 actual queue scope/state/capacity cases and allocation failure: PASS") From 649b825945882c0389c3ca5ea51453e6a3435f0c Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 19:40:33 +1100 Subject: [PATCH 48/52] drm/apple: add output-targeted Wayland presentation check Run a bounded GTK4 animation on the selected Wayland connector and report draw, frame callback and presentation-feedback counts. The window closes automatically without changing persistent display settings. This allows external presentation to be checked while the operator is away. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- .../diagnostics/wayland-output-frame-test.py | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 tools/diagnostics/wayland-output-frame-test.py diff --git a/tools/diagnostics/wayland-output-frame-test.py b/tools/diagnostics/wayland-output-frame-test.py new file mode 100644 index 0000000000000..107fad56f539f --- /dev/null +++ b/tools/diagnostics/wayland-output-frame-test.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +"""Show a bounded animation on one Wayland output and report frame feedback.""" +import argparse +import json +import time + +import gi + +gi.require_version("Gtk", "4.0") +gi.require_version("Gdk", "4.0") +from gi.repository import Gdk, GLib, Gtk # noqa: E402 + +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument("--connector", default="DP-1") +parser.add_argument("--seconds", type=int, default=8, choices=range(3, 31)) +args = parser.parse_args() +Gtk.init() +display = Gdk.Display.get_default() +if display is None: + raise SystemExit("No display connection") +monitors = display.get_monitors() +matches = [monitors.get_item(i) for i in range(monitors.get_n_items()) + if monitors.get_item(i).get_connector() == args.connector] +if len(matches) != 1: + raise SystemExit("Requested output is absent or ambiguous") + +loop = GLib.MainLoop() +window = Gtk.Window(title="Aurora external display frame test") +area = Gtk.DrawingArea() +window.set_child(area) +started = time.monotonic() +stats = {"connector": args.connector, "ticks": 0, "draws": 0} +feedback = set() + + +def draw(_area, context, width, height): + phase = (time.monotonic() - started) % 2 / 2 + context.set_source_rgb(0.06, 0.10, 0.17) + context.paint() + context.set_source_rgb(0.15, 0.80, 0.55) + context.rectangle(phase * width, height * 0.25, width * 0.15, height * 0.5) + context.fill() + stats["draws"] += 1 + + +def tick(widget, clock): + stats["ticks"] += 1 + previous = clock.get_timings(clock.get_frame_counter() - 1) + if previous is not None and previous.get_complete(): + presented = previous.get_presentation_time() + if presented: + feedback.add(presented) + widget.queue_draw() + return GLib.SOURCE_CONTINUE + + +def finish(): + window.destroy() + loop.quit() + return GLib.SOURCE_REMOVE + + +area.set_draw_func(draw) +area.add_tick_callback(tick) +window.connect("close-request", lambda _window: finish()) +window.fullscreen_on_monitor(matches[0]) +window.present() +GLib.timeout_add_seconds(args.seconds, finish) +loop.run() +stats["elapsed_s"] = round(time.monotonic() - started, 3) +stats["presentation_feedback_frames"] = len(feedback) +stats["callback_pass"] = stats["ticks"] >= args.seconds * 15 +stats["draw_pass"] = stats["draws"] >= args.seconds * 15 +stats["presentation_pass"] = len(feedback) >= args.seconds * 10 +print(json.dumps(stats, sort_keys=True)) +raise SystemExit(0 if all(stats[k] for k in + ("callback_pass", "draw_pass", "presentation_pass")) else 1) From 77c3d30fd99b2fd98138d1384aa733d33c3c4892 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 19:41:23 +1100 Subject: [PATCH 49/52] diagnostics: collect delayed Wayland frame feedback Inspect the retained frame history instead of only the immediately preceding frame, whose presentation feedback may still be pending. Report completed timing records separately from presentation timestamps. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- tools/diagnostics/wayland-output-frame-test.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tools/diagnostics/wayland-output-frame-test.py b/tools/diagnostics/wayland-output-frame-test.py index 107fad56f539f..e9f0893ead65d 100644 --- a/tools/diagnostics/wayland-output-frame-test.py +++ b/tools/diagnostics/wayland-output-frame-test.py @@ -32,6 +32,7 @@ started = time.monotonic() stats = {"connector": args.connector, "ticks": 0, "draws": 0} feedback = set() +complete_frames = set() def draw(_area, context, width, height): @@ -46,11 +47,14 @@ def draw(_area, context, width, height): def tick(widget, clock): stats["ticks"] += 1 - previous = clock.get_timings(clock.get_frame_counter() - 1) - if previous is not None and previous.get_complete(): - presented = previous.get_presentation_time() - if presented: - feedback.add(presented) + counter = clock.get_frame_counter() + for frame in range(max(0, counter - 16), counter): + previous = clock.get_timings(frame) + if previous is not None and previous.get_complete(): + complete_frames.add(frame) + presented = previous.get_presentation_time() + if presented: + feedback.add(presented) widget.queue_draw() return GLib.SOURCE_CONTINUE @@ -70,6 +74,7 @@ def finish(): loop.run() stats["elapsed_s"] = round(time.monotonic() - started, 3) stats["presentation_feedback_frames"] = len(feedback) +stats["complete_timings"] = len(complete_frames) stats["callback_pass"] = stats["ticks"] >= args.seconds * 15 stats["draw_pass"] = stats["draws"] >= args.seconds * 15 stats["presentation_pass"] = len(feedback) >= args.seconds * 10 From 9190b4b742cfc4d95bc1b5dea00c8c96820d441e Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 19:45:26 +1100 Subject: [PATCH 50/52] drm/apple: remove temporary external bring-up diagnostics Drop temporary protocol capture counters, PHY lock chatter, the global Type-C trace addition and unused link-rate override. Preserve standard tracepoints, failure diagnostics and the validated HBR3 behavior. Register the remaining focused tests and output-frame utility with MAINTAINERS. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- MAINTAINERS | 3 + drivers/gpu/drm/apple/afk.c | 17 +---- drivers/gpu/drm/apple/afk.h | 2 - drivers/gpu/drm/apple/dcp-internal.h | 2 - drivers/gpu/drm/apple/dptxep.c | 21 +----- drivers/gpu/drm/apple/iomfb.c | 8 +-- drivers/phy/apple/atc-j700-dp.h | 2 - drivers/usb/typec/mux.c | 1 - .../diagnostics/test-apple-j700-link-rate.py | 70 ------------------- 9 files changed, 7 insertions(+), 119 deletions(-) delete mode 100644 tools/diagnostics/test-apple-j700-link-rate.py diff --git a/MAINTAINERS b/MAINTAINERS index 0ceb4b28e0fef..f7a8a7dd863ca 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -2019,9 +2019,12 @@ F: sound/soc/apple/t8140_aop_audio.rs F: tools/diagnostics/apple-dptx-training-test.c F: tools/diagnostics/apple-j700-dp-test.c F: tools/diagnostics/test-apple-j700-crossbar.py +F: tools/diagnostics/test-apple-j700-d134.py F: tools/diagnostics/test-apple-j700-dp.py +F: tools/diagnostics/test-apple-j700-opaque-scope.py F: tools/diagnostics/test-apple-j700-provider.py F: tools/diagnostics/test-apple-j700-tuning.py +F: tools/diagnostics/wayland-output-frame-test.py F: tools/testing/apple/ APPLETALK NETWORK LAYER diff --git a/drivers/gpu/drm/apple/afk.c b/drivers/gpu/drm/apple/afk.c index 38b44c2d6d785..b45695ddf9836 100644 --- a/drivers/gpu/drm/apple/afk.c +++ b/drivers/gpu/drm/apple/afk.c @@ -922,15 +922,6 @@ static void afk_recv_handle(struct apple_dcp_afkep *ep, u32 channel, u32 type, payload_size = data_size - hdr_len; trace_afk_recv_handle(ep, channel, type, data_size, ehdr, eshdr); - if (ep->dcp->hw.iomfb_method_profile == - DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL && - ep->experimental_rx_logs < 32) { - ep->experimental_rx_logs++; - dev_info(ep->dcp->dev, - "external AFK ep=%u channel=%u type=%u subtype=%u category=%u bytes=%zu\n", - ep->endpoint, channel, type, subtype, eshdr->category, - data_size); - } service = afk_epic_find_service(ep, channel); @@ -1092,13 +1083,7 @@ static void afk_receive_message_worker(struct work_struct *work_) work = container_of(work_, struct afk_receive_message_work, work); type = FIELD_GET(RBEP_TYPE, work->message); - if (work->ep->dcp->hw.iomfb_method_profile == - DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL && - (work->ep->endpoint == DPTX_ENDPOINT || - work->ep->endpoint == DPTX_ENDPOINT_V2) && type != RBEP_RECV) - dev_info_ratelimited(work->ep->dcp->dev, - "external AFK control ep=%#x type=%#x\n", - work->ep->endpoint, type); + switch (type) { case RBEP_INIT_ACK: break; diff --git a/drivers/gpu/drm/apple/afk.h b/drivers/gpu/drm/apple/afk.h index 3d1f975d798a8..c6a34fa67399e 100644 --- a/drivers/gpu/drm/apple/afk.h +++ b/drivers/gpu/drm/apple/afk.h @@ -295,8 +295,6 @@ struct afk_ringbuffer { struct apple_dcp_afkep { struct apple_dcp *dcp; - /* Bounded external protocol observations, owned by the RX worker. */ - u8 experimental_rx_logs; u32 endpoint; /* Set before afk_start; protocol selection is per endpoint. */ diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index 883d4f9ce2e80..48f4112151dd3 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -212,8 +212,6 @@ struct apple_dcp { struct apple_connector *connector; struct apple_dcp_hw_data hw; - /* Protected by the H17P IOMFB receive mutex. */ - u8 experimental_callback_logs; bool external_prepared; /* firmware version and compatible firmware version */ diff --git a/drivers/gpu/drm/apple/dptxep.c b/drivers/gpu/drm/apple/dptxep.c index 34c49d078533e..6c4012d6ad752 100644 --- a/drivers/gpu/drm/apple/dptxep.c +++ b/drivers/gpu/drm/apple/dptxep.c @@ -4,7 +4,6 @@ #include #include #include -#include #include #include #include @@ -21,12 +20,6 @@ #include "dptx-j700-training.h" -/* Diagnostic ceiling for the experimental physical J700 output only. */ -static unsigned int j700_dp_max_link_rate = LINK_RATE_HBR3; -module_param(j700_dp_max_link_rate, uint, 0600); -MODULE_PARM_DESC(j700_dp_max_link_rate, - "J700 external DP ceiling: 6=RBR, 10=HBR, 20=HBR2, 30=HBR3"); - struct dcpdptx_connection_cmd { __le32 unk; __le32 target; @@ -350,22 +343,12 @@ static int dptxport_call_get_max_link_rate(struct apple_epic_service *service, void *reply_, size_t reply_size) { struct dptxport_apcall_link_rate *reply = reply_; - struct apple_dcp *dcp = service->ep->dcp; - u32 ceiling = READ_ONCE(j700_dp_max_link_rate); if (reply_size < sizeof(*reply)) return -EINVAL; reply->retcode = cpu_to_le32(0); - if (service->cookie && ((struct dptx_port *)service->cookie)->atcphy && - dcp->hw.iomfb_method_profile == - DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL && - of_machine_is_compatible("apple,j700") && - (ceiling == LINK_RATE_RBR || ceiling == LINK_RATE_HBR || - ceiling == LINK_RATE_HBR2 || ceiling == LINK_RATE_HBR3)) - reply->link_rate = cpu_to_le32(ceiling); - else - reply->link_rate = cpu_to_le32(LINK_RATE_HBR3); + reply->link_rate = cpu_to_le32(LINK_RATE_HBR3); return 0; } @@ -861,7 +844,7 @@ static void dptxport_init(struct apple_epic_service *service, const char *name, return; } port = 0; - dev_info(dcp->dev, "DPTX physical unit %lld maps to logical port 0\n", unit); + dev_dbg(dcp->dev, "DPTX physical unit %lld maps to logical port 0\n", unit); } switch (port) { diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index e7eefa6abf91a..786eb53383329 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -823,13 +823,7 @@ static void dcpep_handle_cb(struct apple_dcp *dcp, enum dcp_context_id context, } tag = dcp_parse_tag(hdr->tag); - if (dcp->hw.iomfb_method_profile == - DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL && - dcp->experimental_callback_logs < 64) { - dcp->experimental_callback_logs++; - dev_info(dev, "external IOMFB callback=%d in=%u out=%u context=%u\n", - tag, hdr->in_len, hdr->out_len, context); - } + handled = tag >= 0 && tag < IOMFB_MAX_CB && dcp->cb_handlers && dcp->cb_handlers[tag]; diff --git a/drivers/phy/apple/atc-j700-dp.h b/drivers/phy/apple/atc-j700-dp.h index 11d7b32003748..9071587bd5e8d 100644 --- a/drivers/phy/apple/atc-j700-dp.h +++ b/drivers/phy/apple/atc-j700-dp.h @@ -303,9 +303,7 @@ static int j700_dp_configure(struct apple_atcphy *p, const struct phy_configure_ unsigned int pmas, capacity, first, table; int rate = -1, ret; - dev_dbg(p->dev, "J700 DP configure waiting for PHY lock\n"); guard(mutex)(&p->lock); - dev_dbg(p->dev, "J700 DP configure acquired PHY lock\n"); if ((o->set_rate && !o->link_rate) || (o->set_lanes && !o->lanes)) return o->set_voltages ? -EINVAL : j700_dp_stop(p); if (p->mode != APPLE_ATCPHY_MODE_DP && p->mode != APPLE_ATCPHY_MODE_USB3_DP) diff --git a/drivers/usb/typec/mux.c b/drivers/usb/typec/mux.c index dc763766012f7..7e327118cbd71 100644 --- a/drivers/usb/typec/mux.c +++ b/drivers/usb/typec/mux.c @@ -388,7 +388,6 @@ int typec_mux_set(struct typec_mux *mux, struct typec_mux_state *state) mux_dev = mux->mux_devs[i]; ret = mux_dev->set(mux_dev, state); - dev_dbg(&mux_dev->dev, "mode=%lu callback result=%d\n", state->mode, ret); if (ret && ret != -EOPNOTSUPP) return ret; } diff --git a/tools/diagnostics/test-apple-j700-link-rate.py b/tools/diagnostics/test-apple-j700-link-rate.py deleted file mode 100644 index 7bef0cc936918..0000000000000 --- a/tools/diagnostics/test-apple-j700-link-rate.py +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env python3 -# SPDX-License-Identifier: GPL-2.0-only -"""Exercise the production link ceiling callback and its isolation boundaries.""" -from pathlib import Path -import subprocess -import tempfile - -root = Path(__file__).resolve().parents[2] -source = (root / "drivers/gpu/drm/apple/dptxep.c").read_text() -start = source.index("static int dptxport_call_get_max_link_rate(") -end = source.index("\n}\n", start) + 3 -stub = r""" -#include -#include -#include -#include -#include -typedef uint32_t u32; -#define LINK_RATE_RBR 6 -#define LINK_RATE_HBR 10 -#define LINK_RATE_HBR2 20 -#define LINK_RATE_HBR3 30 -#define DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL 7 -#define READ_ONCE(x) (x) -#define cpu_to_le32(x) (x) -struct apple_dcp { struct { int iomfb_method_profile; } hw; }; -struct apple_epic_endpoint { struct apple_dcp *dcp; }; -struct apple_epic_service { struct apple_epic_endpoint *ep; void *cookie; }; -struct dptx_port { void *atcphy; }; -struct dptxport_apcall_link_rate { u32 retcode; char pad[12]; u32 link_rate; char tail[12]; }; -static u32 j700_dp_max_link_rate; -static bool j700; -static bool of_machine_is_compatible(const char *name) { (void)name; return j700; } -""" -main = r""" -int main(void) { - struct apple_dcp dcp = {}; - struct apple_epic_endpoint ep = { &dcp }; - struct dptx_port port = {}; - struct apple_epic_service service = { &ep, &port }; - struct dptxport_apcall_link_rate reply; - unsigned cases = 0; - for (unsigned rate = 0; rate <= 40; rate++) - for (unsigned scope = 0; scope < 8; scope++) { - j700 = scope & 1; - port.atcphy = (scope & 2) ? &port : NULL; - dcp.hw.iomfb_method_profile = (scope & 4) ? 7 : 0; - j700_dp_max_link_rate = rate; - assert(dptxport_call_get_max_link_rate(&service, &reply, sizeof(reply)) == 0); - bool valid = rate == 6 || rate == 10 || rate == 20 || rate == 30; - assert(reply.link_rate == ((scope == 7 && valid) ? rate : 30)); - assert(reply.retcode == 0); - cases++; - } - service.cookie = NULL; - assert(!dptxport_call_get_max_link_rate(&service, &reply, sizeof(reply))); - assert(reply.link_rate == 30); - for (unsigned size = 0; size < sizeof(reply); size++) - assert(dptxport_call_get_max_link_rate(&service, &reply, size) == -EINVAL); - return cases != 328; -} -""" -with tempfile.TemporaryDirectory(prefix="j700-rate-") as tmp: - c = Path(tmp) / "test.c" - exe = Path(tmp) / "test" - c.write_text(stub + source[start:end] + main) - subprocess.run(["clang", "-Wall", "-Wextra", "-Werror", - "-fsanitize=address,undefined", str(c), "-o", str(exe)], check=True) - subprocess.run([str(exe)], check=True) -print("328 link ceilings and scope cases, null port and all short replies: PASS") From 18d69dc8064845d35c8d868efc917682b7171a62 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 19:51:31 +1100 Subject: [PATCH 51/52] drm/apple: close external admission and recovery races Make explicitly enabled external providers participate in component matching before readiness, then validate prepared state after component binding. Reject unpublished or retired DPTX services and preserve stable ownership rather than reusing a published service generation. Retain failed J700 PHY transition state so SAFE retries cleanup, and reject normal training while that state is dirty. Add fault, admission/lifetime and provider-order regressions. A published DPTX service loss remains fail-closed until recovery; these changes do not claim physical hotplug qualification. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- MAINTAINERS | 2 + drivers/gpu/drm/apple/Makefile | 2 + drivers/gpu/drm/apple/afk-service-test.c | 99 ++++++ drivers/gpu/drm/apple/afk.c | 27 +- drivers/gpu/drm/apple/afk.h | 32 +- drivers/gpu/drm/apple/apple_drv.c | 23 +- drivers/gpu/drm/apple/dcp.c | 27 +- drivers/gpu/drm/apple/dptx-service.h | 63 ++++ drivers/gpu/drm/apple/dptxep.c | 55 ++-- drivers/phy/apple/atc-j700-dp.h | 26 +- drivers/phy/apple/atc.c | 28 +- tools/diagnostics/apple-j700-dp-test.c | 4 +- tools/diagnostics/test-apple-atc-unwind.py | 295 ++++++++++++++++++ tools/diagnostics/test-apple-dcp-readiness.py | 246 +++++++++++++++ 14 files changed, 856 insertions(+), 73 deletions(-) create mode 100644 drivers/gpu/drm/apple/dptx-service.h create mode 100644 tools/diagnostics/test-apple-atc-unwind.py create mode 100644 tools/diagnostics/test-apple-dcp-readiness.py diff --git a/MAINTAINERS b/MAINTAINERS index f7a8a7dd863ca..e7b22a9c9b774 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -2018,6 +2018,8 @@ F: rust/kernel/soc/apple/aop.rs F: sound/soc/apple/t8140_aop_audio.rs F: tools/diagnostics/apple-dptx-training-test.c F: tools/diagnostics/apple-j700-dp-test.c +F: tools/diagnostics/test-apple-atc-unwind.py +F: tools/diagnostics/test-apple-dcp-readiness.py F: tools/diagnostics/test-apple-j700-crossbar.py F: tools/diagnostics/test-apple-j700-d134.py F: tools/diagnostics/test-apple-j700-dp.py diff --git a/drivers/gpu/drm/apple/Makefile b/drivers/gpu/drm/apple/Makefile index 837aa713ad435..4980b2863225b 100644 --- a/drivers/gpu/drm/apple/Makefile +++ b/drivers/gpu/drm/apple/Makefile @@ -21,3 +21,5 @@ appledrm-$(CONFIG_TRACING) += trace.o obj-$(CONFIG_DRM_APPLE) += appledrm.o obj-$(CONFIG_DRM_APPLE_KUNIT_TEST) += afk_test.o obj-$(CONFIG_DRM_APPLE_KUNIT_TEST) += dcp_backlight_test.o + +obj-$(CONFIG_DRM_APPLE_KUNIT_TEST) += afk-service-test.o diff --git a/drivers/gpu/drm/apple/afk-service-test.c b/drivers/gpu/drm/apple/afk-service-test.c index 4a5f07fa9b15f..a86df389deb17 100644 --- a/drivers/gpu/drm/apple/afk-service-test.c +++ b/drivers/gpu/drm/apple/afk-service-test.c @@ -3,6 +3,7 @@ #include #include "afk.h" +#include "dptx-service.h" static const struct apple_epic_service_ops reusable_ops = { .name = "reusable", @@ -122,7 +123,105 @@ static void afk_service_unconverted_owner_test(struct kunit *test) KUNIT_EXPECT_FALSE(test, afk_service_reinit(service, NULL, &reusable_ops, 2)); } +static int afk_test_prepare(struct apple_epic_service *service, + const char *name, const char *class, s64 unit) +{ + return 0; +} + +static const struct apple_epic_service_ops prepared_ops = { + .name = "prepared", + .prepare = afk_test_prepare, + .teardown = dptx_service_unbind, +}; + +static void afk_service_admission_test(struct kunit *test) +{ + struct apple_epic_service *service = test->priv; + + KUNIT_ASSERT_TRUE(test, afk_service_reinit(service, NULL, &prepared_ops, 1)); + /* Neither callback dispatch nor command callers can observe preparation. */ + KUNIT_EXPECT_FALSE(test, afk_service_matches(service, 1)); + KUNIT_EXPECT_PTR_EQ(test, afk_service_get(service), NULL); + /* A rejected announcement or failed OPEN consumes no service slot. */ + afk_service_discard(service); + KUNIT_ASSERT_TRUE(test, afk_service_reinit(service, NULL, &prepared_ops, 2)); + /* Cookie-less services are valid: admission does not test cookie != NULL. */ + afk_service_publish(service); + KUNIT_EXPECT_TRUE(test, afk_service_matches(service, 2)); + KUNIT_ASSERT_PTR_EQ(test, afk_service_get(service), service); + afk_service_put(service); +} + +static void dptx_service_identity_test(struct kunit *test) +{ + const char *name = "dcpdptx-port-epic"; + const char *class = "AppleDCPDPTXRemotePort"; + + KUNIT_EXPECT_EQ(test, dptx_service_port(name, class, 3, true, 3, 1), 0); + KUNIT_EXPECT_EQ(test, dptx_service_port(name, class, 0, true, 3, 1), -EINVAL); + KUNIT_EXPECT_EQ(test, dptx_service_port(name, class, -1, true, 3, 1), -EINVAL); + KUNIT_EXPECT_EQ(test, dptx_service_port(name, class, 3, true, 3, 2), -EINVAL); + KUNIT_EXPECT_EQ(test, dptx_service_port(NULL, class, 3, true, 3, 1), -EINVAL); + KUNIT_EXPECT_EQ(test, dptx_service_port(name, NULL, 3, true, 3, 1), -EINVAL); + KUNIT_EXPECT_EQ(test, dptx_service_port("wrong", class, 3, true, 3, 1), -EINVAL); + KUNIT_EXPECT_EQ(test, dptx_service_port(name, "wrong", 3, true, 3, 1), -EINVAL); + KUNIT_EXPECT_EQ(test, dptx_service_port(name, class, 1, false, 3, 2), 1); + KUNIT_EXPECT_EQ(test, dptx_service_port(name, class, 2, false, 3, 2), -EINVAL); +} + +static void dptx_service_duplicate_close_test(struct kunit *test) +{ + struct apple_epic_service *first = test->priv; + struct apple_epic_service *next; + struct dptx_port port = {}; + + next = kunit_kzalloc(test, sizeof(*next), GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, next); + spin_lock_init(&next->lock); + init_completion(&port.enable_completion); + KUNIT_ASSERT_TRUE(test, afk_service_reinit(first, NULL, &prepared_ops, 1)); + KUNIT_ASSERT_EQ(test, dptx_service_bind(first, &port, 3), 0); + /* An observer during preparation/OPEN sees no available port or owner. */ + KUNIT_EXPECT_FALSE(test, dptx_service_available(&port)); + KUNIT_EXPECT_PTR_EQ(test, port.service, NULL); + KUNIT_EXPECT_PTR_EQ(test, first->cookie, &port); + /* Failed OPEN does not publish anything and permits a fresh attempt. */ + afk_service_discard(first); + KUNIT_EXPECT_FALSE(test, port.enabled); + KUNIT_EXPECT_PTR_EQ(test, port.service, NULL); + KUNIT_ASSERT_TRUE(test, afk_service_reinit(first, NULL, &prepared_ops, 1)); + KUNIT_ASSERT_EQ(test, dptx_service_bind(first, &port, 3), 0); + afk_service_publish(first); + KUNIT_EXPECT_FALSE(test, port.enabled); + dptx_service_publish(first); + KUNIT_EXPECT_TRUE(test, dptx_service_available(&port)); + KUNIT_EXPECT_PTR_EQ(test, port.service->cookie, &port); + KUNIT_ASSERT_TRUE(test, afk_service_reinit(next, NULL, &prepared_ops, 2)); + KUNIT_EXPECT_EQ(test, dptx_service_bind(next, &port, 3), -EBUSY); + afk_service_discard(next); + KUNIT_EXPECT_TRUE(test, port.enabled); + KUNIT_EXPECT_PTR_EQ(test, port.service, first); + /* CLOSE invalidates a connected session without publishing a replacement. */ + port.connected = true; + afk_test_retire(first); + dptx_service_unbind(first); + KUNIT_EXPECT_FALSE(test, port.enabled); + KUNIT_EXPECT_FALSE(test, port.connected); + KUNIT_EXPECT_PTR_EQ(test, first->cookie, &port); + KUNIT_ASSERT_TRUE(test, afk_service_reinit(next, NULL, &prepared_ops, 3)); + KUNIT_EXPECT_EQ(test, dptx_service_bind(next, &port, 3), -EBUSY); + afk_service_discard(next); + /* Old work between validate/connect/request still names the old session. */ + KUNIT_EXPECT_PTR_EQ(test, port.service, first); + KUNIT_EXPECT_PTR_EQ(test, afk_service_get(port.service), NULL); + KUNIT_EXPECT_FALSE(test, port.enabled); +} + static struct kunit_case afk_service_cases[] = { + KUNIT_CASE(afk_service_admission_test), + KUNIT_CASE(dptx_service_identity_test), + KUNIT_CASE(dptx_service_duplicate_close_test), KUNIT_CASE(afk_service_many_generations_test), KUNIT_CASE(afk_service_reader_pin_test), KUNIT_CASE(afk_service_pending_reply_test), diff --git a/drivers/gpu/drm/apple/afk.c b/drivers/gpu/drm/apple/afk.c index b45695ddf9836..3fecf8d931db5 100644 --- a/drivers/gpu/drm/apple/afk.c +++ b/drivers/gpu/drm/apple/afk.c @@ -388,6 +388,16 @@ static void afk_recv_handle_init(struct apple_dcp_afkep *ep, u32 channel, goto free; } ep->num_channels = max(ep->num_channels, ch_idx + 1); + if (ops->prepare) { + int ret = ops->prepare(service, epic_name, epic_class, epic_unit); + + if (ret) { + dev_err(ep->dcp->dev, "AFK service %s admission failed: %d\n", + service_name, ret); + afk_service_discard(service); + goto free; + } + } if (ep->wire_v2) { int ret = afk_send_epic(ep, channel, 0, EPIC_TYPE_NOTIFY, EPIC_CAT_REPORT, AFKV2_OPEN, NULL, 0); @@ -395,16 +405,23 @@ static void afk_recv_handle_init(struct apple_dcp_afkep *ep, u32 channel, if (ret) { unsigned long flags; - spin_lock_irqsave(&service->lock, flags); - service->enabled = false; - service->torndown = true; - spin_unlock_irqrestore(&service->lock, flags); + if (ops->prepare) { + afk_service_discard(service); + } else { + spin_lock_irqsave(&service->lock, flags); + service->enabled = false; + service->torndown = true; + spin_unlock_irqrestore(&service->lock, flags); + } dev_err(ep->dcp->dev, "AFKV2 interface %u OPEN failed: %d\n", channel, ret); goto free; } } - ops->init(service, epic_name, epic_class, epic_unit); + if (ops->prepare) + afk_service_publish(service); + if (ops->init) + ops->init(service, epic_name, epic_class, epic_unit); dev_info(ep->dcp->dev, "AFK[ep:%02x]: new service %s on channel %d\n", ep->endpoint, service_name, channel); diff --git a/drivers/gpu/drm/apple/afk.h b/drivers/gpu/drm/apple/afk.h index c6a34fa67399e..b8c889da5c356 100644 --- a/drivers/gpu/drm/apple/afk.h +++ b/drivers/gpu/drm/apple/afk.h @@ -71,6 +71,12 @@ struct apple_epic_service_ops { /* All retained pointers must be pinned before enabling slot reuse. */ bool reusable; + /* + * Optional admission, before OPEN/publication. Must not retain service + * on failure; teardown must accept a failed/partial preparation. + */ + int (*prepare)(struct apple_epic_service *service, const char *name, + const char *class, s64 unit); void (*init)(struct apple_epic_service *service, const char *name, const char *class, s64 unit); int (*call)(struct apple_epic_service *service, u32 idx, @@ -177,7 +183,7 @@ afk_service_reinit(struct apple_epic_service *service, spin_lock_irqsave(&service->lock, flags); reusable = afk_service_reclaimable(service); if (reusable) { - service->enabled = true; + service->enabled = !ops->prepare; service->torndown = false; service->ops = ops; service->ep = ep; @@ -189,6 +195,30 @@ afk_service_reinit(struct apple_epic_service *service, return reusable; } +/* Only used before a prepared service has been published to callers. */ +static inline void afk_service_discard(struct apple_epic_service *service) +{ + unsigned long flags; + + if (service->ops->teardown) + service->ops->teardown(service); + spin_lock_irqsave(&service->lock, flags); + service->enabled = false; + service->torndown = true; + service->cookie = NULL; + service->ops = NULL; + spin_unlock_irqrestore(&service->lock, flags); +} + +static inline void afk_service_publish(struct apple_epic_service *service) +{ + unsigned long flags; + + spin_lock_irqsave(&service->lock, flags); + service->enabled = true; + spin_unlock_irqrestore(&service->lock, flags); +} + struct afk_ringbuffer_header { __le32 bufsz; u32 unk; diff --git a/drivers/gpu/drm/apple/apple_drv.c b/drivers/gpu/drm/apple/apple_drv.c index eda6ea33fa225..d0462690c25c2 100644 --- a/drivers/gpu/drm/apple/apple_drv.c +++ b/drivers/gpu/drm/apple/apple_drv.c @@ -640,16 +640,12 @@ static const struct of_device_id apple_dcp_id_tbl[] = { {}, }; -/* - * The T8140 external display coprocessor needs firmware support this driver - * does not have yet. Leave it out of the display subsystem, so that an - * enabled node cannot hold back the internal panel's component master. - */ +/* An explicitly enabled external provider participates in component matching. */ static bool apple_dcp_usable(struct device_node *np) { return of_device_is_available(np) && (!of_device_is_compatible(np, "apple,t8140-dcpext") || - dcp_h17p_external_ready(np)); + dcp_h17p_external_enabled()); } static int apple_drm_init_dcp(struct device *dev) @@ -670,6 +666,13 @@ static int apple_drm_init_dcp(struct device *dev) of_device_is_compatible(np, "apple,t8140-dcpext") || of_property_present(np, "phys"); + /* component_bind_all() established the matched provider lifetime. */ + if (of_device_is_compatible(np, "apple,t8140-dcpext") && + !dcp_h17p_external_ready(np)) { + of_node_put(np); + return -ENODEV; + } + dcp[num_dcp] = of_find_device_by_node(np); if (!dcp[num_dcp]) continue; @@ -679,8 +682,14 @@ static int apple_drm_init_dcp(struct device *dev) ret = apple_probe_per_dcp(dev, &apple->drm, dcp[num_dcp], num_dcp, dcp_ext, &encoder[num_dcp], &crtc_mask[num_dcp]); - if (ret) + if (ret) { + if (of_device_is_compatible(np, "apple,t8140-dcpext")) { + put_device(&dcp[num_dcp]->dev); + of_node_put(np); + return ret; + } continue; + } num_dcp++; } diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 7edee07d7bb10..4f2b4923135af 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -46,6 +46,7 @@ #include "dcp.h" #include "dcp-internal.h" #include "dcp-external-bw.h" +#include "dptx-service.h" #include "iomfb.h" #include "parser.h" #include "trace.h" @@ -169,12 +170,14 @@ bool dcp_h17p_external_ready(struct device_node *np) pdev = of_find_device_by_node(np); if (!pdev) return false; - /* Failed and deferred probes must never become required components. */ - if (device_is_bound(&pdev->dev)) { - dcp = platform_get_drvdata(pdev); - ready = dcp && READ_ONCE(dcp->external_prepared) && - !READ_ONCE(dcp->quiescing) && !READ_ONCE(dcp->crashed); - } + /* + * Called after component binding. component_add() may synchronously + * bind the master before the platform probe itself returns, so the + * driver core's bound flag is not a readiness condition here. + */ + dcp = platform_get_drvdata(pdev); + ready = dcp && READ_ONCE(dcp->external_prepared) && + !READ_ONCE(dcp->quiescing) && !READ_ONCE(dcp->crashed); put_device(&pdev->dev); return ready; } @@ -2185,7 +2188,7 @@ static int dcp_dptx_connect(struct apple_dcp *dcp, u32 port) dcp->connector_type, dcp->dptxport[port].connected); mutex_lock(&dcp->hpd_mutex); - if (!dcp->dptxport[port].enabled) { + if (!dptx_service_available(&dcp->dptxport[port])) { dev_warn(dcp->dev, "dcp_dptx_connect: dptx service for port %d not enabled\n", port); ret = -ENODEV; goto out_unlock; @@ -2322,7 +2325,7 @@ static void dcp_placeholder_edid_work(struct work_struct *work) mutex_lock(&dcp->hpd_mutex); generation = dcp->placeholder_generation; if (!dcp->typec_cable_connected || !dcp->dptxport[0].connected || - !dcp->dptxport[0].enabled || generation != dcp->typec_generation) + !dptx_service_available(&dcp->dptxport[0]) || generation != dcp->typec_generation) goto out_unlock; service = dcp->dptxport[0].service; @@ -2397,7 +2400,7 @@ 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) { + if (dptx_service_available(&dcp->dptxport[port]) && dcp->dptxport[port].connected) { dptxport_release_display(dcp->dptxport[port].service); dcp->dptxport[port].connected = false; } @@ -2449,7 +2452,7 @@ int dcp_dptx_disconnect_oob(struct platform_device *pdev, u32 port) if (dcp->avep) av_service_disconnect(dcp); - if (dcp->dptxport[port].enabled) + if (dptx_service_available(&dcp->dptxport[port])) dptxport_set_hpd(dcp->dptxport[port].service, false); return dcp_dptx_disconnect(dcp, port); @@ -2601,7 +2604,7 @@ int dcp_start(struct platform_device *pdev) * Since dcp0 is not used at the moment let's avoid this * since it is possibly the cause for startup issues. */ - } else if (dcp->dptxport[0].enabled) { + } else if (dptx_service_available(&dcp->dptxport[0])) { bool connected; /* force disconnect on start - necessary if the display * is already up from m1n1 @@ -2850,7 +2853,7 @@ void dcp_poweroff(struct platform_device *pdev) if (dcp_is_typec_output(dcp)) { /* DCP owns a synthetic HPD for Type-C. Release it with the CRTC. */ - if (dcp->dptxport[0].enabled && dcp->dptxport[0].connected) { + if (dptx_service_available(&dcp->dptxport[0]) && dcp->dptxport[0].connected) { ret = dptxport_set_hpd(dcp->dptxport[0].service, false); if (ret) dev_warn(dcp->dev, diff --git a/drivers/gpu/drm/apple/dptx-service.h b/drivers/gpu/drm/apple/dptx-service.h new file mode 100644 index 0000000000000..f806e3253999a --- /dev/null +++ b/drivers/gpu/drm/apple/dptx-service.h @@ -0,0 +1,63 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ +#ifndef APPLE_DPTX_SERVICE_H +#define APPLE_DPTX_SERVICE_H + +#include "afk.h" +#include "dptxep.h" + +static inline int dptx_service_port(const char *name, const char *class, + s64 unit, bool wire_v2, u32 index, + u32 num_ports) +{ + if (!name || !class || strcmp(name, "dcpdptx-port-epic") || + strcmp(class, "AppleDCPDPTXRemotePort")) + return -EINVAL; + if (wire_v2) + return num_ports == 1 && unit == index ? 0 : -EINVAL; + return unit >= 0 && unit < 2 ? unit : -EINVAL; +} + +static inline bool dptx_service_available(struct dptx_port *port) +{ + /* Pairs with publication before readers use the service and its cookie. */ + return smp_load_acquire(&port->enabled); +} + +static inline int dptx_service_bind(struct apple_epic_service *service, + struct dptx_port *port, s64 unit) +{ + if (port->service) + return -EBUSY; + port->unit = unit; + /* No owner is published until OPEN and AFK publication succeed. */ + service->cookie = port; + return 0; +} + +static inline void dptx_service_publish(struct apple_epic_service *service) +{ + struct dptx_port *port = service->cookie; + + port->service = service; + port->connected = false; + /* Pair with dcp.c readers before they dereference the service cookie. */ + smp_store_release(&port->enabled, true); +} + +static inline void dptx_service_unbind(struct apple_epic_service *service) +{ + struct dptx_port *port = service->cookie; + + if (port && port->service == service) { + WRITE_ONCE(port->enabled, false); + WRITE_ONCE(port->connected, false); + reinit_completion(&port->enable_completion); + } + /* + * Keep the published owner and cookie valid for retained callers. Once + * this session closes, bind fails closed until reboot: + * an in-flight connect must never switch to a new firmware session. + */ +} + +#endif diff --git a/drivers/gpu/drm/apple/dptxep.c b/drivers/gpu/drm/apple/dptxep.c index 6c4012d6ad752..451820389f91c 100644 --- a/drivers/gpu/drm/apple/dptxep.c +++ b/drivers/gpu/drm/apple/dptxep.c @@ -15,6 +15,7 @@ #include "afk.h" #include "dcp.h" #include "dptxep.h" +#include "dptx-service.h" #include "parser.h" #include "trace.h" @@ -823,48 +824,26 @@ static int dptxport_call(struct apple_epic_service *service, u32 idx, } } -static void dptxport_init(struct apple_epic_service *service, const char *name, - const char *class, s64 unit) +static int dptxport_prepare(struct apple_epic_service *service, const char *name, + const char *class, s64 unit) { struct apple_dcp *dcp = service->ep->dcp; - s64 port = unit; - - if (!name || !class) - return; - if (strcmp(name, "dcpdptx-port-epic")) - return; - if (strcmp(class, "AppleDCPDPTXRemotePort")) - return; + int port = dptx_service_port(name, class, unit, service->ep->wire_v2, + dcp->index, dcp->hw.num_dptx_ports); + if (port < 0) + return port; trace_dptxport_init(dcp, unit); - if (service->ep->wire_v2) { - /* The single external service reports its physical DCP unit. */ - if (dcp->hw.num_dptx_ports != 1 || unit != dcp->index) { - dev_err(dcp->dev, "Unexpected external DPTX unit %lld\n", unit); - return; - } - port = 0; - dev_dbg(dcp->dev, "DPTX physical unit %lld maps to logical port 0\n", unit); - } + return dptx_service_bind(service, &dcp->dptxport[port], unit); +} - switch (port) { - case 0: - case 1: - if (service->ep->dcp->dptxport[port].enabled) { - dev_err(service->ep->dcp->dev, - "DPTXPort: unit %lld already exists\n", unit); - return; - } - service->ep->dcp->dptxport[port].unit = unit; - service->ep->dcp->dptxport[port].service = service; - service->ep->dcp->dptxport[port].enabled = true; - service->cookie = (void *)&service->ep->dcp->dptxport[port]; - complete(&service->ep->dcp->dptxport[port].enable_completion); - break; - default: - dev_err(service->ep->dcp->dev, "DPTXPort: invalid unit %lld\n", - unit); - } +static void dptxport_init(struct apple_epic_service *service, const char *name, + const char *class, s64 unit) +{ + struct dptx_port *port = service->cookie; + + dptx_service_publish(service); + complete(&port->enable_completion); } static int dptxport_group_call(struct apple_epic_service *service, u16 group, u32 command, @@ -892,6 +871,8 @@ static int dptxport_group_call(struct apple_epic_service *service, u16 group, u3 static const struct apple_epic_service_ops dptxep_ops[] = { { .name = "AppleDCPDPTXRemotePort", + .prepare = dptxport_prepare, + .teardown = dptx_service_unbind, .init = dptxport_init, .call = dptxport_call, .call_v2 = dptxport_group_call, diff --git a/drivers/phy/apple/atc-j700-dp.h b/drivers/phy/apple/atc-j700-dp.h index 9071587bd5e8d..ead9ea6be3f3a 100644 --- a/drivers/phy/apple/atc-j700-dp.h +++ b/drivers/phy/apple/atc-j700-dp.h @@ -3,11 +3,26 @@ #ifndef APPLE_ATC_J700_DP_H #define APPLE_ATC_J700_DP_H +/* Retry only the idle operations claimed by an incomplete route change. */ +static int j700_dp_idle_pending(struct apple_atcphy *p) +{ + int ret; + + for (unsigned int lane = 0; lane < 2; lane++) { + if (!(p->dp_idle_pmas & BIT(lane))) + continue; + ret = j700_pma_idle(p, lane); + if (ret) + return ret; + p->dp_idle_pmas &= ~BIT(lane); + } + return 0; +} + /* Preserve the upper routing state established by the platform firmware. */ static int j700_dp_route(struct apple_atcphy *p, bool four_lanes) { u32 protocols = four_lanes ? 0x492 : (p->swap_lanes ? 0x252 : 0x489); - int ret; core_mask32(p, 0x60, 0xfff, protocols); if (four_lanes) { @@ -19,11 +34,9 @@ static int j700_dp_route(struct apple_atcphy *p, bool four_lanes) for (unsigned int lane = 0; lane < 2; lane++) { if (!four_lanes && lane != !p->swap_lanes) continue; - ret = j700_pma_idle(p, lane); - if (ret) - return ret; + p->dp_idle_pmas |= BIT(lane); } - return 0; + return j700_dp_idle_pending(p); } /* Register facts from the authorized J700 hardware audit. */ @@ -306,6 +319,9 @@ static int j700_dp_configure(struct apple_atcphy *p, const struct phy_configure_ guard(mutex)(&p->lock); if ((o->set_rate && !o->link_rate) || (o->set_lanes && !o->lanes)) return o->set_voltages ? -EINVAL : j700_dp_stop(p); + /* The last completed mode cannot describe a partially changed lane route. */ + if (p->mode_dirty) + return -EBUSY; if (p->mode != APPLE_ATCPHY_MODE_DP && p->mode != APPLE_ATCPHY_MODE_USB3_DP) return -EINVAL; mode = atcphy_get_mode_config(p, p->mode); diff --git a/drivers/phy/apple/atc.c b/drivers/phy/apple/atc.c index 2ec021c2e2c79..ea04dec0f0d05 100644 --- a/drivers/phy/apple/atc.c +++ b/drivers/phy/apple/atc.c @@ -654,6 +654,8 @@ struct atcphy_hw { * @ss_tunables: The complete SuperSpeed tunable set was supplied * @dp_tunables: The complete T8140 DisplayPort bundle was supplied * @aux_power_requested: AUX power-up began and needs matching shutdown + * @mode_dirty: J700 mode transition needs cleanup before it can be retried + * @dp_idle_pmas: J700 PMAs with an incomplete routing idle sequence * @dp_owned_pmas: J700 PMAs requiring main-link cleanup * @dp_spread_spectrum: J700 main-link spread-spectrum state * @dp_presets: Firmware-derived per-rate drive presets @@ -717,6 +719,8 @@ struct apple_atcphy { bool ss_tunables; bool dp_tunables; bool aux_power_requested; + bool mode_dirty; + u8 dp_idle_pmas; u8 dp_owned_pmas; bool dp_spread_spectrum; u32 dp_presets[5][16]; @@ -2683,28 +2687,43 @@ static int atcphy_configure(struct apple_atcphy *atcphy, enum atcphy_mode mode) u32 reg; lockdep_assert_held(&atcphy->lock); - if (atcphy->hw->has_t8140_dp && atcphy->dp_owned_pmas && mode != atcphy->mode) { + if (atcphy->hw->has_t8140_dp && atcphy->dp_owned_pmas && + (mode != atcphy->mode || atcphy->mode_dirty)) { + atcphy->mode_dirty = true; ret = j700_dp_stop(atcphy); if (ret) return ret; } + if (atcphy->hw->has_t8140_dp && atcphy->mode_dirty) { + ret = j700_dp_idle_pending(atcphy); + if (ret) + return ret; + } if (atcphy->hw->has_t8140_dp && atcphy_modes[mode].enable_dp_aux && !atcphy->dp_tunables) return -EOPNOTSUPP; - if (atcphy->hw->has_t8140_dp && !atcphy_modes[mode].enable_dp_aux && + if (atcphy->hw->has_t8140_dp && + (!atcphy_modes[mode].enable_dp_aux || atcphy->mode_dirty) && atcphy->aux_power_requested) { + atcphy->mode_dirty = true; ret = j700_aux_disable(atcphy); if (ret) return ret; } + /* Keep failed transitions retryable even when the requested mode is unchanged. */ + if (atcphy->hw->has_t8140_dp) + atcphy->mode_dirty = true; /* the mode change below may power the PHY down: stop the tunnel clock first */ atc_tunnel_stop_t8103(atcphy); atc_tunnel_restore(atcphy); if (mode == APPLE_ATCPHY_MODE_OFF) { ret = atcphy_power_off(atcphy); + if (ret) + return ret; atcphy->mode = mode; - return ret; + atcphy->mode_dirty = false; + return 0; } ret = atcphy_power_on(atcphy); @@ -2788,6 +2807,7 @@ static int atcphy_configure(struct apple_atcphy *atcphy, enum atcphy_mode mode) } atcphy->mode = mode; + atcphy->mode_dirty = false; return 0; } @@ -3261,7 +3281,7 @@ static int atcphy_mux_set(struct typec_mux_dev *mux, struct typec_mux_state *sta target_mode = APPLE_ATCPHY_MODE_USB2; dev_dbg(atcphy->dev, "mux target=%u\n", target_mode); - if (atcphy->mode == target_mode) + if (atcphy->mode == target_mode && !atcphy->mode_dirty) return 0; switch (target_mode) { diff --git a/tools/diagnostics/apple-j700-dp-test.c b/tools/diagnostics/apple-j700-dp-test.c index d8e565491f96e..23caf5df299c0 100644 --- a/tools/diagnostics/apple-j700-dp-test.c +++ b/tools/diagnostics/apple-j700-dp-test.c @@ -36,8 +36,8 @@ struct apple_atcphy { struct { void *core; } regs; void *dev; int lock, mode, dp_link_rate; - unsigned int dp_owned_pmas; - bool dp_spread_spectrum, swap_lanes, dp_presets_valid[5]; + unsigned int dp_owned_pmas, dp_idle_pmas; + bool dp_spread_spectrum, swap_lanes, mode_dirty, dp_presets_valid[5]; u32 dp_presets[5][16]; }; diff --git a/tools/diagnostics/test-apple-atc-unwind.py b/tools/diagnostics/test-apple-atc-unwind.py new file mode 100644 index 0000000000000..0494b16fb3bc7 --- /dev/null +++ b/tools/diagnostics/test-apple-atc-unwind.py @@ -0,0 +1,295 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +"""Fault-inject production J700 mode, mux, AUX and PMA transitions (no hardware).""" +import hashlib +import os +from pathlib import Path +import re +import shlex +import subprocess +import tempfile + +root = Path(__file__).resolve().parents[2] +atc = (root / 'drivers/phy/apple/atc.c').read_text() +dp = (root / 'drivers/phy/apple/atc-j700-dp.h').read_text() + + +def block(source, marker): + start = source.index(marker) + opening = source.index('{', start) + end, depth = opening + 1, 1 + while depth: + depth += (source[end] == '{') - (source[end] == '}') + end += 1 + return source[start:end] + + +def function(source, name): + return block(source, re.search(r'^static [^\n]+\b' + name + r'\(', source, re.M).group()) + + +# Use the production mode table and wrappers too, so SAFE mapping and fast paths +# are tested rather than mirrored by a second implementation. +parts = re.findall(r'^#define [^\n]*(?:\\\n[^\n]*)*', atc, re.M) +for name in ('atcphy_generation', 'atcphy_lane_mode', 'atcphy_pipehandler_state', 'atcphy_mode', + 'atcphy_dp_link_rate'): + parts.append(block(atc, 'enum ' + name + ' {') + ';') +for name in ('atcphy_mode_configuration', 'atcphy_hw'): + parts.append(block(atc, 'struct ' + name + ' {') + ';') +parts.append(r''' +struct apple_atcphy { + const struct atcphy_hw *hw; + struct { unsigned char *core, *lpdptx; } regs; + void *dev; + int lock, dp_link_rate; + bool fixed_usb2, host_active, swap_lanes, ss_tunables, dp_tunables; + bool aux_power_requested, mode_dirty, dp_spread_spectrum, dp_presets_valid[5]; + u32 dp_presets[5][16]; + unsigned int dp_idle_pmas, dp_owned_pmas; + enum atcphy_mode mode, typec_mode; + enum atcphy_pipehandler_state pipe_state; +}; +''') +start = atc.index('static const struct {\n\tconst struct atcphy_mode_configuration normal;') +parts.append(atc[start:atc.index('\n};', start) + 3]) +parts.append(r''' +struct phy_configure_opts_dp { + unsigned int link_rate, lanes, voltage[4], pre[4]; + bool set_rate, set_lanes, set_voltages, ssc; +}; +static u32 registers[0x30000 / 4]; +static unsigned int polls, fail_poll, writes, usb2_on; +static u32 readl(void *address) { return *(u32 *)address; } +static void core_mask32(struct apple_atcphy *p, u32 offset, u32 mask, u32 value) +{ + (void)p; + assert(offset < sizeof(registers) && offset % 4 == 0); + registers[offset / 4] = (registers[offset / 4] & ~mask) | value; + writes++; + if (offset == ATCPHY_POWER_CTRL) + registers[ATCPHY_POWER_STAT / 4] = registers[offset / 4]; + if (offset == 0x2000) { + if (value == 0x10000001) + registers[offset / 4] |= BIT(1); + else { + registers[offset / 4] &= ~BIT(1); + registers[0x7034 / 4] &= ~BIT(3); + } + } +} +static void core_set32(struct apple_atcphy *p, u32 o, u32 v) { core_mask32(p, o, 0, v); } +static void core_clear32(struct apple_atcphy *p, u32 o, u32 v) { core_mask32(p, o, v, 0); } +static void writel(u32 value, void *address) { *(u32 *)address = value; } +static void mask32(void *p, u32 m, u32 v) { * (u32 *)p = (*(u32 *)p & ~m) | v; } +static void set32(void *p, u32 v) { mask32(p, 0, v); } +static void clear32(void *p, u32 v) { mask32(p, v, 0); } +#define readl_poll_timeout_atomic(address, value, condition, interval, timeout) \ + ({ (value) = readl(address); polls++; \ + polls == fail_poll || !(condition) ? -ETIMEDOUT : 0; }) +#define readl_poll_timeout readl_poll_timeout_atomic +static void atcphy_usb2_power_on(struct apple_atcphy *p) { (void)p; usb2_on++; } +static void atcphy_apply_tunables(struct apple_atcphy *p, enum atcphy_mode mode) +{ (void)p; (void)mode; } +static void atc_tunnel_stop_t8103(struct apple_atcphy *p) { (void)p; } +static void atc_tunnel_restore(struct apple_atcphy *p) { (void)p; } +static void atcphy_park_pipehandler(struct apple_atcphy *p) +{ p->pipe_state = ATCPHY_PIPEHANDLER_STATE_DUMMY; } +static int atcphy_configure_pipehandler(struct apple_atcphy *p, bool host) +{ (void)host; p->pipe_state = atcphy_modes[p->mode].pipehandler_state; return 0; } +''') +for name in ('atcphy_get_mode_config', 'j700_field_update', 'j700_pma_idle'): + parts.append(function(atc, name)) +parts.append(dp) +for name in ('atcphy_configure_lanes', 'j700_aux_enable', 'j700_aux_disable', + 'atcphy_enable_dp_aux', 'atcphy_disable_dp_aux', 'atcphy_power_off', + 'atcphy_power_on', 'atcphy_configure', 'atcphy_mux_set'): + parts.append(function(atc, name)) +production = '\n'.join(parts) +print('Production transition SHA256:', hashlib.sha256(production.encode()).hexdigest()) +prefix = r''' +#include +#include +#include +#include +#include +#include +typedef uint32_t u32; +typedef uint16_t u16; +#define BIT(n) (1U << (n)) +#define GENMASK(h, l) ((~0U << (l)) & (~0U >> (31 - (h)))) +#define FIELD_PREP(m, v) (((u32)(v) << __builtin_ctz(m)) & (m)) +#define FIELD_GET(m, v) (((v) & (m)) >> __builtin_ctz(m)) +#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) +#define hweight32(v) __builtin_popcount(v) +#define dev_info_once(...) ((void)0) +#define dev_dbg(...) ((void)0) +#define dev_warn(...) ((void)0) +static void report(void *dev, const char *fmt, ...) { (void)dev; (void)fmt; } +#define dev_err report +#define lockdep_assert_held(...) ((void)0) +#define WARN_ON_ONCE(v) assert(!(v)) +#define guard(kind) (void) +#define fallthrough __attribute__((fallthrough)) +#define ndelay(n) ((void)(n)) +#define udelay(n) ((void)(n)) +#define usleep_range(a, b) ((void)(a), (void)(b)) +enum { TYPEC_STATE_SAFE, TYPEC_STATE_USB, TYPEC_MODE_USB4, + TYPEC_DP_STATE_C, TYPEC_DP_STATE_D, TYPEC_DP_STATE_E }; +enum { USB_TYPEC_TBT_SID = 0x8087, USB_TYPEC_DP_SID = 0xff01 }; +#define EUDO_USB_MODE_MASK 3U +enum { EUDO_USB_MODE_USB2, EUDO_USB_MODE_USB3, EUDO_USB_MODE_USB4 }; +struct enter_usb_data { u32 eudo; }; +struct typec_altmode { unsigned int svid; }; +struct typec_mux_state { unsigned long mode; struct typec_altmode *alt; void *data; }; +struct typec_mux_dev { struct apple_atcphy *data; }; +#define typec_mux_get_drvdata(mux) ((mux)->data) +''' +suffix = r''' +static struct atcphy_hw hw = { + .has_t8140_dp = true, .gen = ATCPHY_GENERATION_T8122, + .aciophy_lane_mode = 0x60, .aciophy_crossbar = 0x64, +}; +static void initial(struct apple_atcphy *p, bool fixed, bool swapped) +{ + memset(registers, 0, sizeof(registers)); + registers[AUS_COMMON_DIG_RCAL1 / 4] = AUS_COMMON_DIG_RCAL1_ALL_CODES_DONE; + *p = (struct apple_atcphy) { + .hw = &hw, .regs.core = (unsigned char *)registers, + .regs.lpdptx = (unsigned char *)registers, + .fixed_usb2 = fixed, .swap_lanes = swapped, + .ss_tunables = true, .dp_tunables = true, .dp_presets_valid = { true }, + .mode = fixed ? APPLE_ATCPHY_MODE_USB2 : APPLE_ATCPHY_MODE_OFF, + .pipe_state = ATCPHY_PIPEHANDLER_STATE_DUMMY, + }; + polls = fail_poll = writes = usb2_on = 0; +} +static int safe(struct apple_atcphy *p) +{ + struct typec_mux_dev mux = { p }; + struct typec_mux_state state = { .mode = TYPEC_STATE_SAFE }; + return atcphy_mux_set(&mux, &state); +} +int main(void) +{ + struct apple_atcphy p; + unsigned int cases = 0; + for (unsigned int fixed = 0; fixed < 2; fixed++) { + for (unsigned int swapped = 0; swapped < 2; swapped++) { + const enum atcphy_mode modes[] = { + APPLE_ATCPHY_MODE_DP, APPLE_ATCPHY_MODE_USB3_DP, APPLE_ATCPHY_MODE_USB3, + }; + for (unsigned int m = 0; m < ARRAY_SIZE(modes); m++) { + unsigned int aux = modes[m] != APPLE_ATCPHY_MODE_USB3 ? 4 : 0; + unsigned int idle = modes[m] == APPLE_ATCPHY_MODE_DP ? 12 : 6; + for (unsigned int fault = 3; fault <= 2 + aux + idle; fault++) { + enum atcphy_mode old = fixed ? APPLE_ATCPHY_MODE_USB2 : APPLE_ATCPHY_MODE_OFF; + initial(&p, fixed, swapped); + fail_poll = fault; + assert(atcphy_configure(&p, modes[m]) == -ETIMEDOUT); + assert(p.mode == old && p.mode_dirty); + if (fault <= 2 + aux) + assert(p.aux_power_requested && !p.dp_idle_pmas); + else + assert(p.dp_idle_pmas); + bool owned_aux = p.aux_power_requested; + unsigned int owned_idle = p.dp_idle_pmas; + polls = 0; + fail_poll = 1; + usb2_on = 0; + assert(safe(&p) == -ETIMEDOUT); + assert(p.mode == old && p.mode_dirty); + assert(p.aux_power_requested == owned_aux && p.dp_idle_pmas == owned_idle); + assert(usb2_on == 0); /* Cleanup failure must not continue into power changes. */ + polls = fail_poll = 0; + assert(safe(&p) == 0); + assert(p.mode == old && !p.mode_dirty); + assert(!p.aux_power_requested && !p.dp_idle_pmas && !p.dp_owned_pmas); + assert(usb2_on == fixed); /* Fixed-hub SAFE restores USB2, never powers it off. */ + polls = writes = 0; + assert(safe(&p) == 0 && polls == 0 && writes == 0); + cases++; + } + } + } + } + /* A failed route change must not expose the old DP mode to link training. */ + initial(&p, false, false); + p.mode = APPLE_ATCPHY_MODE_DP; + p.aux_power_requested = true; + p.dp_owned_pmas = 3; + assert(atcphy_configure(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + unsigned int last_idle_poll = polls - 1; /* The final poll is RCAL, after route idle. */ + initial(&p, false, false); + p.mode = APPLE_ATCPHY_MODE_DP; + p.aux_power_requested = true; + p.dp_owned_pmas = 3; + fail_poll = last_idle_poll; + assert(atcphy_configure(&p, APPLE_ATCPHY_MODE_USB3_DP) == -ETIMEDOUT); + assert(p.mode == APPLE_ATCPHY_MODE_DP && p.mode_dirty && p.dp_idle_pmas); + struct phy_configure_opts_dp rate = { + .set_rate = true, .set_lanes = true, .link_rate = 8100, .lanes = 4, + }; + struct phy_configure_opts_dp training = { .set_voltages = true, .lanes = 4 }; + unsigned int writes_before = writes, idle_before = p.dp_idle_pmas; + unsigned int owned_before = p.dp_owned_pmas; + assert(j700_dp_configure(&p, &rate) == -EBUSY); + assert(j700_dp_configure(&p, &training) == -EBUSY); + assert(writes == writes_before && p.dp_idle_pmas == idle_before); + assert(p.dp_owned_pmas == owned_before && p.aux_power_requested && p.mode_dirty); + /* Explicit stop is allowed, but must leave the route cleanup obligation. */ + struct phy_configure_opts_dp stop = { .set_rate = true }; + assert(j700_dp_configure(&p, &stop) == 0); + assert(p.dp_idle_pmas == idle_before && p.mode_dirty); + fail_poll = polls = 0; + assert(safe(&p) == 0 && !p.mode_dirty && !p.dp_idle_pmas && !p.aux_power_requested); + /* If teardown itself fails, reject training without dropping live ownership. */ + initial(&p, false, false); + p.mode = APPLE_ATCPHY_MODE_DP; + p.aux_power_requested = true; + p.dp_owned_pmas = 3; + fail_poll = 1; + assert(atcphy_configure(&p, APPLE_ATCPHY_MODE_USB3_DP) == -ETIMEDOUT); + assert(p.mode_dirty && p.dp_owned_pmas == 3); + writes_before = writes; + assert(j700_dp_configure(&p, &rate) == -EBUSY); + assert(j700_dp_configure(&p, &training) == -EBUSY); + assert(writes == writes_before && p.dp_owned_pmas == 3 && p.aux_power_requested); + fail_poll = polls = 0; + assert(safe(&p) == 0 && !p.mode_dirty && !p.dp_owned_pmas && !p.aux_power_requested); + /* A cleanup failure also dirties an initially successful mode transition. */ + initial(&p, false, false); + p.mode = APPLE_ATCPHY_MODE_DP; + p.aux_power_requested = true; + fail_poll = 1; + assert(safe(&p) == -ETIMEDOUT && p.mode_dirty && p.aux_power_requested); + fail_poll = polls = 0; + assert(safe(&p) == 0 && !p.mode_dirty && !p.aux_power_requested); + /* Existing main-link ownership survives a failed cleanup of a dirty mode. */ + initial(&p, true, false); + p.mode_dirty = p.aux_power_requested = true; + p.dp_owned_pmas = 3; + fail_poll = 1; + assert(safe(&p) == -ETIMEDOUT); + assert(p.mode_dirty && p.aux_power_requested && p.dp_owned_pmas == 3); + fail_poll = polls = 0; + assert(safe(&p) == 0 && !p.mode_dirty && !p.aux_power_requested && !p.dp_owned_pmas); + /* OFF is not committed when domain sleep fails, and SAFE retries it. */ + initial(&p, false, false); + p.mode = APPLE_ATCPHY_MODE_USB2; + fail_poll = 1; + assert(safe(&p) == -ETIMEDOUT && p.mode == APPLE_ATCPHY_MODE_USB2 && p.mode_dirty); + fail_poll = polls = 0; + assert(safe(&p) == 0 && p.mode == APPLE_ATCPHY_MODE_OFF && !p.mode_dirty); + printf("PASS: %u AUX/PMA fault scenarios, failed cleanup, dirty training guard, SAFE retry, fixed USB2, OFF commit\n", cases); + return 0; +} +''' +with tempfile.TemporaryDirectory(prefix='apple-atc-unwind-') as directory: + path = Path(directory) + unit = path / 'test.c' + unit.write_text(prefix + production + suffix) + command = shlex.split(os.environ.get('CC', 'clang')) + command += ['-std=gnu11', '-Wall', '-Wextra', '-Werror', '-O1', '-g', + '-fsanitize=address,undefined', '-fno-omit-frame-pointer'] + subprocess.run(command + [str(unit), '-o', str(path / 'test')], check=True) + subprocess.run([str(path / 'test')], check=True) diff --git a/tools/diagnostics/test-apple-dcp-readiness.py b/tools/diagnostics/test-apple-dcp-readiness.py new file mode 100644 index 0000000000000..3d6e4cb5593e0 --- /dev/null +++ b/tools/diagnostics/test-apple-dcp-readiness.py @@ -0,0 +1,246 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +"""Exercise production DCP matching/readiness and publication against host stubs.""" +import argparse +import hashlib +import os +from pathlib import Path +import shlex +import subprocess +import tempfile + +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument('--source-root', type=Path, default=Path(__file__).resolve().parents[2]) +args = parser.parse_args() +dcp = (args.source_root / 'drivers/gpu/drm/apple/dcp.c').read_text() +drm = (args.source_root / 'drivers/gpu/drm/apple/apple_drv.c').read_text() + + +def function(source, marker): + start = source.index(marker) + opening = source.index('{', start) + end, depth = opening + 1, 1 + while depth: + depth += (source[end] == '{') - (source[end] == '}') + end += 1 + return source[start:end] + + +# This slice is the production probe's publication boundary, including its +# failure exits and prepared-before-component_add ordering, not a copied model. +probe = function(dcp, 'static int dcp_platform_probe(') +publish = probe[probe.rindex('\tif (dcp->hw.iomfb_method_profile ==\n\t DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) {'):] +production = '\n'.join(( + function(dcp, 'bool dcp_h17p_external_ready('), + function(drm, 'static bool apple_dcp_usable('), + function(drm, 'static int add_dcp_components('), + function(drm, 'static int apple_drm_init_dcp('), + 'static int publish_provider(struct platform_device *pdev, struct apple_dcp *dcp)\n' + '{\n\tstruct device *dev = &pdev->dev;\n\tint ret;\n' + publish, +)) +print('Production readiness/publication SHA256:', hashlib.sha256(production.encode()).hexdigest()) +prefix = r''' +#include +#include +#include +#include +#include +#include +typedef uint32_t u32; +typedef uint64_t u64; +#define READ_ONCE(x) (x) +#define WRITE_ONCE(x, v) ((x) = (v)) +#define IS_ENABLED(x) 0 +#define MAX_COPROCESSORS 4 +#define DL_FLAG_AUTOREMOVE_SUPPLIER 0 +#define DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL 2 +#define dev_warn(...) ((void)0) +#define msleep(x) ((void)(x)) +#define component_compare_of 0 +#define apple_dcp_id_tbl 0 +static int dcp_comp_ops; +struct device_node { bool available, external, phys; unsigned int index; }; +struct device { struct device_node *of_node; void *data; }; +struct platform_device { struct device dev; bool bound, registered; }; +struct apple_dcp { + bool external_prepared, quiescing, crashed; + struct { int iomfb_method_profile; } hw; +}; +struct drm_device { int unused; }; +struct drm_encoder { int unused; }; +struct apple_drm_private { struct drm_device drm; }; +struct component_match { bool includes[2]; }; +static struct device_node nodes[2]; +static struct platform_device devices[2]; +static struct apple_dcp dcps[2]; +static struct apple_drm_private priv; +static struct device master = { .data = &priv }; +static struct component_match matches; +static bool enabled, absent_external; +static unsigned int references, released, node_puts, probes[2], bind_attempts, disables; +static int preflight_error, external_probe_error, bind_result; +static void try_bind(void); +static bool dcp_h17p_external_enabled(void) { return enabled; } +static bool of_device_is_available(struct device_node *n) { return n->available; } +static bool of_device_is_compatible(struct device_node *n, const char *s) +{ + return n->external && (!strcmp(s, "apple,t8140-dcpext") || !strcmp(s, "apple,dcpext")); +} +static bool of_property_present(struct device_node *n, const char *s) +{ assert(!strcmp(s, "phys")); return n->phys; } +#define for_each_matching_node(np, table) \ + for (unsigned int node_i = 0; node_i < 2 && ((np) = &nodes[node_i]); node_i++) +#define for_each_endpoint_of_node(np, endpoint) \ + for ((endpoint) = NULL; (endpoint) != NULL; (endpoint) = NULL) +static struct device_node *of_graph_get_remote_port_parent(struct device_node *n) +{ return n; } +static void of_node_put(struct device_node *n) { (void)n; node_puts++; } +static struct platform_device *of_find_device_by_node(struct device_node *n) +{ + if (n->external && absent_external) + return NULL; + references++; + return &devices[n->index]; +} +static void *platform_get_drvdata(struct platform_device *p) { return p->dev.data; } +static void *dev_get_drvdata(struct device *d) { return d->data; } +static void put_device(struct device *d) { (void)d; released++; } +/* Deliberately provided: using this during synchronous bind is a regression. */ +#define device_is_bound(dev) (devices[(dev)->of_node->index].bound) +static void drm_of_component_match_add(struct device *d, struct component_match **m, + int compare, struct device_node *n) +{ (void)d; (void)compare; *m = &matches; matches.includes[n->index] = true; } +static void device_link_add(struct device *a, struct device *b, int flags) +{ (void)a; (void)b; (void)flags; } +static int apple_probe_per_dcp(struct device *d, struct drm_device *drm, + struct platform_device *p, int index, bool external, + struct drm_encoder **encoder, u32 *mask) +{ + (void)d; (void)drm; (void)index; (void)encoder; (void)mask; + assert(external == p->dev.of_node->external); + probes[p->dev.of_node->index]++; + return external ? external_probe_error : 0; +} +static int apple_probe_typec_ports(struct drm_device *d, struct platform_device **p, + u32 *masks, int count) +{ (void)d; (void)p; (void)masks; (void)count; return 0; } +static int dcp_start(struct platform_device *p) { (void)p; return 0; } +static int dcp_wait_ready(struct platform_device *p, u64 timeout) +{ (void)p; (void)timeout; return 0; } +static u64 get_jiffies_64(void) { return 0; } +static u64 msecs_to_jiffies(unsigned int ms) { return ms; } +static bool time_after_eq64(u64 a, u64 b) { return a >= b; } +static int dcp_comp_bind(struct device *d, void *master_data, void *data) +{ (void)d; (void)master_data; (void)data; return preflight_error; } +static void dcp_disable_typec_work(struct apple_dcp *d, bool quiesce) +{ disables++; d->quiescing = quiesce; } +static int component_add(struct device *d, const void *ops) +{ + (void)ops; + struct platform_device *p = &devices[d->of_node->index]; + assert(!p->bound); + p->registered = true; + try_bind(); /* Linux component_add may bind the master before probe returns. */ + return 0; +} +''' +suffix = r''' +static void try_bind(void) +{ + for (unsigned int i = 0; i < 2; i++) + if (matches.includes[i] && !devices[i].registered) + return; + bind_attempts++; + bind_result = apple_drm_init_dcp(&master); +} +static void initial(bool opt_in) +{ + memset(nodes, 0, sizeof(nodes)); + memset(devices, 0, sizeof(devices)); + memset(dcps, 0, sizeof(dcps)); + memset(&matches, 0, sizeof(matches)); + memset(probes, 0, sizeof(probes)); + for (unsigned int i = 0; i < 2; i++) { + nodes[i] = (struct device_node){ .available = true, .external = i == 1, .index = i }; + devices[i].dev = (struct device){ .of_node = &nodes[i], .data = &dcps[i] }; + } + dcps[1].hw.iomfb_method_profile = DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL; + devices[0].registered = devices[0].bound = true; + enabled = opt_in; + absent_external = false; + references = released = node_puts = bind_attempts = disables = 0; + preflight_error = external_probe_error = bind_result = 0; +} +static int match_providers(void) +{ + struct component_match *match = NULL; + return add_dcp_components(&master, &match); +} +int main(void) +{ + /* Default internal display does not wait for an unsupported external DCP. */ + initial(false); + assert(match_providers() == 1 && matches.includes[0] && !matches.includes[1]); + assert(!dcp_h17p_external_ready(&nodes[1]) && references == 0); + try_bind(); + assert(bind_attempts == 1 && bind_result == 0 && probes[0] == 1 && !probes[1]); + + /* Delayed external provider remains in the match; the master must wait. */ + initial(true); + assert(match_providers() == 2 && matches.includes[1]); + try_bind(); + assert(bind_attempts == 0 && !probes[0] && !probes[1]); + /* Last provider is prepared, but driver core bound is still false here. */ + assert(publish_provider(&devices[1], &dcps[1]) == 0); + assert(dcps[1].external_prepared && !devices[1].bound); + assert(bind_attempts == 1 && bind_result == 0 && probes[0] == 1 && probes[1] == 1); + devices[1].bound = true; + + /* Permanent preflight failure cannot silently drop a requested provider. */ + initial(true); + assert(match_providers() == 2); + preflight_error = -EIO; + assert(publish_provider(&devices[1], &dcps[1]) == -EIO); + assert(!devices[1].registered && !dcps[1].external_prepared && disables == 1); + try_bind(); + assert(matches.includes[1] && bind_attempts == 0 && !probes[0]); + + /* An unavailable DT node is excluded; a matched but absent device is fatal. */ + initial(true); + nodes[1].available = false; + assert(!apple_dcp_usable(&nodes[1]) && match_providers() == 1); + initial(true); + absent_external = true; + assert(match_providers() == 2); + assert(apple_drm_init_dcp(&master) == -ENODEV && !probes[1]); + + /* Readiness rejects every unavailable provider state and releases lookup refs. */ + for (unsigned int state = 0; state < 4; state++) { + initial(true); + dcps[1].external_prepared = true; + if (state == 0) devices[1].dev.data = NULL; + if (state == 1) dcps[1].external_prepared = false; + if (state == 2) dcps[1].quiescing = true; + if (state == 3) dcps[1].crashed = true; + assert(!dcp_h17p_external_ready(&nodes[1])); + assert(references == 1 && released == 1); + assert(apple_drm_init_dcp(&master) == -ENODEV && !probes[1]); + } + initial(true); + dcps[1].external_prepared = true; + external_probe_error = -EIO; + assert(apple_drm_init_dcp(&master) == -EIO); + assert(probes[1] == 1 && node_puts == 1); + puts("PASS: opt-in matching, delayed/preflight-failed providers, synchronous publication, readiness rejection"); + return 0; +} +''' +with tempfile.TemporaryDirectory(prefix='apple-dcp-readiness-') as directory: + path = Path(directory) + unit = path / 'test.c' + unit.write_text(prefix + production + suffix) + command = shlex.split(os.environ.get('CC', 'clang')) + command += ['-std=gnu11', '-Wall', '-Wextra', '-Werror', '-O1', '-g', + '-fsanitize=address,undefined', '-fno-omit-frame-pointer'] + subprocess.run(command + [str(unit), '-o', str(path / 'test')], check=True) + subprocess.run([str(path / 'test')], check=True) From cf6198d365e9060bc5f4516926b50e0d10600667 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 19:56:39 +1100 Subject: [PATCH 52/52] tests: publish standalone AFK admission regression harness Make the host-extracted admission/lifetime checks reproducible alongside the kernel KUnit suite, with repository-relative source selection and an optional source-root override. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- MAINTAINERS | 1 + tools/diagnostics/test-apple-afk-admission.py | 118 ++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 tools/diagnostics/test-apple-afk-admission.py diff --git a/MAINTAINERS b/MAINTAINERS index e7b22a9c9b774..9e9fbe808e1bc 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -2018,6 +2018,7 @@ F: rust/kernel/soc/apple/aop.rs F: sound/soc/apple/t8140_aop_audio.rs F: tools/diagnostics/apple-dptx-training-test.c F: tools/diagnostics/apple-j700-dp-test.c +F: tools/diagnostics/test-apple-afk-admission.py F: tools/diagnostics/test-apple-atc-unwind.py F: tools/diagnostics/test-apple-dcp-readiness.py F: tools/diagnostics/test-apple-j700-crossbar.py diff --git a/tools/diagnostics/test-apple-afk-admission.py b/tools/diagnostics/test-apple-afk-admission.py new file mode 100644 index 0000000000000..fc5669048bdc9 --- /dev/null +++ b/tools/diagnostics/test-apple-afk-admission.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Compile the actual inline admission helpers against host-only kernel mocks.""" +from pathlib import Path +import subprocess +import tempfile +import argparse + +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument("--source-root", type=Path, + default=Path(__file__).resolve().parents[2], + help="kernel source tree (defaults to this script's tree)") +root = parser.parse_args().source_root.resolve() +def extract(file, name): + s = (root / 'drivers/gpu/drm/apple' / file).read_text() + endname = s.index(name + '(') + start = s.rfind('static inline', 0, endname) + brace = s.index('{', endname) + depth = 1 + end = brace + 1 + while depth: + depth += (s[end] == '{') - (s[end] == '}') + end += 1 + return s[start:end] +preamble = r''' +#include +#include +#include +#include +#include +#include +typedef int64_t s64; +typedef uint32_t u32; +#define MAX_PENDING_CMDS 16 +#define spin_lock_irqsave(lock, flags) ((void)(lock), (flags)=0) +#define spin_unlock_irqrestore(lock, flags) ((void)(lock), (void)(flags)) +#define WARN_ON(x) (x) +#define smp_store_release(p,v) (*(p)=(v)) +#define smp_load_acquire(p) (*(p)) +#define WRITE_ONCE(p,v) ((p)=(v)) +#define bitmap_empty(p, n) (!*(p)) +struct apple_epic_service; +struct apple_dcp_afkep { int unused; }; +struct apple_epic_service_ops { + bool reusable; + int (*prepare)(struct apple_epic_service *,const char *,const char *,s64); + void (*teardown)(struct apple_epic_service *); +}; +struct apple_epic_service { + int lock; unsigned int users; u32 channel; bool enabled, torndown; + void *cookie; const struct apple_epic_service_ops *ops; + struct apple_dcp_afkep *ep; unsigned long cmd_map[1]; +}; +struct dptx_port { + bool enabled, connected; int enable_completion; u32 unit; + struct apple_epic_service *service; +}; +static void reinit_completion(int *p) { *p = 0; } +''' +functions = [('afk.h', n) for n in ['afk_service_get','afk_service_put','afk_service_matches','afk_service_reclaimable','afk_service_reinit','afk_service_discard','afk_service_publish']] +functions += [('dptx-service.h', n) for n in ['dptx_service_port','dptx_service_available','dptx_service_bind','dptx_service_publish','dptx_service_unbind']] +body = r''' +static int prepare(struct apple_epic_service *s,const char *n,const char *c,s64 u) { return 0; } +int main(void) { + const char *n="dcpdptx-port-epic", *c="AppleDCPDPTXRemotePort"; + assert(dptx_service_port(n,c,3,true,3,1)==0); + assert(dptx_service_port(n,c,0,true,3,1)==-EINVAL); + assert(dptx_service_port(n,c,-1,true,3,1)==-EINVAL); + assert(dptx_service_port(n,c,3,true,3,2)==-EINVAL); + assert(dptx_service_port(NULL,c,3,true,3,1)==-EINVAL); + assert(dptx_service_port(n,NULL,3,true,3,1)==-EINVAL); + assert(dptx_service_port("bad",c,3,true,3,1)==-EINVAL); + assert(dptx_service_port(n,"bad",3,true,3,1)==-EINVAL); + assert(dptx_service_port(n,c,1,false,0,1)==1); + struct apple_epic_service_ops ops={.prepare=prepare,.teardown=dptx_service_unbind}; + struct apple_epic_service_ops legacy={0}; + struct apple_epic_service a={0}, b={0}, old={0}; struct dptx_port p={0}; + assert(afk_service_reinit(&old,NULL,&legacy,8)); + assert(afk_service_matches(&old,8)); + assert(afk_service_reinit(&a,NULL,&ops,1)); + assert(!afk_service_get(&a) && !afk_service_matches(&a,1)); + afk_service_discard(&a); + assert(afk_service_reinit(&a,NULL,&ops,1)); + assert(dptx_service_bind(&a,&p,3)==0); + assert(!dptx_service_available(&p) && !p.service && a.cookie==&p); + assert(!afk_service_get(&a)); + afk_service_discard(&a); /* failed OPEN */ + assert(!p.enabled && !p.service); + assert(afk_service_reinit(&a,NULL,&ops,1)); + assert(dptx_service_bind(&a,&p,3)==0); + afk_service_publish(&a); + assert(!p.enabled && !p.service); + dptx_service_publish(&a); + assert(dptx_service_available(&p) && p.service==&a && p.service->cookie==&p); + assert(afk_service_get(&a)==&a); afk_service_put(&a); + assert(afk_service_reinit(&b,NULL,&ops,2)); + assert(dptx_service_bind(&b,&p,3)==-EBUSY); + afk_service_discard(&b); assert(p.enabled && p.service==&a); + p.connected=true; + a.enabled=false; a.torndown=true; dptx_service_unbind(&a); + assert(!p.enabled && !p.connected && a.cookie==&p); + assert(!afk_service_reinit(&a,NULL,&ops,3)); + assert(afk_service_reinit(&b,NULL,&ops,3)); + assert(dptx_service_bind(&b,&p,3)==-EBUSY); + afk_service_discard(&b); + assert(p.service==&a && !afk_service_get(p.service) && !p.enabled); + assert(afk_service_reinit(&b,NULL,&ops,4)); + afk_service_publish(&b); assert(!b.cookie && afk_service_get(&b)==&b); + afk_service_put(&b); + return 0; +} +''' +with tempfile.TemporaryDirectory() as d: + source=Path(d)/'test.c' + source.write_text(preamble+'\n'.join(extract(*x) for x in functions)+body) + subprocess.run(['cc','-std=gnu11','-Wall','-Werror',str(source),'-o',str(Path(d)/'test')],check=True) + subprocess.run([str(Path(d)/'test')],check=True) +print('PASS: host-extracted admission/identity/lifetime assertions (kernel mocks, no target kernel build)')