diff --git a/Documentation/devicetree/bindings/display/apple,dcp.yaml b/Documentation/devicetree/bindings/display/apple,dcp.yaml index bba5d64f50451..0fe31267f99e1 100644 --- a/Documentation/devicetree/bindings/display/apple,dcp.yaml +++ b/Documentation/devicetree/bindings/display/apple,dcp.yaml @@ -320,7 +320,7 @@ allOf: properties: compatible: contains: - const: apple,t8140-dcp + enum: ['apple,t8140-dcp', 'apple,t8140-dcpext'] then: properties: apple,power-lut-data-x: false diff --git a/MAINTAINERS b/MAINTAINERS index f3d453a49eb90..cc6fe5b42f38a 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -2016,25 +2016,8 @@ 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/display-attributes-fixture.json -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-dptx-readiness.py -F: tools/diagnostics/test-apple-external-lifecycle.py -F: tools/diagnostics/test-apple-display-metadata*.py -F: tools/diagnostics/test-apple-display-trace.py -F: tools/diagnostics/test-apple-external-sdr.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 -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/ +F: tools/testing/selftests/drivers/gpu/apple-dcp/ APPLETALK NETWORK LAYER L: netdev@vger.kernel.org @@ -2655,8 +2638,6 @@ 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: 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/arch/arm64/boot/dts/apple/Makefile b/arch/arm64/boot/dts/apple/Makefile index 27cb75f1e2856..06e84a336b5b0 100644 --- a/arch/arm64/boot/dts/apple/Makefile +++ b/arch/arm64/boot/dts/apple/Makefile @@ -111,5 +111,4 @@ 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-dcpext.dtsi b/arch/arm64/boot/dts/apple/t8140-j700-dcpext.dtsi index 3b8be06ed22f5..69d6eff9aa756 100644 --- a/arch/arm64/boot/dts/apple/t8140-j700-dcpext.dtsi +++ b/arch/arm64/boot/dts/apple/t8140-j700-dcpext.dtsi @@ -1,8 +1,6 @@ // 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 remains disabled until the loader validates and preserves * the inherited firmware and its complete dependency group. diff --git a/arch/arm64/boot/dts/apple/t8140-j700-external-test.dts b/arch/arm64/boot/dts/apple/t8140-j700-external-test.dts deleted file mode 100644 index 5b9b3d78c8160..0000000000000 --- a/arch/arm64/boot/dts/apple/t8140-j700-external-test.dts +++ /dev/null @@ -1,3 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0+ OR MIT -/* Compatibility alias: external display is now part of the ordinary J700 tree. */ -#include "t8140-j700.dts" diff --git a/drivers/gpu/drm/apple/Makefile b/drivers/gpu/drm/apple/Makefile index 601689ee3b172..6b3fbdbc55aa6 100644 --- a/drivers/gpu/drm/apple/Makefile +++ b/drivers/gpu/drm/apple/Makefile @@ -6,7 +6,7 @@ appledrm-y := apple_drv.o appledrm-y += afk.o dcp.o dcp_backlight.o dptxep.o iomfb.o parser.o systemep.o appledrm-$(CONFIG_DRM_APPLE_AUDIO) += audio.o appledrm-$(CONFIG_DRM_APPLE_AUDIO) += av.o -appledrm-y += connector.o dcp-display-metadata.o +appledrm-y += connector.o dcp-display-metadata.o dcp-180hz.o appledrm-y += ibootep.o appledrm-y += iomfb_v12_3.o appledrm-y += iomfb_v13_3.o diff --git a/drivers/gpu/drm/apple/afk.c b/drivers/gpu/drm/apple/afk.c index 50f6e796d7908..0d598980c96e4 100644 --- a/drivers/gpu/drm/apple/afk.c +++ b/drivers/gpu/drm/apple/afk.c @@ -52,6 +52,21 @@ enum rbep_msg_type { #define SEND_WPTR GENMASK(31, 0) +static void afk_service_worker(struct work_struct *work); +static void afk_discard_pending_open(struct apple_epic_service *service); + +void afk_service_retry(struct apple_dcp_afkep *ep) +{ + unsigned long flags; + + if (!ep || !ep->wire_v2 || ep != ep->dcp->dcpavservep) + return; + spin_lock_irqsave(&ep->lock, flags); + if (!ep->stopping) + queue_work(ep->wq, &ep->service_work); + spin_unlock_irqrestore(&ep->lock, flags); +} + static void afk_send(struct apple_dcp_afkep *ep, u64 message) { dcp_send_message(ep->dcp, ep->endpoint, message); @@ -62,8 +77,10 @@ static void afk_release_context(void *data) struct apple_dcp_afkep *afkep = data; afk_quiesce(afkep); - if (!afkep->dcp->retain_dma) + if (!afkep->dcp->retain_dma) { + kvfree(afkep->command_tags); kfree(afkep); + } } struct apple_dcp_afkep *afk_init(struct apple_dcp *dcp, u32 endpoint, @@ -86,6 +103,7 @@ struct apple_dcp_afkep *afk_init(struct apple_dcp *dcp, u32 endpoint, goto out_free_afkep; } + INIT_WORK(&afkep->service_work, afk_service_worker); init_completion(&afkep->started); init_completion(&afkep->stopped); spin_lock_init(&afkep->lock); @@ -104,12 +122,21 @@ struct apple_dcp_afkep *afk_init(struct apple_dcp *dcp, u32 endpoint, void afk_quiesce(struct apple_dcp_afkep *afkep) { + unsigned long flags; + u32 i; + if (IS_ERR_OR_NULL(afkep)) return; + spin_lock_irqsave(&afkep->lock, flags); + afkep->stopping = true; + spin_unlock_irqrestore(&afkep->lock, flags); if (afkep->wq) { destroy_workqueue(afkep->wq); afkep->wq = NULL; } + for (i = 0; i < afkep->num_channels; i++) + if (afkep->services[i].pending_open) + afk_discard_pending_open(&afkep->services[i]); debugfs_remove_recursive(afkep->debugfs_entry); afkep->debugfs_entry = NULL; } @@ -140,6 +167,14 @@ int afk_start(struct apple_dcp_afkep *ep) { int ret; + /* Allocate before firmware can announce services or issue commands. */ + if (ep->wire_v2 && !ep->command_tags) { + ep->command_tags = kvcalloc(AFKV2_INTERFACES, sizeof(*ep->command_tags), + GFP_KERNEL); + if (!ep->command_tags) + return -ENOMEM; + } + reinit_completion(&ep->started); ret = apple_rtkit_start_ep(ep->dcp->rtk, ep->endpoint); if (ret) @@ -148,7 +183,7 @@ int afk_start(struct apple_dcp_afkep *ep) 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) + DCP_IOMFB_METHODS_H17P_EXTERNAL) 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); @@ -158,37 +193,6 @@ int afk_start(struct apple_dcp_afkep *ep) return 0; } -/* Read only host-owned ring headers; never consume an entry or notify firmware. */ -void afk_trace_ring_snapshot(struct apple_dcp_afkep *ep) -{ - unsigned long flags; - u32 rx_rptr = 0, rx_wptr = 0, tx_rptr = 0, tx_wptr = 0; - bool rx_ready, tx_ready; - - if (!ep || !ep->wire_v2 || !trace_afk_ring_snapshot_enabled()) - return; - spin_lock_irqsave(&ep->lock, flags); - if (ep->stopping) { - spin_unlock_irqrestore(&ep->lock, flags); - return; - } - rx_ready = READ_ONCE(ep->rxbfr.ready) && ep->rxbfr.rptr && ep->rxbfr.wptr; - tx_ready = READ_ONCE(ep->txbfr.ready) && ep->txbfr.rptr && ep->txbfr.wptr; - dma_rmb(); - if (rx_ready) { - rx_rptr = le32_to_cpu(READ_ONCE(*ep->rxbfr.rptr)); - rx_wptr = le32_to_cpu(READ_ONCE(*ep->rxbfr.wptr)); - } - if (tx_ready) { - tx_rptr = le32_to_cpu(READ_ONCE(*ep->txbfr.rptr)); - tx_wptr = le32_to_cpu(READ_ONCE(*ep->txbfr.wptr)); - } - trace_afk_ring_snapshot(ep, rx_ready, rx_rptr, rx_wptr, - READ_ONCE(ep->rxbfr.bufsz), tx_ready, tx_rptr, - tx_wptr, READ_ONCE(ep->txbfr.bufsz)); - spin_unlock_irqrestore(&ep->lock, flags); -} - static void afk_getbuf(struct apple_dcp_afkep *ep, u64 message) { u32 size = FIELD_GET(GETBUF_SIZE, message) << BLOCK_SHIFT; @@ -341,12 +345,74 @@ 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); +/* Pending identities are owned only by the ordered endpoint workqueue. */ +static void afk_discard_pending_open(struct apple_epic_service *service) +{ + service->pending_open = false; + kfree(service->pending_name); + kfree(service->pending_class); + service->pending_name = NULL; + service->pending_class = NULL; + afk_service_discard(service); +} + +static void afk_retry_service_init(struct apple_dcp_afkep *ep) +{ + u32 i; + + for (i = 0; i < ep->num_channels; i++) { + struct apple_epic_service *service = &ep->services[i]; + const struct apple_epic_service_ops *ops = service->ops; + int ret; + + if (!service->pending_open) + continue; + ret = ops->revalidate(service, service->pending_name, + service->pending_class, service->pending_unit); + if (ret) { + afk_discard_pending_open(service); + continue; + } + /* Old replies keep their original slot, tag and buffers until drained. */ + if (afk_epic_find_service(ep, service->channel)) + continue; + ret = afk_send_epic(ep, service->channel, 0, EPIC_TYPE_NOTIFY, + EPIC_CAT_REPORT, AFKV2_OPEN, NULL, 0); + if (ret) { + dev_err(ep->dcp->dev, "AFKV2 interface %u deferred OPEN failed: %d\n", + service->channel, ret); + afk_discard_pending_open(service); + continue; + } + service->pending_open = false; + afk_service_publish(service); + ops->init(service, service->pending_name, service->pending_class, + service->pending_unit); + if (service->enabled) + afk_populate_service_debugfs(service); + kfree(service->pending_name); + kfree(service->pending_class); + service->pending_name = NULL; + service->pending_class = NULL; + } +} + +static void afk_service_worker(struct work_struct *work) +{ + struct apple_dcp_afkep *ep = container_of(work, struct apple_dcp_afkep, service_work); + + if (!READ_ONCE(ep->stopping)) + afk_retry_service_init(ep); +} + static void afk_recv_handle_init(struct apple_dcp_afkep *ep, u32 channel, u8 *payload, size_t payload_size) { char name[32]; s64 epic_unit = -1; - struct apple_epic_service *service = NULL; + struct apple_epic_service *service = NULL, *previous; + unsigned long flags; + bool retired = false; u32 ch_idx; const char *service_name = name; const char *epic_name = NULL, *epic_class = NULL; @@ -355,8 +421,25 @@ static void afk_recv_handle_init(struct apple_dcp_afkep *ep, u32 channel, u8 *props = payload + sizeof(name); size_t props_size = payload_size - sizeof(name); - if (afk_epic_find_service(ep, channel)) - return; + previous = afk_epic_find_service(ep, channel); + if (previous) { + spin_lock_irqsave(&previous->lock, flags); + retired = previous->torndown; + spin_unlock_irqrestore(&previous->lock, flags); + if (!ep->wire_v2 || ep != ep->dcp->dcpavservep || + !retired || !previous->ops->revalidate) + return; + } + for (ch_idx = 0; ch_idx < ep->num_channels; ch_idx++) { + struct apple_epic_service *pending = &ep->services[ch_idx]; + + if (!pending->pending_open || pending->channel != channel) + continue; + if (!pending->ops->revalidate(pending, pending->pending_name, + pending->pending_class, pending->pending_unit)) + return; + afk_discard_pending_open(pending); + } if (payload_size < sizeof(name)) { dev_err(ep->dcp->dev, "AFK[ep:%02x]: payload too small: %lx\n", @@ -423,12 +506,26 @@ static void afk_recv_handle_init(struct apple_dcp_afkep *ep, u32 channel, 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); + if (ops->optional && ret == -EOPNOTSUPP) + dev_info(ep->dcp->dev, + "AFK optional %s (%s) is unsupported on channel %u\n", + ops->name, service_name, channel); + else + dev_err(ep->dcp->dev, "AFK service %s admission failed: %d\n", + service_name, ret); afk_service_discard(service); goto free; } } + if (previous && retired && ops->revalidate) { + /* Preserve the admitted identity and its original provider ticket. */ + service->pending_open = true; + service->pending_name = epic_name; + service->pending_class = epic_class; + service->pending_unit = epic_unit; + afk_service_retry(ep); + return; + } if (ep->wire_v2) { int ret = afk_send_epic(ep, channel, 0, EPIC_TYPE_NOTIFY, EPIC_CAT_REPORT, AFKV2_OPEN, NULL, 0); @@ -469,6 +566,14 @@ static void afk_recv_handle_teardown(struct apple_dcp_afkep *ep, u32 channel) const struct apple_epic_service_ops *ops; unsigned long flags; + /* Accepted CLOSE/CLOSED supersedes unpublished recovery for this interface. */ + for (u32 i = 0; i < ep->num_channels; i++) { + struct apple_epic_service *pending = &ep->services[i]; + + if (pending->pending_open && pending->channel == channel) + afk_discard_pending_open(pending); + } + service = afk_epic_find_service(ep, channel); if (!service) { dev_warn(ep->dcp->dev, "AFK[ep:%02x]: teardown for disabled channel %u\n", @@ -672,7 +777,7 @@ static void afk_recv_handle_std_service(struct apple_dcp_afkep *ep, u32 channel, "AFK[ep:%02x]: channel %d received unhandled standard service message: %x / %x\n", ep->endpoint, channel, type, eshdr->category); if (ep->dcp->hw.iomfb_method_profile != - DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) + DCP_IOMFB_METHODS_H17P_EXTERNAL) print_hex_dump(KERN_INFO, "AFK: ", DUMP_PREFIX_NONE, 16, 1, payload, payload_size, true); } @@ -950,8 +1055,6 @@ static void afk_recv_handle(struct apple_dcp_afkep *ep, u32 channel, u32 type, u8 *payload; size_t payload_size; - trace_afk_recv_header(ep, channel, type, data_size, data); - if (ep->wire_v2) { afk_v2_receive(ep, channel, type, data, data_size); return; @@ -1060,7 +1163,7 @@ 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); if (ep->dcp->hw.iomfb_method_profile != - DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) + DCP_IOMFB_METHODS_H17P_EXTERNAL) print_hex_dump(KERN_INFO, "AFK: ", DUMP_PREFIX_NONE, 16, 1, payload, payload_size, true); return; @@ -1245,38 +1348,16 @@ static int afk_send_epic_token(struct apple_dcp_afkep *ep, u32 channel, u16 tag, 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; - /* - * H17P frames EPIC messages differently (see afk_recv_handle()): an - * 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 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 && !compact && !ep->wire_v2) { + /* External H17P services use AFKV2; the internal profile has no EPIC sender. */ + if (ep->dcp->fw_compat == DCP_FIRMWARE_H17P && !ep->wire_v2) { 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; - if (ep->wire_v2) { if (!channel || channel > U16_MAX || tag > U8_MAX || payload_len > SZ_1M) return -EINVAL; @@ -1322,7 +1403,7 @@ static int afk_send_epic_token(struct apple_dcp_afkep *ep, u32 channel, u16 tag, goto out; } - if ((compact || ep->wire_v2) && + if (ep->wire_v2 && (!ep->txbfr.ready || !ep->txbfr.rptr || !ep->txbfr.wptr || !ep->txbfr.buf)) { ret = -ESHUTDOWN; goto out; @@ -1333,11 +1414,11 @@ static int afk_send_epic_token(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 || ep->wire_v2) ? sizeof(struct epic_sub_hdr_h17p) : sizeof(*eshdr)) + + (ep->wire_v2 ? sizeof(struct epic_sub_hdr_h17p) : sizeof(*eshdr)) + payload_len; total_size = sizeof(*hdr) + total_epic_size; - if (compact || ep->wire_v2) { + if (ep->wire_v2) { u32 available, needed; if (!ep->txbfr.ready || !ep->txbfr.stride || @@ -1440,7 +1521,7 @@ static int afk_send_epic_token(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 || ep->wire_v2) ? 0 : channel); + hdr->channel = cpu_to_le32(ep->wire_v2 ? 0 : channel); hdr->type = cpu_to_le32(ep->wire_v2 ? 0 : etype); if (hdr2) memcpy(hdr2, hdr, sizeof(*hdr)); @@ -1454,18 +1535,6 @@ static int afk_send_epic_token(struct apple_dcp_afkep *ep, u32 channel, u16 tag, if (ret) goto out; wptr += written; - } else 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)); @@ -1526,6 +1595,23 @@ int afk_send_command(struct apple_epic_service *service, u8 type, MSEC_PER_SEC); } +static u16 afk_command_tag(struct apple_epic_service *service, u8 slot) +{ + struct apple_dcp_afkep *ep = service->ep; + unsigned long flags; + u16 tag; + + lockdep_assert_held(&service->lock); + if (!ep->wire_v2) + return ((service->cmd_tag++ & 0xff) << 8) | slot; + + /* Send already nests the endpoint lock below the service lock. */ + spin_lock_irqsave(&ep->lock, flags); + tag = afkv2_command_id(ep->command_tags[service->channel]++, slot); + spin_unlock_irqrestore(&ep->lock, flags); + return tag; +} + static int afk_send_command_timeout_len(struct apple_epic_service *service, u8 type, const void *payload, size_t payload_len, void *output, size_t output_len, u32 *retcode, @@ -1562,6 +1648,11 @@ static int afk_send_command_timeout_len(struct apple_epic_service *service, u8 t if (!service) return -ENODEV; ep = service->ep; + if (ep->wire_v2 && (!service->channel || service->channel >= AFKV2_INTERFACES || + !ep->command_tags)) { + ret = -ENODEV; + goto err_put_service; + } if (READ_ONCE(ep->dcp->quiescing)) { ret = -ENODEV; goto err_put_service; @@ -1617,9 +1708,7 @@ static int afk_send_command_timeout_len(struct apple_epic_service *service, u8 t goto err_unlock; } - tag = ep->wire_v2 ? afkv2_command_id(service->cmd_tag, idx) : - ((service->cmd_tag & 0xff) << 8) | idx; - service->cmd_tag++; + tag = afk_command_tag(service, idx); service->cmds[idx].tag = tag; service->cmds[idx].type = type; diff --git a/drivers/gpu/drm/apple/afk.h b/drivers/gpu/drm/apple/afk.h index 686b5e1881779..0304410bd6e82 100644 --- a/drivers/gpu/drm/apple/afk.h +++ b/drivers/gpu/drm/apple/afk.h @@ -19,10 +19,13 @@ #define AFK_MAX_CHANNEL 16 #define MAX_PENDING_CMDS 16 +#define AFKV2_INTERFACES BIT(16) struct apple_epic_service_ops; struct apple_dcp_afkep; +void afk_service_retry(struct apple_dcp_afkep *ep); + struct epic_cmd_info { u16 tag; u8 type; @@ -47,6 +50,7 @@ struct apple_epic_service { struct epic_cmd_info cmds[MAX_PENDING_CMDS]; DECLARE_BITMAP(cmd_map, MAX_PENDING_CMDS); + /* Legacy EPIC sequence; AFKV2 history belongs to the interface. */ u8 cmd_tag; spinlock_t lock; /* Pins held by owners and synchronous callers; protected by lock. */ @@ -60,6 +64,11 @@ struct apple_epic_service { struct { u64 generation, epoch; } admission; void *cookie; + /* Admitted reannouncement waiting for the previous channel to drain. */ + bool pending_open; + const char *pending_name, *pending_class; + s64 pending_unit; + struct { struct dentry *entry; u8 *scratch; @@ -72,6 +81,8 @@ struct apple_epic_service_ops { const char name[32]; /* All retained pointers must be pinned before enabling slot reuse. */ bool reusable; + /* EOPNOTSUPP from prepare means intentional absence only for this role. */ + bool optional; /* * Optional admission, before OPEN/publication. Must not retain service @@ -79,6 +90,9 @@ struct apple_epic_service_ops { */ int (*prepare)(struct apple_epic_service *service, const char *name, const char *class, s64 unit); + /* Check the original prepare ticket before a deferred OPEN. */ + int (*revalidate)(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, @@ -139,6 +153,7 @@ static inline void afk_service_put(struct apple_epic_service *service) if (!WARN_ON(!service->users)) service->users--; spin_unlock_irqrestore(&service->lock, flags); + afk_service_retry(service->ep); } static inline void afk_service_disable(struct apple_epic_service *service) @@ -202,7 +217,7 @@ afk_service_reinit(struct apple_epic_service *service, service->ep = ep; service->channel = channel; service->cookie = NULL; - /* Keep the command-tag sequence across service generations. */ + /* Keep legacy slot history; AFKV2 history belongs to the interface. */ } spin_unlock_irqrestore(&service->lock, flags); return reusable; @@ -344,7 +359,10 @@ struct apple_dcp_afkep { bool wire_v2; /* External H17P DPAV only; other endpoints retain owned-DMA commands. */ bool inline_v2; + /* Fixed 16-bit interface domain; survives slot and channel reuse. */ + u8 *command_tags; struct workqueue_struct *wq; + struct work_struct service_work; struct completion started; struct completion stopped; @@ -377,7 +395,6 @@ struct apple_dcp_afkep *afk_init(struct apple_dcp *dcp, u32 endpoint, 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_trace_ring_snapshot(struct apple_dcp_afkep *ep); void afk_shutdown(struct apple_dcp_afkep *ep); void afk_quiesce(struct apple_dcp_afkep *ep); int afk_receive_message(struct apple_dcp_afkep *ep, u64 message); diff --git a/drivers/gpu/drm/apple/apple_drv.c b/drivers/gpu/drm/apple/apple_drv.c index 9a98818391aa2..54f8d52c864e5 100644 --- a/drivers/gpu/drm/apple/apple_drv.c +++ b/drivers/gpu/drm/apple/apple_drv.c @@ -643,12 +643,12 @@ static const struct of_device_id apple_dcp_id_tbl[] = { {}, }; -/* An explicitly enabled external provider participates in component matching. */ +/* A supported firmware-described 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_enabled()); + dcp_h17p_external_supported(np)); } static int apple_drm_init_dcp(struct device *dev) diff --git a/drivers/gpu/drm/apple/dcp-180hz-policy.h b/drivers/gpu/drm/apple/dcp-180hz-policy.h new file mode 100644 index 0000000000000..a221c0e4a0773 --- /dev/null +++ b/drivers/gpu/drm/apple/dcp-180hz-policy.h @@ -0,0 +1,101 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ +#ifndef APPLE_DCP_180HZ_POLICY_H +#define APPLE_DCP_180HZ_POLICY_H + +#include +#include +#include + +/* Only this qualified uncompressed configuration is admitted. */ +static inline bool dcp_180hz_payload(u32 rate, u32 lanes, u32 clock) +{ + return rate == 0x1e && lanes == 2 && clock == 414090 && + (u64)clock * 24 <= (u64)8100000 * lanes * 8 / 10; +} + +static inline bool dcp_180hz_dtd(const u8 *d, const struct drm_display_mode *m) +{ + int ha = d[2] | (d[4] & 0xf0) << 4; + int hb = d[3] | (d[4] & 0x0f) << 8; + int va = d[5] | (d[7] & 0xf0) << 4; + int vb = d[6] | (d[7] & 0x0f) << 8; + int hf = d[8] | (d[11] & 0xc0) << 2; + int hs = d[9] | (d[11] & 0x30) << 4; + int vf = (d[10] >> 4) | (d[11] & 0x0c) << 2; + int vs = (d[10] & 0x0f) | (d[11] & 0x03) << 4; + u32 flags = (d[17] & 2 ? DRM_MODE_FLAG_PHSYNC : DRM_MODE_FLAG_NHSYNC) | + (d[17] & 4 ? DRM_MODE_FLAG_PVSYNC : DRM_MODE_FLAG_NVSYNC); + + return ((d[0] | d[1] << 8) * 10 == m->clock) && m->clock == 414090 && + (d[17] & 0x98) == 0x18 && ha == 1920 && va == 1080 && + ha + hb == 2080 && va + vb == 1106 && + m->hdisplay == ha && m->htotal == ha + hb && + m->hsync_start == ha + hf && m->hsync_end == ha + hf + hs && + m->vdisplay == va && m->vtotal == va + vb && + m->vsync_start == va + vf && m->vsync_end == va + vf + vs && + m->flags == flags; +} + +/* Bounded actual EDID only; no serial field is consulted. */ +static inline bool dcp_180hz_edid(const u8 *p, size_t len, + const struct drm_display_mode *mode) +{ + static const u8 header[] = { 0, 255, 255, 255, 255, 255, 255, 0 }; + bool name = false, timing = false, forum = false; + unsigned int names = 0, i, off, end; + + if (!p || !mode || len != 256 || memcmp(p, header, 8) || p[126] != 1 || + p[8] != 0x04 || p[9] != 0x72 || p[10] != 0x4f || p[11] != 0x0b || + p[18] != 1 || p[19] < 3 || p[19] > 4 || !(p[20] & 0x80)) + return false; + for (off = 0; off < len; off += 128) { + u8 sum = 0; + + for (i = 0; i < 128; i++) + sum += p[off + i]; + if (sum) + return false; + } + for (off = 54; off + 18 <= 126; off += 18) { + const u8 *d = p + off; + + if (d[0] || d[1]) { + timing |= dcp_180hz_dtd(d, mode); + } else if (!d[2] && d[3] == 0xfc && !d[4]) { + unsigned int n = 13; + + names++; + while (n && (d[5 + n - 1] == ' ' || d[5 + n - 1] == '\n' || + d[5 + n - 1] == 0)) + n--; + name = (n == 8 && !memcmp(d + 5, "QG270 S3", 8)) || + (n == 7 && !memcmp(d + 5, "QG270S3", 7)); + } + } + p += 128; + if (p[0] != 2 || p[1] < 3 || p[2] < 4 || p[2] > 127) + return false; + end = p[2]; + for (off = 4; off < end;) { + const u8 *db = p + off; + unsigned int size = (db[0] & 31) + 1; + + if (size > end - off) + return false; + if ((db[0] >> 5) == 3 && size >= 4 && + db[1] == 0xd8 && db[2] == 0x5d && db[3] == 0xc4) { + /* HF-VSDB overrides the older HDMI VSDB clock, as drm_edid does. */ + if (forum || size < 8 || db[4] != 1 || !(db[6] & 0x80) || + (u32)db[5] * 5000 < (u32)mode->clock) + return false; + forum = true; + } + off += size; + } + for (off = end; off + 18 <= 127; off += 18) + if (p[off] || p[off + 1]) + timing |= dcp_180hz_dtd(p + off, mode); + return name && names == 1 && timing && forum; +} + +#endif diff --git a/drivers/gpu/drm/apple/dcp-180hz.c b/drivers/gpu/drm/apple/dcp-180hz.c new file mode 100644 index 0000000000000..8eb4166aa6dbb --- /dev/null +++ b/drivers/gpu/drm/apple/dcp-180hz.c @@ -0,0 +1,260 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +#include +#include +#include +#include "connector.h" +#include "afk.h" +#include "dcp.h" +#include "parser.h" +#include "dcp-180hz.h" +#include "dcp-180hz-policy.h" + +static bool dcp_180hz_direct_port(struct apple_epic_service *service) +{ + struct apple_dcp *dcp; + struct dptx_port *port; + + if (!service || !service->ep || !service->cookie) + return false; + dcp = service->ep->dcp; + port = service->cookie; + return dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL && + service->ep->endpoint == DPTX_ENDPOINT_V2 && service->ep->wire_v2 && + port == &dcp->dptxport[0] && READ_ONCE(port->enabled) && + !READ_ONCE(port->closed) && READ_ONCE(port->service) == service && + READ_ONCE(service->enabled) && port->atcphy && port->atcphy == dcp->phy && + dcp->active_typec_route && !dcp->dptx_tunnel && !dcp_is_usb4_output(dcp); +} + +void dcp_180hz_invalidate(struct apple_dcp *dcp) +{ + unsigned long flags; + + spin_lock_irqsave(&dcp->metadata.lock, flags); + dcp->rate_proof.sequence++; + dcp->rate_proof.rate.valid = false; + dcp->rate_proof.lanes.valid = false; + dcp->rate_proof.retries = 0; + spin_unlock_irqrestore(&dcp->metadata.lock, flags); +} + +struct dcp_180hz_token dcp_180hz_config_begin(struct apple_epic_service *service, bool lanes) +{ + struct apple_dcp *dcp = service->ep->dcp; + struct dcp_180hz_token token = {}; + unsigned long flags; + + /* RX callbacks never acquire lifecycle/HPD/AV mutexes. */ + spin_lock_irqsave(&dcp->metadata.lock, flags); + token.generation = READ_ONCE(dcp->typec_generation); + token.sequence = dcp->rate_proof.sequence; + token.connection_sequence = dcp->rate_proof.connection_sequence; + token.phy = dcp->phy; + token.route = dcp->active_typec_route; + token.service = service; + token.valid = !READ_ONCE(dcp->quiescing) && dcp_180hz_direct_port(service) && + READ_ONCE(dcp->typec_cable_connected) && + token.generation == dcp->metadata.epoch.generation; + if (lanes) + dcp->rate_proof.lanes.valid = false; + else + dcp->rate_proof.rate.valid = false; + spin_unlock_irqrestore(&dcp->metadata.lock, flags); + return token; +} + +/* Preserve the receipt-start registry ticket even when it awaited OPEN. */ +static bool dcp_180hz_timing_epoch(const struct dcp_display_mode *mode, u64 epoch) +{ + if (mode->automatic_service_pending) + return mode->unsafe_rgb8_service_epoch != U64_MAX && + epoch == mode->unsafe_rgb8_service_epoch + 1; + return epoch && mode->unsafe_rgb8_service_epoch == epoch; +} + +void dcp_180hz_config_complete(struct apple_epic_service *service, bool lanes, + struct dcp_180hz_token token, u32 value, bool success) +{ + struct apple_dcp *dcp = service->ep->dcp; + struct dcp_180hz_fact *fact; + bool notify = false; + unsigned long flags; + + if (!success) { + dcp_180hz_invalidate(dcp); + return; + } + spin_lock_irqsave(&dcp->metadata.lock, flags); + if (token.valid && token.service == service && + token.sequence == dcp->rate_proof.sequence && + token.generation == READ_ONCE(dcp->typec_generation) && + token.connection_sequence == dcp->rate_proof.connection_sequence && + READ_ONCE(dcp->typec_cable_connected) && + token.phy == dcp->phy && token.route == dcp->active_typec_route && + dcp_180hz_direct_port(service) && !READ_ONCE(dcp->quiescing)) { + fact = lanes ? &dcp->rate_proof.lanes : &dcp->rate_proof.rate; + *fact = (struct dcp_180hz_fact) { .token = token, .value = value, .valid = true }; + notify = dcp->rate_proof.rate.valid && dcp->rate_proof.lanes.valid; + dcp->rate_proof.retries = 0; + } + spin_unlock_irqrestore(&dcp->metadata.lock, flags); + if (notify) + dcp_180hz_notify(dcp); +} + +static bool dcp_180hz_fact_current(struct apple_dcp *dcp, const struct dcp_180hz_fact *fact) +{ + const struct dcp_180hz_token *t = &fact->token; + + return fact->valid && t->generation == dcp->typec_generation && + t->sequence == dcp->rate_proof.sequence && + t->connection_sequence == dcp->rate_proof.connection_sequence && t->phy == dcp->phy && + t->route == dcp->active_typec_route && t->service == dcp->dptxport[0].service; +} + +/* Catalog withdrawal needs a DRM refresh even when no exception is ready. */ +void dcp_180hz_catalog_changed(struct apple_dcp *dcp) +{ + struct apple_connector *con = READ_ONCE(dcp->connector); + + if (dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17P_EXTERNAL || + !con || READ_ONCE(dcp->quiescing) || !READ_ONCE(con->connected) || + READ_ONCE(con->dcp) != to_platform_device(dcp->dev) || + READ_ONCE(con->base.registration_state) != DRM_CONNECTOR_REGISTERED) + return; + schedule_work(&con->hotplug_wq); +} + +/* Timing and configuration can arrive in either order, before AV admission. */ +void dcp_180hz_notify(struct apple_dcp *dcp) +{ + struct apple_connector *con = READ_ONCE(dcp->connector); + unsigned long flags; + bool ready; + + spin_lock_irqsave(&dcp->metadata.lock, flags); + ready = dcp->metadata.epoch.connected && READ_ONCE(dcp->typec_cable_connected) && + !READ_ONCE(dcp->quiescing) && READ_ONCE(dcp->nr_modes) && + dcp_180hz_direct_port(dcp->dptxport[0].service) && + dcp_180hz_fact_current(dcp, &dcp->rate_proof.rate) && + dcp_180hz_fact_current(dcp, &dcp->rate_proof.lanes) && + dcp_180hz_payload(dcp->rate_proof.rate.value, dcp->rate_proof.lanes.value, 414090); + spin_unlock_irqrestore(&dcp->metadata.lock, flags); + if (ready && con && READ_ONCE(con->connected) && READ_ONCE(dcp->dptxport[0].connected) && + READ_ONCE(con->dcp) == to_platform_device(dcp->dev) && + READ_ONCE(con->base.registration_state) == DRM_CONNECTOR_REGISTERED) + schedule_work(&con->hotplug_wq); +} + +bool dcp_180hz_mode_current(struct apple_dcp *dcp, const struct dcp_display_mode *mode) +{ + struct apple_connector *con = dcp->connector; + const struct edid *raw; + + lockdep_assert_held(&dcp->hpd_mutex); + lockdep_assert_held(&dcp->dcpavserv.lock); + lockdep_assert_held(&dcp->metadata.lock); + if (!con || dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17P_EXTERNAL || + !dcp_180hz_direct_port(dcp->dptxport[0].service) || + !mode->unsafe_rgb8_ticket.associated || + mode->unsafe_rgb8_ticket.generation != dcp->typec_generation || + mode->automatic_connection_sequence != dcp->rate_proof.connection_sequence || + mode->automatic_catalog_sequence != dcp->rate_proof.catalog_sequence || + !dcp->metadata.epoch.connected || !dcp->typec_cable_connected || dcp->quiescing || + READ_ONCE(con->dcp) != to_platform_device(dcp->dev) || + !dcpavserv_edid_service_current(dcp, con->drm_edid_service_epoch) || + !dcp_180hz_timing_epoch(mode, dcp->dcpavserv.epoch) || + !con->drm_edid || con->drm_edid_firmware_derived || + READ_ONCE(con->edid_invalidation_pending) || + con->drm_edid_generation != dcp->typec_generation || + !dcp->dptxport[0].connected || !dcp->dptxport[0].enabled || dcp->dptxport[0].closed || + !dcp_180hz_fact_current(dcp, &dcp->rate_proof.rate) || + !dcp_180hz_fact_current(dcp, &dcp->rate_proof.lanes) || + !dcp_180hz_payload(dcp->rate_proof.rate.value, dcp->rate_proof.lanes.value, mode->mode.clock) || + mode->sdr_rgb.depth != 8 || mode->sdr_rgb.format != DCP_COLOR_FORMAT_RGB || + mode->sdr_rgb.range != DCP_COLOR_YCBCR_RANGE_FULL || + mode->sdr_rgb.eotf != DCP_EOTF_SDR_GAMMA || mode->sdr_rgb.colorimetry != DCP_COLORIMETRY_SRGB) + return false; + raw = drm_edid_raw(con->drm_edid); + /* The transport validates extent/checksums; this quirk accepts only its 256-byte form. */ + return raw && raw->extensions == 1 && + dcp_180hz_edid((const u8 *)raw, 256, &mode->mode); +} + +bool dcp_180hz_pending(struct apple_dcp *dcp, const struct dcp_display_mode *mode) +{ + struct apple_connector *con = dcp->connector; + const struct edid *raw; + + if (!con || !dcp->active_typec_route || dcp_is_usb4_output(dcp) || dcp->dptx_tunnel || + !mode->unsafe_rgb8_ticket.associated || + mode->unsafe_rgb8_ticket.generation != dcp->typec_generation || + mode->automatic_connection_sequence != dcp->rate_proof.connection_sequence || + mode->automatic_catalog_sequence != dcp->rate_proof.catalog_sequence || + (!dcp_180hz_timing_epoch(mode, dcp->dcpavserv.epoch) && + !(mode->automatic_service_pending && !dcp->dcpavserv.enabled && + mode->unsafe_rgb8_service_epoch == dcp->dcpavserv.epoch))) + return false; + if (!con->drm_edid) + return true; + raw = drm_edid_raw(con->drm_edid); + return raw && raw->extensions == 1 && dcp_180hz_edid((const u8 *)raw, 256, &mode->mode); +} + +static void dcp_180hz_retry_work(struct work_struct *work) +{ + struct apple_dcp *dcp = container_of(to_delayed_work(work), struct apple_dcp, rate_retry_wq); + unsigned long flags; + bool current_generation; + + guard(mutex)(&dcp->hpd_mutex); + spin_lock_irqsave(&dcp->metadata.lock, flags); + current_generation = dcp->typec_cable_connected && !dcp->quiescing && + dcp->rate_proof.retry_generation == dcp->typec_generation; + spin_unlock_irqrestore(&dcp->metadata.lock, flags); + if (current_generation && dcp->connector && + READ_ONCE(dcp->connector->dcp) == to_platform_device(dcp->dev)) + schedule_work(&dcp->connector->hotplug_wq); +} + +void dcp_180hz_retry(struct apple_dcp *dcp) +{ + unsigned long flags; + bool retry = false; + + if (!dcp->rate_retry_initialized || READ_ONCE(dcp->quiescing)) + return; + spin_lock_irqsave(&dcp->metadata.lock, flags); + if (dcp->rate_proof.retry_generation != READ_ONCE(dcp->typec_generation)) { + dcp->rate_proof.retry_generation = READ_ONCE(dcp->typec_generation); + dcp->rate_proof.retries = 0; + } + if (READ_ONCE(dcp->typec_cable_connected) && dcp->rate_proof.retries < 8) { + dcp->rate_proof.retries++; + retry = true; + } + spin_unlock_irqrestore(&dcp->metadata.lock, flags); + if (retry) + mod_delayed_work(system_freezable_wq, &dcp->rate_retry_wq, msecs_to_jiffies(300)); +} + +static void dcp_180hz_cancel(void *data) +{ + struct apple_dcp *dcp = data; + + WRITE_ONCE(dcp->rate_retry_initialized, false); + cancel_delayed_work_sync(&dcp->rate_retry_wq); +} + +int dcp_180hz_init(struct apple_dcp *dcp) +{ + int ret; + + if (dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17P_EXTERNAL) + return 0; + INIT_DELAYED_WORK(&dcp->rate_retry_wq, dcp_180hz_retry_work); + ret = devm_add_action_or_reset(dcp->dev, dcp_180hz_cancel, dcp); + if (!ret) + dcp->rate_retry_initialized = true; + return ret; +} diff --git a/drivers/gpu/drm/apple/dcp-180hz.h b/drivers/gpu/drm/apple/dcp-180hz.h new file mode 100644 index 0000000000000..4995dae4cf648 --- /dev/null +++ b/drivers/gpu/drm/apple/dcp-180hz.h @@ -0,0 +1,34 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ +#ifndef APPLE_DCP_180HZ_H +#define APPLE_DCP_180HZ_H +#include +struct apple_dcp; +struct apple_epic_service; +struct dcp_display_mode; +struct dcp_180hz_token { + /* Successful physical configuration is independent of AV discovery. */ + u64 generation, sequence, connection_sequence; + const void *phy, *route, *service; + bool valid; +}; +struct dcp_180hz_fact { + struct dcp_180hz_token token; + u32 value; + bool valid; +}; +struct dcp_180hz_proof { + u64 sequence, connection_sequence, catalog_sequence, retry_generation; + unsigned int retries; + struct dcp_180hz_fact rate, lanes; +}; +struct dcp_180hz_token dcp_180hz_config_begin(struct apple_epic_service *service, bool lanes); +void dcp_180hz_config_complete(struct apple_epic_service *service, bool lanes, + struct dcp_180hz_token token, u32 value, bool success); +void dcp_180hz_invalidate(struct apple_dcp *dcp); +void dcp_180hz_notify(struct apple_dcp *dcp); +void dcp_180hz_catalog_changed(struct apple_dcp *dcp); +bool dcp_180hz_mode_current(struct apple_dcp *dcp, const struct dcp_display_mode *mode); +bool dcp_180hz_pending(struct apple_dcp *dcp, const struct dcp_display_mode *mode); +void dcp_180hz_retry(struct apple_dcp *dcp); +int dcp_180hz_init(struct apple_dcp *dcp); +#endif diff --git a/drivers/gpu/drm/apple/dcp-external-bw.h b/drivers/gpu/drm/apple/dcp-external-bw.h index a3a799ddefbd7..99430d3e4d883 100644 --- a/drivers/gpu/drm/apple/dcp-external-bw.h +++ b/drivers/gpu/drm/apple/dcp-external-bw.h @@ -5,6 +5,19 @@ #include +#define DCP_EXTERNAL_FW_CELLS 3 + +/* The external H17P wire contract is supported only for this firmware release. */ +static inline bool +apple_dcp_external_firmware_supported(const u32 *firmware, int firmware_cells, + const u32 *compat, int compat_cells) +{ + return firmware && compat && firmware_cells == DCP_EXTERNAL_FW_CELLS && + compat_cells == DCP_EXTERNAL_FW_CELLS && + firmware[0] == 26 && firmware[1] == 6 && firmware[2] == 2 && + compat[0] == 26 && compat[1] == 6 && compat[2] == 2; +} + static inline bool apple_dcp_external_bw_valid(u64 size, int scratch, int request) { if (size < sizeof(u32) || scratch < 0 || request < 0) @@ -21,19 +34,5 @@ static inline bool apple_dcp_external_clock_window(u64 start, u64 size, return start == expected_start && size == expected_size; } -/* Resolve optional diagnostic overrides without changing the SoC defaults. */ -static inline bool apple_dcp_external_bw_resolve(u64 size, int default_scratch, - int default_request, int override_scratch, - int override_request, u32 *scratch, u32 *request) -{ - int s = override_scratch == -1 ? default_scratch : override_scratch; - int r = override_request == -1 ? default_request : override_request; - - if (!apple_dcp_external_bw_valid(size, s, r)) - return false; - *scratch = s; - *request = r; - return true; -} #endif diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index 2007129a3ac12..95881b9cb79fe 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -20,6 +20,7 @@ #include "dcp_backlight.h" #include "dcp-display-metadata.h" #include "dcp-metadata-epoch.h" +#include "dcp-180hz.h" #include "iomfb.h" #include "iomfb_h17p.h" #include "iomfb-state.h" @@ -69,6 +70,9 @@ struct apple_dcp_typec_route { bool tunnel; /* tunnel: crossbar brought up (at DidChangeLinkConfiguration) */ bool xbar_up; + /* Lower mux release is consumed even when idle programming fails. */ + bool deactivate_xbar_done; + int deactivate_error; }; bool dcp_is_typec_output(struct apple_dcp *dcp); @@ -111,6 +115,10 @@ struct dcp_chunks { bool contiguous; bool payload_complete; struct dcp_metadata_ticket metadata_ticket; + u64 av_service_epoch; /* Captured at property start, never at publication. */ + struct dcp_metadata_ticket mode_ticket; + u64 automatic_connection_sequence; + bool automatic_service_pending; }; #define DCP_MAX_MAPPINGS (128) /* should be enough */ @@ -184,7 +192,7 @@ struct dcp_panel { enum dcp_iomfb_method_profile { DCP_IOMFB_METHODS_DEFAULT, DCP_IOMFB_METHODS_H17G, - DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL, + DCP_IOMFB_METHODS_H17P_EXTERNAL, }; struct apple_dcp_hw_data { @@ -229,6 +237,8 @@ struct apple_dcp { u64 external_rx_generation; u64 external_stop_generation; bool external_cable; + /* RX tombstone: initially unknown; only physical OOB events change it. */ + bool external_physically_absent; bool external_running; bool external_power_attempted; /* iomfb.lock: replay the latest callback after activation succeeds. */ @@ -236,8 +246,6 @@ struct apple_dcp { bool external_hotplug_connected; bool external_stopped; int external_error; - u64 external_power_reply; - bool external_power_reply_valid; struct work_struct external_stop_work; @@ -291,6 +299,9 @@ struct apple_dcp { /* Active chunked transfer. There can only be one at a time. */ struct dcp_chunks chunks; + struct dcp_180hz_proof rate_proof; + struct delayed_work rate_retry_wq; + bool rate_retry_initialized; struct { /* Protects connection tickets and cached metadata snapshots. */ spinlock_t lock; @@ -465,8 +476,6 @@ struct apple_dcp { struct dcp_present_h17p present_h17p; struct dcp_present_state_h17p present_state_h17p; DECLARE_BITMAP(unknown_callbacks, IOMFB_MAX_CB); - /* Runtime callbacks whose record size has been reported once. */ - DECLARE_BITMAP(sized_callbacks, IOMFB_MAX_CB); /* Serializes H17P transmit preparation with RTKit receive callbacks. */ struct { diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index e208e102949f9..ca5988f81fe20 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -88,7 +88,7 @@ static int dcp_pin_live_session(struct apple_dcp *dcp) if ((!dcp->hw.adopt_live_session && dcp->hw.iomfb_method_profile != - DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) || + DCP_IOMFB_METHODS_H17P_EXTERNAL) || dcp->fw_compat != DCP_FIRMWARE_H17P || dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17G || dcp->retain_dma) return 0; @@ -139,24 +139,22 @@ static void dcp_release_rtkit(void *data) #define DCP_BOOT_TIMEOUT msecs_to_jiffies(1000) -static bool experimental_h17p_external = true; -module_param(experimental_h17p_external, bool, 0444); -MODULE_PARM_DESC(experimental_h17p_external, - "Enable T8140 external display protocol (default: enabled)"); - -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, - "Override external bandwidth scratch offset (-1 uses the hardware profile)"); - -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, - "Override external bandwidth request offset (-1 uses the hardware profile)"); - -bool dcp_h17p_external_enabled(void) +bool dcp_h17p_external_supported(struct device_node *np) { - return experimental_h17p_external; + u32 firmware[DCP_EXTERNAL_FW_CELLS], compat[DCP_EXTERNAL_FW_CELLS]; + int firmware_cells, compat_cells; + + if (!of_machine_is_compatible("apple,j700") || + !of_device_is_compatible(np, "apple,t8140-dcpext")) + return false; + firmware_cells = of_property_read_variable_u32_array( + np, "apple,firmware-version", firmware, + DCP_EXTERNAL_FW_CELLS, DCP_EXTERNAL_FW_CELLS); + compat_cells = of_property_read_variable_u32_array( + np, "apple,firmware-compat", compat, + DCP_EXTERNAL_FW_CELLS, DCP_EXTERNAL_FW_CELLS); + return apple_dcp_external_firmware_supported(firmware, firmware_cells, + compat, compat_cells); } bool dcp_h17p_external_ready(struct device_node *np) @@ -165,7 +163,7 @@ bool dcp_h17p_external_ready(struct device_node *np) struct apple_dcp *dcp; bool ready = false; - if (!dcp_h17p_external_enabled()) + if (!dcp_h17p_external_supported(np)) return false; pdev = of_find_device_by_node(np); if (!pdev) @@ -212,6 +210,9 @@ struct apple_dcp_typec_port { u32 applied_status; u32 applied_conf; bool applied_valid; + /* SAFE preparation retains this owner until the set phase completes. */ + bool safe_pending; + bool safe_finish_ready; bool hpd; /* Direct DP-alt: the port is in DP mode, routed or not, and its HPD. */ bool dp_wanted; @@ -264,6 +265,13 @@ static bool dcp_typec_route_fixed_output_busy(struct apple_dcp_typec_route *rout static bool dcp_typec_route_available(struct apple_dcp_typec_route *route) { + struct apple_dcp_typec_port *port; + + list_for_each_entry(port, &dcp_typec_ports, link) { + if (port->safe_pending && port->owner && + port->owner->dcp == route->dcp) + return false; + } return !route->dcp->active_typec_route && !dcp_typec_route_fixed_output_busy(route); } @@ -303,7 +311,7 @@ static bool dcp_typec_route_fits(struct apple_dcp_typec_route *route, static bool dcp_is_external_h17p(struct apple_dcp *dcp) { - return dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL; + return dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL; } static int dcp_external_stop_locked(struct apple_dcp *dcp, u64 generation); @@ -365,9 +373,9 @@ static int dcp_typec_route_activate(struct apple_dcp_typec_route *route, if (dcp->fixed_route_selected) { ret = mux_control_deselect(dcp->xbar); + dcp->fixed_route_selected = false; if (ret) return ret; - dcp->fixed_route_selected = false; } /* @@ -429,6 +437,8 @@ static int dcp_typec_route_activate(struct apple_dcp_typec_route *route, } dcp->active_typec_route = route; scoped_guard(mutex, &dcp->tb_lock) { + route->deactivate_xbar_done = false; + route->deactivate_error = 0; route->active_xbar = xbar; route->tunnel = xbar != route->xbar; route->xbar_up = !route->tunnel; @@ -463,13 +473,22 @@ static int dcp_typec_route_deactivate(struct apple_dcp_typec_route *route) * crossbar nor restart the tunnel pixel clock behind our back. */ scoped_guard(mutex, &dcp->tb_lock) { - if (!route->tunnel || route->xbar_up) - ret = mux_control_deselect(route->active_xbar ?: route->xbar); - else - dcp_dpxbar_preselect(route->active_xbar, MUX_IDLE_DISCONNECT); - if (ret) - return ret; - route->xbar_up = false; + if (route->deactivate_error) + return route->deactivate_error; + if (!route->deactivate_xbar_done) { + if (!route->tunnel || route->xbar_up) + ret = mux_control_deselect(route->active_xbar ?: route->xbar); + else + ret = dcp_dpxbar_preselect(route->active_xbar, MUX_IDLE_DISCONNECT); + /* Deselect always unlocks: never consume that selection twice. */ + route->deactivate_xbar_done = true; + route->xbar_up = false; + if (ret) { + /* Idle is unproven; retain the owner and the real error. */ + route->deactivate_error = ret; + return ret; + } + } if (route->tunnel && dcp->phy) { /* the tunnel pixel clock must not outlive the tunnel */ @@ -539,7 +558,7 @@ static int dcp_typec_route_deactivate(struct apple_dcp_typec_route *route) dcp->phy = dcp->fixed_phy; dcp->dptx_phy = dcp->fixed_dptx_phy; - if (dcp->xbar) { + if (dcp->xbar && !dcp->fixed_route_selected) { ret = mux_control_select(dcp->xbar, dcp->fixed_mux_index); if (ret) @@ -858,6 +877,8 @@ dcp_typec_rebalance_locked(struct apple_dcp_typec_port *arriving, struct apple_dcp_typec_route *owner = port->owner; struct apple_dcp *dcp; + if (port->safe_pending) + continue; if (!owner || owner->tunnel || owner == port->target) continue; @@ -942,6 +963,86 @@ void dcp_typec_reorder(void) dcp_typec_rebalance_locked(NULL, 0); } +static int dcp_typec_route_prepare(struct typec_mux_dev *mux, + struct typec_mux_state *state) +{ + struct apple_dcp_typec_route *route = typec_mux_get_drvdata(mux); + struct apple_dcp_typec_port *port = route->port; + struct apple_dcp *dcp; + int ret; + + guard(mutex)(&dcp_typec_fabric_lock); + if (state->mode != TYPEC_STATE_SAFE) + return port->safe_pending ? -EBUSY : 0; + if (!port->owner || port->owner->tunnel) + return 0; + dcp = port->owner->dcp; + /* Stop new admission, but retain the owner until the set phase succeeds. */ + if (!port->safe_pending) + port->safe_finish_ready = false; + port->safe_pending = true; + port->applied_valid = false; + port->dp_wanted = false; + port->dp_hpd = false; + /* HPD absence alone is not proof of a completed firmware shutdown. */ + if (dcp_is_external_h17p(dcp)) { + scoped_guard(mutex, &dcp->external_lock) { + if (dcp->external_error) + return dcp->external_error; + if (!dcp->external_cable && dcp->external_stopped && + !READ_ONCE(dcp->dptxport[0].connected)) { + port->hpd = false; + return 0; + } + } + } + ret = dcp_dptx_disconnect_oob(to_platform_device(dcp->dev), 0); + if (ret) + return ret; + port->hpd = false; + return 0; +} + +static int dcp_typec_route_finish(struct typec_mux_dev *mux, + struct typec_mux_state *state) +{ + struct apple_dcp_typec_port *port = + ((struct apple_dcp_typec_route *)typec_mux_get_drvdata(mux))->port; + int ret; + + guard(mutex)(&dcp_typec_fabric_lock); + if (state->mode != TYPEC_STATE_SAFE || !port->safe_pending || + port->safe_finish_ready) + return 0; + ret = port->owner ? dcp_typec_route_deactivate(port->owner) : 0; + if (ret) + return ret; + port->safe_finish_ready = true; + return 0; +} + +static void dcp_typec_route_commit(struct typec_mux_dev *mux, + struct typec_mux_state *state) +{ + struct apple_dcp_typec_port *port = + ((struct apple_dcp_typec_route *)typec_mux_get_drvdata(mux))->port; + + guard(mutex)(&dcp_typec_fabric_lock); + if (state->mode != TYPEC_STATE_SAFE || !port->safe_pending) + return; + if (port->owner) + port->preferred_route = port->owner; + port->owner = NULL; + port->safe_pending = false; + port->safe_finish_ready = false; + port->applied_alt = state->alt; + port->applied_mode = state->mode; + port->applied_status = 0; + port->applied_conf = 0; + port->applied_valid = true; + dcp_typec_pipeline_freed(); +} + static int dcp_typec_route_set(struct typec_mux_dev *mux, struct typec_mux_state *state) { @@ -957,6 +1058,11 @@ static int dcp_typec_route_set(struct typec_mux_dev *mux, guard(mutex)(&dcp_typec_fabric_lock); + if (state->mode == TYPEC_STATE_SAFE && port->safe_pending) + return 0; + if (port->safe_pending) + return -EBUSY; + /* * Every candidate route for this port is notified with the same state, * so only the first to arrive does the work; the others return early. @@ -1013,6 +1119,7 @@ static int dcp_typec_route_set(struct typec_mux_dev *mux, dcp_typec_rebalance_locked(NULL, 0); } + port->safe_pending = false; /* Data-only USB4 changes do not invalidate other display routes. */ port->applied_valid = true; return 0; @@ -1183,7 +1290,8 @@ int dcp_tunnel_crossbar_up(struct apple_dcp *dcp) guard(mutex)(&dcp->tb_lock); route = dcp->active_typec_route; - if (!route || !route->tunnel || !route->active_xbar) + if (!route || !route->tunnel || !route->active_xbar || + route->deactivate_xbar_done) return -ENODEV; if (!dcp->tb_clock_ok) { dev_warn(dcp->dev, "no DP tunnel pixel clock, crossbar left down\n"); @@ -1688,6 +1796,9 @@ static int dcp_register_typec_routes(struct apple_dcp *dcp) desc.fwnode = of_fwnode_handle(route_np); desc.set = dcp_typec_route_set; + desc.prepare = dcp_typec_route_prepare; + desc.finish = dcp_typec_route_finish; + desc.commit = dcp_typec_route_commit; desc.name = mux_name; desc.drvdata = route; route->typec_mux = typec_mux_register(dev, &desc); @@ -1827,25 +1938,7 @@ void dcp_drm_crtc_page_flip(struct apple_dcp *dcp, ktime_t now) static bool dcp_firmware_metadata_enabled(struct apple_dcp *dcp) { - return dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL; -} - -/* Called under metadata.lock; no monitor contents or pointers are exposed. */ -static void dcp_trace_metadata(struct apple_dcp *dcp, const char *phase, bool valid) -{ - u32 flags = (dcp->metadata.epoch.connected ? BIT(0) : 0) | - (dcp->metadata.epoch.associated_once ? BIT(1) : 0) | - (dcp->metadata.epoch.invalidated ? BIT(2) : 0) | - (dcp->metadata.attributes_valid ? BIT(3) : 0) | - (dcp->metadata.timing_valid ? BIT(4) : 0) | - (dcp->chunks.metadata_ticket.associated ? BIT(5) : 0) | - (dcp->chunks.contiguous ? BIT(6) : 0) | (valid ? BIT(7) : 0) | - (dcp->chunks.payload_complete ? BIT(8) : 0); - - if (dcp_firmware_metadata_enabled(dcp)) - trace_iomfb_metadata(dcp, phase, dcp->metadata.epoch.generation, - dcp->chunks.metadata_ticket.generation, flags, - dcp->chunks.received, dcp->chunks.length); + return dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL; } void dcp_firmware_metadata_connection(struct apple_dcp *dcp, u64 generation, bool connected) @@ -1856,17 +1949,21 @@ void dcp_firmware_metadata_connection(struct apple_dcp *dcp, u64 generation, boo return; spin_lock_irqsave(&dcp->metadata.lock, flags); if (generation != dcp->metadata.epoch.generation || !connected) { + dcp->rate_proof.connection_sequence++; + dcp->rate_proof.sequence++; + dcp->rate_proof.rate.valid = false; + dcp->rate_proof.lanes.valid = false; dcp->metadata.attributes_valid = false; dcp->metadata.timing_valid = false; dcp->metadata.hpd_seen = false; } dcp_metadata_epoch_connection(&dcp->metadata.epoch, generation, connected); - dcp_trace_metadata(dcp, "connection", connected); spin_unlock_irqrestore(&dcp->metadata.lock, flags); } void dcp_firmware_metadata_hotplug(struct apple_dcp *dcp, bool connected) { + struct apple_connector *con = READ_ONCE(dcp->connector); unsigned long flags; if (!dcp_firmware_metadata_enabled(dcp)) @@ -1874,14 +1971,29 @@ void dcp_firmware_metadata_hotplug(struct apple_dcp *dcp, bool connected) spin_lock_irqsave(&dcp->metadata.lock, flags); if (connected) { dcp->metadata.hpd_seen = true; + /* + * Fresh proof may precede HPD assertion after a same-cable loss. + * Keep invalidated firmware metadata and old receipt tickets revoked. + */ + if (READ_ONCE(dcp->typec_cable_connected) && + dcp->metadata.epoch.generation == READ_ONCE(dcp->typec_generation)) + dcp->metadata.epoch.connected = true; } else if (dcp->metadata.hpd_seen) { + dcp->metadata.hpd_seen = false; + dcp->rate_proof.connection_sequence++; + dcp->rate_proof.sequence++; + dcp->rate_proof.rate.valid = false; + dcp->rate_proof.lanes.valid = false; + if (con && READ_ONCE(con->dcp) == to_platform_device(dcp->dev)) + WRITE_ONCE(con->edid_invalidation_pending, true); dcp_metadata_epoch_connection(&dcp->metadata.epoch, dcp->metadata.epoch.generation, false); dcp->metadata.attributes_valid = false; dcp->metadata.timing_valid = false; } - dcp_trace_metadata(dcp, "hotplug", connected); spin_unlock_irqrestore(&dcp->metadata.lock, flags); + if (connected) + dcp_180hz_notify(dcp); } void dcp_firmware_metadata_begin(struct apple_dcp *dcp) @@ -1893,7 +2005,6 @@ void dcp_firmware_metadata_begin(struct apple_dcp *dcp) dcp->chunks.received = 0; dcp->chunks.contiguous = true; dcp->chunks.payload_complete = false; - dcp_trace_metadata(dcp, "begin", dcp->chunks.metadata_ticket.associated); spin_unlock_irqrestore(&dcp->metadata.lock, flags); } @@ -1932,7 +2043,6 @@ void dcp_firmware_metadata_attributes(struct apple_dcp *dcp, dcp->metadata.attributes_ticket = dcp->chunks.metadata_ticket; } } - dcp_trace_metadata(dcp, "attributes", !!attributes); spin_unlock_irqrestore(&dcp->metadata.lock, flags); dcp_firmware_metadata_notify(dcp); } @@ -1960,7 +2070,6 @@ void dcp_firmware_metadata_timing(struct apple_dcp *dcp) break; } unlock: - dcp_trace_metadata(dcp, "timing", dcp->metadata.timing_valid); spin_unlock_irqrestore(&dcp->metadata.lock, flags); dcp_firmware_metadata_notify(dcp); } @@ -1991,7 +2100,6 @@ const struct drm_edid *dcp_firmware_edid(struct apple_dcp *dcp, u64 *generation) valid = dcp->metadata.attributes_valid && dcp->metadata.timing_valid && dcp_metadata_matches(&dcp->metadata.epoch, dcp->metadata.attributes_ticket) && dcp_metadata_matches(&dcp->metadata.epoch, dcp->metadata.timing_ticket); - dcp_trace_metadata(dcp, "probe", valid); if (valid) { attributes = dcp->metadata.attributes; preferred = dcp->metadata.preferred; @@ -2336,6 +2444,12 @@ int dcp_crtc_atomic_check(struct drm_crtc *crtc, struct drm_atomic_state *state) crtc_state = drm_atomic_get_new_crtc_state(state, crtc); + /* External H17P does not program adaptive sync. */ + if (dcp->hw.iomfb_method_profile == + DCP_IOMFB_METHODS_H17P_EXTERNAL && + crtc_state->vrr_enabled) + return -EINVAL; + needs_modeset = drm_atomic_crtc_needs_modeset(crtc_state) || !READ_ONCE(dcp->mode_state.valid); if (!needs_modeset && (!dcp->connector || !dcp->connector->connected)) { @@ -2493,6 +2607,8 @@ static void dcp_external_report_disconnected(struct apple_dcp *dcp) unsigned int action; guard(mutex)(&dcp->iomfb.lock); + /* A paused callback from the old sink must not be replayed on wake. */ + dcp->external_hotplug_pending = false; action = dcp_mode_hotplug(&dcp->mode_state, false, dcp->connector ? &dcp->connector->connected : NULL); if (dcp->connector) { @@ -2622,6 +2738,7 @@ static int dcp_external_disconnect(struct apple_dcp *dcp, bool physical) guard(mutex)(&dcp->external_lock); /* Invalidate physical EDID before waiting for firmware power/link teardown. */ if (physical) { + WRITE_ONCE(dcp->external_physically_absent, true); scoped_guard(mutex, &dcp->hpd_mutex) { WRITE_ONCE(dcp->typec_cable_connected, false); dcp->typec_generation++; @@ -2666,7 +2783,7 @@ bool dcp_external_hotplug(struct apple_dcp *dcp, bool connected) return false; /* No lifecycle or HPD mutex may be acquired from the RX callback. */ lockdep_assert_held(&dcp->iomfb.lock); - if (READ_ONCE(dcp->quiescing)) + if (READ_ONCE(dcp->quiescing) || READ_ONCE(dcp->external_physically_absent)) return true; if (dcp->iomfb.presentation_paused) { dcp->external_hotplug_pending = true; @@ -2688,6 +2805,8 @@ bool dcp_external_hotplug(struct apple_dcp *dcp, bool connected) return true; } if (!connected && !READ_ONCE(dcp->iomfb.presentation_paused)) { + /* Fence admission in RX before a busy actor can delay teardown. */ + dcp_firmware_metadata_hotplug(dcp, false); dcp->external_stop_generation = dcp->external_rx_generation; schedule_work(&dcp->external_stop_work); return true; @@ -2844,6 +2963,7 @@ int dcp_dptx_connect_oob(struct platform_device *pdev, u32 port) if (new_cable) dcp->external_generation++; dcp->external_cable = true; + WRITE_ONCE(dcp->external_physically_absent, false); dcp_external_publish_generation(dcp); scoped_guard(mutex, &dcp->hpd_mutex) { WRITE_ONCE(dcp->typec_cable_connected, true); @@ -3008,7 +3128,7 @@ int dcp_start(struct platform_device *pdev) } /* Make the H17P allocator available before external service discovery. */ - if (dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) { + if (dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL) { 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"); @@ -3033,7 +3153,7 @@ int dcp_start(struct platform_device *pdev) ret); if (dcp->hw.iomfb_method_profile == - DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) { + DCP_IOMFB_METHODS_H17P_EXTERNAL) { ret = dptx_controller_init(dcp); if (ret) return dev_err_probe(dcp->dev, ret, @@ -3687,7 +3807,7 @@ static int dcp_get_disp_regs(struct apple_dcp *dcp) if (dcp->hw.firmware_clock) { unsigned int windows = dcp->hw.firmware_display_windows; bool external = dcp->hw.iomfb_method_profile == - DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL; + DCP_IOMFB_METHODS_H17P_EXTERNAL; if (!windows || windows >= MAX_DISP_REGISTERS) return -EINVAL; @@ -3918,7 +4038,7 @@ static int dcp_comp_bind(struct device *dev, struct device *main, void *data) } if (dcp->hw.iomfb_method_profile == - DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL && + DCP_IOMFB_METHODS_H17P_EXTERNAL && !dcp_external_is_idle(dcp)) return dev_err_probe(dev, -EBUSY, "External DCP is not in the inherited idle state\n"); @@ -3997,7 +4117,7 @@ static int dcp_comp_bind(struct device *dev, struct device *main, void *data) } if (dcp->hw.iomfb_method_profile == - DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL && + DCP_IOMFB_METHODS_H17P_EXTERNAL && (!dcp->clk_194 || !clk_get_rate(dcp->clk) || !clk_get_rate(dcp->clk_194))) { ret = dev_err_probe(dev, -EINVAL, @@ -4013,7 +4133,7 @@ static int dcp_comp_bind(struct device *dev, struct device *main, void *data) INIT_DELAYED_WORK(&dcp->swap_watchdog_wq, dcp_swap_watchdog); if (dcp->hw.iomfb_method_profile == - DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) { + DCP_IOMFB_METHODS_H17P_EXTERNAL) { /* Pin before the first write or mailbox wake can expose DMA. */ ret = dcp_pin_live_session(dcp); if (ret) @@ -4038,7 +4158,7 @@ static int dcp_comp_bind(struct device *dev, struct device *main, void *data) goto err_piodma; /* External H17P supplies shared-buffer addresses during RTKit startup. */ if (dcp->hw.iomfb_method_profile == - DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) + DCP_IOMFB_METHODS_H17P_EXTERNAL) dcp->rtk = apple_rtkit_init(dev, dcp, "mbox", 0, &external_h17p_rtkit_ops); else @@ -4057,7 +4177,7 @@ static int dcp_comp_bind(struct device *dev, struct device *main, void *data) /* Announce host readiness before awaiting external IOP readiness. */ if (dcp->hw.iomfb_method_profile == - DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) + DCP_IOMFB_METHODS_H17P_EXTERNAL) ret = apple_rtkit_wake_early_ap(dcp->rtk); else ret = apple_rtkit_wake(dcp->rtk); @@ -4193,10 +4313,10 @@ static int dcp_platform_probe(struct platform_device *pdev) if (dcp_session_retained(dev)) 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(). */ + /* Reject unsupported external firmware before replacing the boot display. */ 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"); + !dcp_h17p_external_supported(dev->of_node)) { + dev_err(dev, "Unsupported external display hardware or firmware handoff\n"); return -ENODEV; } @@ -4242,17 +4362,13 @@ static int dcp_platform_probe(struct platform_device *pdev) mutex_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) { + DCP_IOMFB_METHODS_H17P_EXTERNAL) { if (!dcp->hw.firmware_clock || - !apple_dcp_external_bw_resolve(resource_size(dcp->hw.firmware_clock), + !apple_dcp_external_bw_valid(resource_size(dcp->hw.firmware_clock), dcp->hw.firmware_scratch, - dcp->hw.firmware_request, - experimental_h17p_external_bw_scratch, - experimental_h17p_external_bw_request, - &dcp->hw.firmware_scratch, - &dcp->hw.firmware_request)) + dcp->hw.firmware_request)) return dev_err_probe(dev, -EINVAL, - "External bandwidth offsets are invalid\n"); + "Invalid external profile bandwidth resources\n"); iomfb_pause_presentation(dcp, true); } @@ -4416,7 +4532,7 @@ static int dcp_platform_probe(struct platform_device *pdev) return ret; if (dcp->hw.iomfb_method_profile == - DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) { + DCP_IOMFB_METHODS_H17P_EXTERNAL) { /* Complete optional startup before the DRM master can depend on it. */ ret = dcp_comp_bind(dev, NULL, NULL); if (ret) { @@ -4443,7 +4559,7 @@ 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_IOMFB_METHODS_H17P_EXTERNAL) { dcp_comp_unbind(&pdev->dev, NULL, NULL); return; } @@ -4603,7 +4719,7 @@ static const struct apple_dcp_hw_data apple_dcp_hw_t8140 = { /* 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, + .iomfb_method_profile = DCP_IOMFB_METHODS_H17P_EXTERNAL, .adopt_live_session = false, .firmware_compat = DCP_FIRMWARE_H17P, .firmware_clock = &t8140_firmware_clock, diff --git a/drivers/gpu/drm/apple/dcp.h b/drivers/gpu/drm/apple/dcp.h index 411240d3e5f42..9678701b8613f 100644 --- a/drivers/gpu/drm/apple/dcp.h +++ b/drivers/gpu/drm/apple/dcp.h @@ -12,7 +12,7 @@ #include "dcp-internal.h" #include "parser.h" -bool dcp_h17p_external_enabled(void); +bool dcp_h17p_external_supported(struct device_node *np); bool dcp_h17p_external_ready(struct device_node *np); struct apple_crtc { diff --git a/drivers/gpu/drm/apple/dptxep.c b/drivers/gpu/drm/apple/dptxep.c index 92b4a6e390722..7bc55bfffe213 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 "dcp-180hz.h" #include "dptx-service.h" #include "parser.h" #include "trace.h" @@ -218,6 +219,7 @@ int dptxport_release_display(struct apple_epic_service *service) struct dptx_port *dptx = service->cookie; int ret; + dcp_180hz_invalidate(service->ep->dcp); dptx->release_calls++; dev_dbg(service->ep->dcp->dev, "DPTX release_display: call #%u this boot caller=%pS\n", @@ -500,6 +502,7 @@ dptxport_call_will_change_link_config(struct apple_epic_service *service) { struct dptx_port *dptx = service->cookie; + dcp_180hz_invalidate(service->ep->dcp); dptx->phy_ops.dp.set_lanes = 0; dptx->phy_ops.dp.set_rate = 0; dptx->phy_ops.dp.set_voltages = 0; @@ -688,6 +691,7 @@ dptxport_call_deactivate(struct apple_epic_service *service, struct dptx_port *dptx = service->cookie; struct apple_dcp *dcp = service->ep->dcp; + dcp_180hz_invalidate(dcp); dev_info(dcp->dev, "DPTXPort: DEACTIVATE\n"); if (dcp->dptx_tunnel) dcp_tunnel_dpin_activate(dcp, false); @@ -701,6 +705,40 @@ dptxport_call_deactivate(struct apple_epic_service *service, return 0; } +static int dptxport_call_qualified_config(struct apple_epic_service *service, bool lanes, + const void *data, size_t size, void *reply, size_t room) +{ + struct dcp_180hz_token token = dcp_180hz_config_begin(service, lanes); + struct dptx_port *port = service->cookie; + bool success = false; + u32 value = 0; + int ret; + + if (lanes) { + ret = dptxport_call_set_active_lane_count(service, data, size, reply, room); + if (!ret && size >= sizeof(struct dptxport_apcall_set_active_lane_count) && + room >= sizeof(struct dptxport_apcall_set_active_lane_count)) { + const struct dptxport_apcall_set_active_lane_count *request = data; + const struct dptxport_apcall_set_active_lane_count *response = reply; + + value = le64_to_cpu(request->lane_count); + success = !le32_to_cpu(response->retcode) && value == 2 && port->lane_count == 2; + } + } else { + ret = dptxport_call_set_link_rate(service, data, size, reply, room); + if (!ret && size >= sizeof(struct dptxport_apcall_link_rate) && + room >= sizeof(struct dptxport_apcall_link_rate)) { + const struct dptxport_apcall_link_rate *request = data; + const struct dptxport_apcall_link_rate *response = reply; + + value = le32_to_cpu(request->link_rate); + success = !le32_to_cpu(response->retcode) && value == LINK_RATE_HBR3 && port->link_rate == value; + } + } + dcp_180hz_config_complete(service, lanes, token, value, success); + return ret; +} + static int dptxport_call(struct apple_epic_service *service, u32 idx, const void *data, size_t data_size, void *reply, size_t reply_size) @@ -710,7 +748,7 @@ static int dptxport_call(struct apple_epic_service *service, u32 idx, dev_dbg(service->ep->dcp->dev, "DPTXPort: APCALL %u (%zu bytes)\n", idx, data_size); if (data_size && service->ep->dcp->hw.iomfb_method_profile != - DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) + DCP_IOMFB_METHODS_H17P_EXTERNAL) print_hex_dump_debug("DPTXPort: apcall data: ", DUMP_PREFIX_OFFSET, 16, 1, data, min(data_size, (size_t)64), true); @@ -738,8 +776,7 @@ static int dptxport_call(struct apple_epic_service *service, u32 idx, case DPTX_APCALL_GET_LINK_RATE: return dptxport_call_get_link_rate(service, reply, reply_size); case DPTX_APCALL_SET_LINK_RATE: - return dptxport_call_set_link_rate(service, data, data_size, - reply, reply_size); + return dptxport_call_qualified_config(service, false, data, data_size, reply, reply_size); case DPTX_APCALL_GET_MAX_LANE_COUNT: return dptxport_call_get_max_lane_count(service, reply, reply_size); case DPTX_APCALL_GET_ACTIVE_LANE_COUNT: { @@ -752,8 +789,7 @@ static int dptxport_call(struct apple_epic_service *service, u32 idx, return 0; } case DPTX_APCALL_SET_ACTIVE_LANE_COUNT: - return dptxport_call_set_active_lane_count(service, data, data_size, - reply, reply_size); + return dptxport_call_qualified_config(service, true, data, data_size, reply, reply_size); case DPTX_APCALL_GET_SUPPORTS_HPD: return dptxport_call_get_supports_hpd(service, reply, reply_size); @@ -766,6 +802,7 @@ static int dptxport_call(struct apple_epic_service *service, u32 idx, case DPTX_APCALL_DEVICE_NOT_RESPONDING: case DPTX_APCALL_DEVICE_BUSY_TIMEOUT: case DPTX_APCALL_DEVICE_NOT_STARTED: + dcp_180hz_invalidate(service->ep->dcp); dev_warn(service->ep->dcp->dev, "DPTXPort: firmware reports link fault %u on target %u:%u\n", idx, service->ep->dcp->dptx_die, @@ -809,7 +846,7 @@ static int dptxport_call(struct apple_epic_service *service, u32 idx, return 0; default: if (service->ep->dcp->hw.iomfb_method_profile == - DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) + DCP_IOMFB_METHODS_H17P_EXTERNAL) return -EOPNOTSUPP; /* just try to ACK and hope for the best... */ dev_info(service->ep->dcp->dev, "DPTXPort: acking unhandled call %u\n", @@ -846,7 +883,7 @@ static void dptxport_init(struct apple_epic_service *service, const char *name, complete(&port->enable_completion); /* Publication may follow the startup wait or the bounded retry window. */ if (service->ep->dcp->hw.iomfb_method_profile == - DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL && + DCP_IOMFB_METHODS_H17P_EXTERNAL && READ_ONCE(service->ep->dcp->typec_cable_connected) && !READ_ONCE(service->ep->dcp->quiescing) && !READ_ONCE(service->ep->dcp->external_error)) @@ -938,11 +975,17 @@ static int dptxport_group_call(struct apple_epic_service *service, u16 group, return 0; } +static void dptxport_qualified_teardown(struct apple_epic_service *service) +{ + dcp_180hz_invalidate(service->ep->dcp); + dptx_service_unbind(service); +} + static const struct apple_epic_service_ops dptxep_ops[] = { { .name = "AppleDCPDPTXRemotePort", .prepare = dptxport_prepare, - .teardown = dptx_service_unbind, + .teardown = dptxport_qualified_teardown, .init = dptxport_init, .call = dptxport_call, .call_v2 = dptxport_group_call, @@ -975,7 +1018,7 @@ int dptx_controller_init(struct apple_dcp *dcp) 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_IOMFB_METHODS_H17P_EXTERNAL; dcp->dptx_controller_ep = ep; ret = afk_start(ep); @@ -992,7 +1035,7 @@ 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; + DCP_IOMFB_METHODS_H17P_EXTERNAL; int ret; u32 port; unsigned long timeout = msecs_to_jiffies(1000); @@ -1025,5 +1068,5 @@ int dptxep_init(struct apple_dcp *dcp) timeout = ret; } - return 0; + return dcp_180hz_init(dcp); } diff --git a/drivers/gpu/drm/apple/epic/dpavservep.c b/drivers/gpu/drm/apple/epic/dpavservep.c index 903d9578b0f80..a61d28e5e2edb 100644 --- a/drivers/gpu/drm/apple/epic/dpavservep.c +++ b/drivers/gpu/drm/apple/epic/dpavservep.c @@ -50,7 +50,8 @@ static bool dcpavserv_admission_current(struct apple_dcp *dcp, u64 generation) { lockdep_assert_held(&dcp->hpd_mutex); /* Initial service discovery may precede the first Type-C attachment. */ - return !READ_ONCE(dcp->quiescing) && generation == dcp->typec_generation && + return !READ_ONCE(dcp->quiescing) && !READ_ONCE(dcp->external_physically_absent) && + generation == dcp->typec_generation && (!generation || dcp->typec_cable_connected); } @@ -81,6 +82,26 @@ static int dcpavserv_prepare(struct apple_epic_service *service, const char *nam return ret; } +static int dcpavserv_revalidate(struct apple_epic_service *service, const char *name, + const char *class, s64 unit) +{ + struct apple_dcp *dcp = service->ep->dcp; + int ret = 0; + + if (!dcpavserv_identity(service, name, class, unit)) + return -ENODEV; + mutex_lock(&dcp->hpd_mutex); + mutex_lock(&dcp->dcpavserv.lock); + if (dcp->dcpavserv.enabled) + ret = -EBUSY; + else if (service->admission.epoch != dcp->dcpavserv.epoch || + !dcpavserv_admission_current(dcp, service->admission.generation)) + ret = -ENODEV; + mutex_unlock(&dcp->dcpavserv.lock); + mutex_unlock(&dcp->hpd_mutex); + return ret; +} + static void dcpavserv_init(struct apple_epic_service *service, const char *name, const char *class, s64 unit) { @@ -177,7 +198,7 @@ static int dcpdpserv_prepare(struct apple_epic_service *service, const char *nam const char *class, s64 unit) { /* No production command transport is needed for this H17P service. */ - return service->ep->wire_v2 ? -ENODEV : 0; + return service->ep->wire_v2 ? -EOPNOTSUPP : 0; } static void dcpdpserv_init(struct apple_epic_service *service, const char *name, @@ -361,16 +382,18 @@ bool dcpavserv_edid_current(struct apple_dcp *dcp, u64 generation) { lockdep_assert_held(&dcp->hpd_mutex); return dcp->hw.iomfb_method_profile != - DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL || + DCP_IOMFB_METHODS_H17P_EXTERNAL || (dcp->typec_cable_connected && generation == dcp->typec_generation && - !READ_ONCE(dcp->quiescing)); + !READ_ONCE(dcp->quiescing) && !READ_ONCE(dcp->external_physically_absent)); } bool dcpavserv_edid_service_current(struct apple_dcp *dcp, u64 epoch) { lockdep_assert_held(&dcp->dcpavserv.lock); return dcp->dcpavserv.enabled && dcp->dcpavserv.service && - epoch == dcp->dcpavserv.epoch; + epoch == dcp->dcpavserv.epoch && + (dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17P_EXTERNAL || + !READ_ONCE(dcp->external_physically_absent)); } const struct drm_edid *dcpavserv_copy_edid(struct apple_dcp *dcp, u64 generation, @@ -416,6 +439,7 @@ static const struct apple_epic_service_ops dpavservep_ops[] = { .name = "dcpav-service-epic", .reusable = true, .prepare = dcpavserv_prepare, + .revalidate = dcpavserv_revalidate, .init = dcpavserv_init, .teardown = dcpavserv_teardown, .report = dcpavserv_report, @@ -423,6 +447,7 @@ static const struct apple_epic_service_ops dpavservep_ops[] = { { .name = "dcpdp-service-epic", .reusable = true, + .optional = true, .prepare = dcpdpserv_prepare, .init = dcpdpserv_init, .teardown = dcpdpserv_teardown, @@ -439,13 +464,13 @@ int dpavservep_init(struct apple_dcp *dcp) init_completion(&dcp->dcpavserv.enable_completion); ep = afk_init(dcp, dcp->hw.iomfb_method_profile == - DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL ? + DCP_IOMFB_METHODS_H17P_EXTERNAL ? DPAVSERV_ENDPOINT_V2 : DPAVSERV_ENDPOINT, dpavservep_ops); if (IS_ERR(ep)) return PTR_ERR(ep); /* External H17P uses AFKV2 service calls with host-owned OOB buffers. */ ep->wire_v2 = dcp->hw.iomfb_method_profile == - DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL; + DCP_IOMFB_METHODS_H17P_EXTERNAL; ep->inline_v2 = false; dcp->dcpavservep = ep; @@ -466,7 +491,6 @@ int dpavservep_init(struct apple_dcp *dcp) ret = wait_for_completion_timeout(&dcp->dcpavserv.enable_completion, msecs_to_jiffies(1000)); if (!ret) { - afk_trace_ring_snapshot(ep); dev_warn(dcp->dev, "DCPAVSERV: timed out waiting for EDID service announcement\n"); return -ETIMEDOUT; } diff --git a/drivers/gpu/drm/apple/ibootep.c b/drivers/gpu/drm/apple/ibootep.c index 79d5dc19569c3..3cada415c249a 100644 --- a/drivers/gpu/drm/apple/ibootep.c +++ b/drivers/gpu/drm/apple/ibootep.c @@ -32,7 +32,7 @@ int ibootep_init(struct apple_dcp *dcp) { const struct apple_epic_service_ops *ops = ibootep_ops; - if (dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) + if (dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL) ops = external_ibootep_ops; dcp->ibootep = afk_init(dcp, DISP0_ENDPOINT, ops); if (IS_ERR(dcp->ibootep)) diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index 2d7b42869dc68..1027af19ab49e 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -195,7 +195,7 @@ static int iomfb_enqueue_opaque_x(struct apple_dcp *dcp) 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) + DCP_IOMFB_METHODS_H17P_EXTERNAL) return 0; if (atomic_read(&dcp->iomfb.opaque_x_state) != IOMFB_OPAQUE_X_NEEDED) return 0; @@ -866,7 +866,7 @@ static bool dcp_external_zero_reply(struct apple_dcp *dcp, int tag, 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 && + DCP_IOMFB_METHODS_H17P_EXTERNAL && of_machine_is_compatible("apple,j700") && of_device_is_compatible(dcp->dev->of_node, "apple,t8140-dcpext"); } @@ -920,7 +920,7 @@ static void dcpep_handle_cb(struct apple_dcp *dcp, enum dcp_context_id context, 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 && !zero_reply) { + DCP_IOMFB_METHODS_H17P_EXTERNAL && !zero_reply) { /* Retain the failed session without inventing a successful reply. */ dcp->crashed = true; complete(&dcp->start_done); @@ -1046,6 +1046,55 @@ static void dcpep_got_msg(struct apple_dcp *dcp, u64 message) dcpep_handle_cb(dcp, ctx_id, data, length, offset); } +/* Retained arrays are not freed while mode readers can still refer to them. */ +static struct dcp_display_mode *dcp_modes_snapshot(struct apple_dcp *dcp, + unsigned int *count) +{ + struct dcp_display_mode *modes; + unsigned long flags; + + spin_lock_irqsave(&dcp->metadata.lock, flags); + modes = dcp->modes; + *count = dcp->nr_modes; + spin_unlock_irqrestore(&dcp->metadata.lock, flags); + return modes; +} + +/* Caller holds hpd_mutex -> dcpavserv.lock -> metadata.lock. */ +static bool dcp_exception_mode_current_locked(struct apple_dcp *dcp, + const struct dcp_display_mode *mode) +{ + if (!mode->automatic_rgb8_quirk) + return true; + return dcp_180hz_mode_current(dcp, mode); +} + +static bool dcp_publish_mode(struct apple_dcp *dcp, struct drm_connector *connector, + const struct dcp_display_mode *source, + struct drm_display_mode *copy) +{ + unsigned long flags; + bool eligible; + + /* Ordinary modes keep their existing publication policy. */ + if (!source->automatic_rgb8_quirk) { + drm_mode_probed_add(connector, copy); + return true; + } + /* External get_modes holds hpd_mutex after its final EDID generation check. */ + /* Allocation happened outside metadata.lock; recheck before linking. */ + spin_lock_irqsave(&dcp->metadata.lock, flags); + eligible = dcp->connector && &dcp->connector->base == connector && + dcp_exception_mode_current_locked(dcp, source); + if (eligible) + /* drm_mode_probed_add only links a list under mode_config.mutex. */ + drm_mode_probed_add(connector, copy); + spin_unlock_irqrestore(&dcp->metadata.lock, flags); + if (!eligible) + drm_mode_destroy(connector->dev, copy); + return eligible; +} + int dcp_get_modes(struct drm_connector *connector) { struct apple_connector *apple_connector = to_apple_connector(connector); @@ -1054,12 +1103,15 @@ int dcp_get_modes(struct drm_connector *connector) struct drm_device *dev = connector->dev; struct drm_display_mode *mode; + struct dcp_display_mode *modes; + unsigned int nr_modes, published = 0; u16 min_vfreq = 0, max_vfreq = 0; bool vrr_capable = false; bool physical_edid; + bool pending_180hz = false, published_180hz = false; u64 edid_generation = 0; u64 service_epoch = 0; - int i; + unsigned int i; dcp_consume_edid_invalidation(apple_connector); @@ -1072,31 +1124,10 @@ int dcp_get_modes(struct drm_connector *connector) return 0; } dcp = platform_get_drvdata(pdev); - - for (i = 0; i < dcp->nr_modes; ++i) { - if (dcp->modes[i].vrr) { - u16 lo = dcp->modes[i].min_vrr >> 16; - u16 hi = dcp->modes[i].max_vrr >> 16; - - if (!min_vfreq || lo < min_vfreq) - min_vfreq = lo; - if (hi > max_vfreq) - max_vfreq = hi; - } - vrr_capable |= dcp->modes[i].vrr; - mode = drm_mode_duplicate(dev, &dcp->modes[i].mode); - - if (!mode) { - dev_err(dev->dev, "Failed to duplicate display mode\n"); - return 0; - } - - drm_mode_probed_add(connector, mode); - } - drm_connector_set_vrr_capable_property(connector, vrr_capable); + modes = dcp_modes_snapshot(dcp, &nr_modes); physical_edid = dcp->hw.iomfb_method_profile == - DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL; + DCP_IOMFB_METHODS_H17P_EXTERNAL; /* get_modes holds mode_config.mutex; HPD paths never acquire it. */ if (physical_edid) { mutex_lock(&dcp->hpd_mutex); @@ -1117,7 +1148,7 @@ int dcp_get_modes(struct drm_connector *connector) } /* Internal H17P still has no supported EDID command transport. */ - if (dcp->nr_modes && !apple_connector->drm_edid && + if (nr_modes && !apple_connector->drm_edid && (dcp->fw_compat != DCP_FIRMWARE_H17P || (dcp->dcpavservep && dcp->dcpavservep->wire_v2))) { const struct drm_edid *edid; @@ -1162,12 +1193,18 @@ int dcp_get_modes(struct drm_connector *connector) apple_connector->drm_edid_service_epoch = service_epoch; } } - if (dcp->nr_modes && apple_connector->drm_edid) { - apple_connector->drm_edid_firmware_derived = false; - drm_edid_connector_update(connector, apple_connector->drm_edid); + if (nr_modes && apple_connector->drm_edid) { + int update = drm_edid_connector_update(connector, apple_connector->drm_edid); + + if (update) { + dev_info(dcp->dev, "raw EDID publication failed: %d\n", update); + dcp_clear_connector_edid(apple_connector); + } else { + apple_connector->drm_edid_firmware_derived = false; + } } else { u64 generation = 0; - const struct drm_edid *derived = dcp->nr_modes ? + const struct drm_edid *derived = nr_modes ? dcp_firmware_edid(dcp, &generation) : ERR_PTR(-ENODATA); if (!IS_ERR(derived)) { @@ -1193,9 +1230,38 @@ int dcp_get_modes(struct drm_connector *connector) } edid_done: + /* Publish after a possibly delayed raw read, under its physical generation. */ + for (i = 0; i < nr_modes; ++i) { + mode = drm_mode_duplicate(dev, &modes[i].mode); + if (!mode) { + dev_err(dev->dev, "Failed to duplicate display mode\n"); + published = 0; + goto modes_done; + } + pending_180hz |= modes[i].automatic_rgb8_quirk && + dcp_180hz_pending(dcp, &modes[i]); + if (!dcp_publish_mode(dcp, connector, &modes[i], mode)) + continue; + published_180hz |= modes[i].automatic_rgb8_quirk; + published++; + if (modes[i].vrr) { + u16 lo = modes[i].min_vrr >> 16; + u16 hi = modes[i].max_vrr >> 16; + + if (!min_vfreq || lo < min_vfreq) + min_vfreq = lo; + if (hi > max_vfreq) + max_vfreq = hi; + } + vrr_capable |= modes[i].vrr; + } + drm_connector_set_vrr_capable_property(connector, vrr_capable); +modes_done: mutex_unlock(&dcp->dcpavserv.lock); if (physical_edid) mutex_unlock(&dcp->hpd_mutex); + if (pending_180hz && !published_180hz) + dcp_180hz_retry(dcp); if (apple_connector->drm_edid) dcp_retry_placeholder_edid(dcp, apple_connector->drm_edid); @@ -1211,20 +1277,35 @@ int dcp_get_modes(struct drm_connector *connector) connector->display_info.monitor_range.max_vfreq = max_vfreq; } - return dcp->nr_modes; + return published; } /* The user may own drm_display_mode, so we need to search for our copy */ struct dcp_display_mode *lookup_mode(struct apple_dcp *dcp, const struct drm_display_mode *mode) { - int i; + struct dcp_display_mode *modes; + unsigned int count, i; + unsigned long flags; + bool eligible; - for (i = 0; i < dcp->nr_modes; ++i) { - if (drm_mode_match(mode, &dcp->modes[i].mode, + modes = dcp_modes_snapshot(dcp, &count); + for (i = 0; i < count; ++i) { + if (drm_mode_match(mode, &modes[i].mode, DRM_MODE_MATCH_TIMINGS | - DRM_MODE_MATCH_CLOCK)) - return &dcp->modes[i]; + DRM_MODE_MATCH_CLOCK)) { + if (!modes[i].automatic_rgb8_quirk) + return &modes[i]; + mutex_lock(&dcp->hpd_mutex); + mutex_lock(&dcp->dcpavserv.lock); + spin_lock_irqsave(&dcp->metadata.lock, flags); + eligible = dcp_exception_mode_current_locked(dcp, &modes[i]); + spin_unlock_irqrestore(&dcp->metadata.lock, flags); + mutex_unlock(&dcp->dcpavserv.lock); + mutex_unlock(&dcp->hpd_mutex); + if (eligible) + return &modes[i]; + } } return NULL; @@ -1247,7 +1328,7 @@ static bool dcp_mode_settable(struct apple_dcp *dcp, if (dcp->fw_compat == DCP_FIRMWARE_H17P && dcp->hw.iomfb_method_profile != - DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) + DCP_IOMFB_METHODS_H17P_EXTERNAL) return dcp_mode->mode.type & DRM_MODE_TYPE_PREFERRED; return true; @@ -1279,7 +1360,7 @@ int dcp_crtc_atomic_modeset(struct drm_crtc *crtc, /* Disconnect cannot pass the admission check before these RPCs finish. */ guard(mutex)(&dcp->external_lock); - if (dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL && + if (dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL && (!dcp->external_running || dcp->external_error || READ_ONCE(dcp->quiescing) || READ_ONCE(dcp->crashed) || READ_ONCE(dcp->iomfb.presentation_paused))) return -EPIPE; @@ -1772,7 +1853,7 @@ int iomfb_start_rtkit(struct apple_dcp *dcp) * display DART faults. */ if (dcp->fw_compat == DCP_FIRMWARE_H17P && - dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) { + dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17P_EXTERNAL) { ret = apple_rtkit_start_ep(dcp->rtk, REMOTE_ALLOC_ENDPOINT); if (ret) return ret; diff --git a/drivers/gpu/drm/apple/iomfb_h17p.c b/drivers/gpu/drm/apple/iomfb_h17p.c index 02bb44d76c29b..45743b4173699 100644 --- a/drivers/gpu/drm/apple/iomfb_h17p.c +++ b/drivers/gpu/drm/apple/iomfb_h17p.c @@ -446,13 +446,15 @@ static bool trampoline_provider_property_h17p(struct apple_dcp *dcp, int tag, } else if (!of_property_present(node, property->dt_name) && of_machine_is_compatible("apple,j700") && (of_device_is_compatible(node, "apple,t8140-dcp") || - (dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL && + (dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL && 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); reply->length = cpu_to_le32(property->length); - dev_warn_once(dcp->dev, "using J700 provider data for an older loader\n"); + dev_warn_once(dcp->dev, + "missing provider property %s (%u bytes); using qualified J700 compatibility data\n", + property->dt_name, property->length); return true; } else if (length != (int)property->length) { dev_err(dcp->dev, "invalid provider property %s: %d bytes\n", @@ -696,26 +698,10 @@ static const iomfb_cb_handler cb_handlers[IOMFB_MAX_CB] = { [599] = trampoline_nop, /* find_swap_function_gated */ }; -/* - * Validate a callback record and, the first time each runtime callback whose - * length is only bounded arrives with a different length, report the length - * actually seen. Those reports are the first capture of these records. - */ bool iomfb_check_callback_h17p(struct apple_dcp *dcp, int tag, u32 in_len, u32 out_len) { - const struct dcp_callback_size *size; - - if (!iomfb_validate_callback_h17p(tag, in_len, out_len)) - return false; - size = &callback_sizes[tag]; - if (size->minimum && - (in_len != size->in_len || out_len != size->out_len) && - !test_and_set_bit(tag, dcp->sized_callbacks)) - dev_info(dcp->dev, - "callback D%03d record is %#x/%#x bytes; its handler uses %#x/%#x\n", - tag, in_len, out_len, size->in_len, size->out_len); - return true; + return iomfb_validate_callback_h17p(tag, in_len, out_len); } static_assert(sizeof(struct dcp_set_power_state_resp) == 8); @@ -738,10 +724,6 @@ static void iomfb_external_power_complete(struct apple_dcp *dcp, void *out, void header->out_len == sizeof(struct dcp_set_power_state_resp)) { u64 reply = get_unaligned_le64(out); - WRITE_ONCE(dcp->external_power_reply, reply); - WRITE_ONCE(dcp->external_power_reply_valid, true); - dev_info(dcp->dev, "A473 power=%u reply=%#llx\n", - cookie->on, reply); ret = reply == (u64)cookie->on ? 0 : -EREMOTEIO; } } @@ -798,7 +780,6 @@ int iomfb_external_power_h17p(struct apple_dcp *dcp, bool on) if (!cookie) return -ENOMEM; cookie->on = on; - WRITE_ONCE(dcp->external_power_reply_valid, false); cookie->wait.status = -ETIMEDOUT; init_completion(&cookie->wait.done); kref_init(&cookie->wait.refcount); diff --git a/drivers/gpu/drm/apple/iomfb_template.c b/drivers/gpu/drm/apple/iomfb_template.c index 34557f7ed1f4e..88d54cdd1756f 100644 --- a/drivers/gpu/drm/apple/iomfb_template.c +++ b/drivers/gpu/drm/apple/iomfb_template.c @@ -69,16 +69,8 @@ static void dcp_swap_submit(struct apple_dcp *dcp, bool oob, dcp_callback_t cb, void *cookie) { const void *data = request; - unsigned int i; if (dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17G) { - for (i = 0; i < SWAP_SURFACES; i++) { - const struct dcp_surface *surface = &request->surf[i].base; - - if (!request->surf_null[i]) - trace_iomfb_surface(dcp, i, surface->format, surface->xfer_func, - surface->colorspace, surface->is_premultiplied); - } iomfb_serialize_present_h17p(&dcp->present_h17p, request); if (!iomfb_apply_backlight_h17p(dcp, request, &dcp->present_h17p)) { if (cb) @@ -941,7 +933,30 @@ static u8 dcpep_cb_prop_start(struct apple_dcp *dcp, u32 *length) #endif dcp->chunks.length = *length; - dcp_firmware_metadata_begin(dcp); + dcp->chunks.av_service_epoch = 0; + dcp->chunks.automatic_service_pending = false; + if (dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL) { + /* Match the receipt's start, not a replacement service at its end. */ + mutex_lock(&dcp->dcpavserv.lock); + dcp->chunks.av_service_epoch = dcp->dcpavserv.epoch; + dcp->chunks.automatic_service_pending = + !dcp->dcpavserv.enabled && !dcp->dcpavserv.service; + dcp_firmware_metadata_begin(dcp); + { + unsigned long flags; + + spin_lock_irqsave(&dcp->metadata.lock, flags); + dcp->chunks.automatic_connection_sequence = dcp->rate_proof.connection_sequence; + dcp->chunks.mode_ticket = dcp_metadata_begin(&dcp->metadata.epoch); + dcp->chunks.mode_ticket.associated = + dcp->metadata.epoch.generation == READ_ONCE(dcp->typec_generation) && + READ_ONCE(dcp->typec_cable_connected); + spin_unlock_irqrestore(&dcp->metadata.lock, flags); + } + mutex_unlock(&dcp->dcpavserv.lock); + } else { + dcp_firmware_metadata_begin(dcp); + } dcp->chunks.data = kzalloc(*length, GFP_KERNEL); if (!dcp->chunks.data) { @@ -987,7 +1002,7 @@ static bool dcpep_process_chunks(struct apple_dcp *dcp, } /* Validate serialized extent outside the metadata spinlock. */ - if (dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) + if (dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL) dcp->chunks.payload_complete = dcp->chunks.contiguous && dcp_property_complete(dcp->chunks.data, dcp->chunks.received, dcp->chunks.length); @@ -999,27 +1014,71 @@ static bool dcpep_process_chunks(struct apple_dcp *dcp, if (ret) { dev_warn(dcp->dev, "bad header on dcpav props\n"); + /* A failed replacement cannot keep an unsafe catalog admitted. */ + if (dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL && + !strcmp(req->key, "TimingElements")) { + unsigned long flags; + + spin_lock_irqsave(&dcp->metadata.lock, flags); + dcp->rate_proof.catalog_sequence++; + spin_unlock_irqrestore(&dcp->metadata.lock, flags); + dcp_180hz_catalog_changed(dcp); + } return false; } if (!strcmp(req->key, "TimingElements")) { - dcp->modes = enumerate_modes(&ctx, &dcp->nr_modes, + struct dcp_display_mode *modes; + struct dcp_metadata_ticket rate_ticket = {}; + u64 rate_service_epoch = 0; + unsigned int nr_modes = 0, i; + unsigned long flags; + + if (dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL && + dcp->chunks.payload_complete && dcp->chunks.mode_ticket.associated) { + ctx.automatic_1080p180_rgb8 = true; + rate_ticket = dcp->chunks.mode_ticket; + rate_service_epoch = dcp->chunks.av_service_epoch; + } + modes = enumerate_modes(&ctx, &nr_modes, dcp->width_mm, dcp->height_mm, dcp->notch_height, dcp->fixed_connector_type == DRM_MODE_CONNECTOR_eDP, dcp->hw.iomfb_method_profile == - DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL); + DCP_IOMFB_METHODS_H17P_EXTERNAL); - if (IS_ERR(dcp->modes)) { + if (IS_ERR(modes)) { dev_warn(dcp->dev, "failed to parse modes\n"); - dcp->modes = NULL; - dcp->nr_modes = 0; + modes = NULL; + nr_modes = 0; + } else { + for (i = 0; i < nr_modes; i++) + if (modes[i].automatic_rgb8_quirk) { + modes[i].unsafe_rgb8_ticket = rate_ticket; + modes[i].unsafe_rgb8_service_epoch = rate_service_epoch; + modes[i].automatic_connection_sequence = dcp->chunks.automatic_connection_sequence; + modes[i].automatic_service_pending = dcp->chunks.automatic_service_pending; + } + } + /* Publish one catalog revision with its immutable receipt tickets. */ + spin_lock_irqsave(&dcp->metadata.lock, flags); + dcp->rate_proof.catalog_sequence++; + for (i = 0; i < nr_modes; i++) + if (modes[i].automatic_rgb8_quirk) + modes[i].automatic_catalog_sequence = dcp->rate_proof.catalog_sequence; + dcp->modes = modes; + dcp->nr_modes = nr_modes; + spin_unlock_irqrestore(&dcp->metadata.lock, flags); + /* Notify removals as well as newly eligible modes, outside the lock. */ + dcp_180hz_catalog_changed(dcp); + if (!modes) { dcp_firmware_metadata_timing(dcp); return false; } - if (dcp->nr_modes == 0) + if (nr_modes == 0) dev_warn(dcp->dev, "TimingElements without valid modes!\n"); + dcp_180hz_notify(dcp); dcp_firmware_metadata_timing(dcp); } else if (!strcmp(req->key, "DisplayAttributes")) { struct dcp_parse_ctx metadata_ctx = ctx; @@ -1033,7 +1092,7 @@ static bool dcpep_process_chunks(struct apple_dcp *dcp, } dcp_set_dimensions(dcp); - if (dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) { + if (dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL) { struct dcp_display_metadata metadata; ret = parse_display_metadata(&metadata_ctx, &metadata); @@ -2131,7 +2190,6 @@ void iomfb_present_backlight_h17p(struct apple_dcp *dcp) static void dcp_post_swap_a428(struct apple_dcp *dcp, void *out, void *cookie) { - trace_iomfb_control_result(dcp, 428, !!out, out ? get_unaligned_le32(out) : 0); do_swap(dcp, out, cookie); } @@ -2139,7 +2197,6 @@ static void dcp_pre_swap_a428(struct apple_dcp *dcp, void *out, void *cookie) { u32 identity = 0x10000; - trace_iomfb_control_result(dcp, 423, !!out, out ? get_unaligned_le32(out) : 0); dcp_pipe_set_428(dcp, false, &identity, dcp_post_swap_a428, cookie); } #endif @@ -2241,12 +2298,12 @@ int DCP_FW_NAME(iomfb_modeset)(struct apple_dcp *dcp, if (DCP_INHERIT_BOOT_MODE && dcp->hw.iomfb_method_profile != - DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) { + DCP_IOMFB_METHODS_H17P_EXTERNAL) { dev_dbg(dcp->dev, "inheriting the bootloader's display mode\n"); complete_set_digital_out_mode(dcp, NULL, cookie); } else if (mode->vrr && dcp->hw.iomfb_method_profile != - DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) + DCP_IOMFB_METHODS_H17P_EXTERNAL) dcp_set_adaptive_sync(dcp, crtc_state->vrr_enabled ? mode->min_vrr : 0, cookie); @@ -2360,7 +2417,7 @@ void DCP_FW_NAME(iomfb_flush)(struct apple_dcp *dcp, struct drm_crtc *crtc, stru * Keep YUV, other source spaces and other profiles unchanged. */ if (dcp->hw.iomfb_method_profile == - DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL && + DCP_IOMFB_METHODS_H17P_EXTERNAL && !new_state->fb->format->is_yuv && req->surf[l].base.colorspace == DCP_COLORSPACE_NATIVE) req->surf[l].base.colorspace = DCP_COLORSPACE_BG_SRGB; @@ -2430,7 +2487,6 @@ void DCP_FW_NAME(iomfb_flush)(struct apple_dcp *dcp, struct drm_crtc *crtc, stru mat.matrix[0] = mat.matrix[4] = mat.matrix[8] = 1LLU << 32; } - trace_iomfb_ctm(dcp, mat.location, mat.matrix, mat.matrix_null); #if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) iomfb_set_matrix(dcp, false, &mat, dcp_pre_swap_a428, NULL); #else diff --git a/drivers/gpu/drm/apple/parser.c b/drivers/gpu/drm/apple/parser.c index 8c2a1069417b6..0ff3c2d79e805 100644 --- a/drivers/gpu/drm/apple/parser.c +++ b/drivers/gpu/drm/apple/parser.c @@ -317,6 +317,8 @@ bool dcp_property_complete(const void *blob, size_t received, size_t advertised) static int parse_dimension(struct dcp_parse_ctx *handle, struct dimension *dim, bool polarity) { + static const char * const fields[] = { "Active", "Total", "FrontPorch", "SyncWidth", "PreciseSyncRate" }; + unsigned int seen = 0; struct iterator it; int ret = 0; @@ -336,6 +338,14 @@ static int parse_dimension(struct dcp_parse_ctx *handle, struct dimension *dim, for (; it.idx < it.len; ++it.idx) { char *key = parse_string(it.handle); + unsigned int field; + + if (!IS_ERR(key) && handle->automatic_1080p180_rgb8) + for (field = 0; field < 5; field++) + if (!strcmp(key, fields[field])) { + handle->ambiguous_180hz |= !!(seen & (1U << field)); + seen |= 1U << field; + } if (IS_ERR(key)) ret = PTR_ERR(key); @@ -526,10 +536,11 @@ static int parse_timing_color_restrictions(struct dcp_parse_ctx *handle, static int parse_color_modes(struct dcp_parse_ctx *handle, struct dcp_display_mode *out, bool external_sdr, - const struct timing_color_restrictions *limits) + const struct timing_color_restrictions *limits, + struct dcp_color_mode *unsafe_rgb8) { struct iterator outer_it; - unsigned int best_sdr_rank = 0; + unsigned int best_sdr_rank = 0, best_unsafe_rank = 0; int ret = 0; out->sdr_444.score = -1; out->sdr_rgb.score = -1; @@ -542,6 +553,8 @@ static int parse_color_modes(struct dcp_parse_ctx *handle, for (; outer_it.idx < outer_it.len; ++outer_it.idx) { struct iterator it; + static const char * const fields[] = { "Colorimetry", "Depth", "DynamicRange", "EOTF", "ID", "PixelEncoding", "Score", "IsVirtual" }; + unsigned int seen = 0; bool is_virtual = true; struct color_mode cmode = { .colorimetry = -1, @@ -559,6 +572,14 @@ static int parse_color_modes(struct dcp_parse_ctx *handle, for (; it.idx < it.len; ++it.idx) { char *key = parse_string(it.handle); + unsigned int field; + + if (!IS_ERR(key) && handle->automatic_1080p180_rgb8) + for (field = 0; field < 8; field++) + if (!strcmp(key, fields[field])) { + handle->ambiguous_180hz |= !!(seen & (1U << field)); + seen |= 1U << field; + } if (IS_ERR(key)) ret = PTR_ERR(key); @@ -601,10 +622,25 @@ static int parse_color_modes(struct dcp_parse_ctx *handle, unsigned int rank; if (cmode.id <= U32_MAX && - (timing_color_contains(&limits->unsafe, cmode.id) || - timing_color_contains(&limits->dsc_required, cmode.id))) + timing_color_contains(&limits->dsc_required, cmode.id)) continue; rank = external_sdr_rank(&cmode); + if (cmode.id <= U32_MAX && + timing_color_contains(&limits->unsafe, cmode.id)) { + /* + * Collect only a timing-local bounded RGB8 candidate. + * Admission still requires current EDID and link proof. + */ + if (unsafe_rgb8 && cmode.depth == 8 && rank && + cmode.colorimetry == DCP_COLORIMETRY_SRGB && + rank >= best_unsafe_rank) { + if (rank > best_unsafe_rank) + unsafe_rgb8->score = -1; + fill_color_mode(unsafe_rgb8, &cmode); + best_unsafe_rank = rank; + } + continue; + } if (!rank || rank < best_sdr_rank) continue; if (rank > best_sdr_rank) @@ -647,10 +683,17 @@ static int parse_mode(struct dcp_parse_ctx *handle, struct dcp_display_mode *out, s64 *score, int width_mm, int height_mm, unsigned notch_height, bool internal, bool external_sdr) { + static const char * const fields[] = { + "ID", "Score", "MinimumVariableRefreshRate", "MaximumVariableRefreshRate", + }; + unsigned int seen = 0; int ret = 0; struct iterator it; struct dimension horiz = { .active = -1 }, vert = { .active = -1 }; struct timing_color_restrictions limits = {}; + struct dcp_color_mode unsafe_rgb8 = { .score = -1 }; + bool have_horiz = false, have_vert = false, have_virtual = false; + bool duplicate_scope = false; s64 min_vrr = 0, max_vrr = 0; u64 clock; s64 id = -1; @@ -667,6 +710,7 @@ static int parse_mode(struct dcp_parse_ctx *handle, .best.score = -1, }; *score = -1; + handle->ambiguous_180hz = false; if (external_sdr) { struct dcp_parse_ctx scan = *handle; @@ -682,26 +726,42 @@ static int parse_mode(struct dcp_parse_ctx *handle, for (; it.idx < it.len; ++it.idx) { char *key = parse_string(it.handle); + unsigned int field; + + if (!IS_ERR(key) && handle->automatic_1080p180_rgb8) + for (field = 0; field < ARRAY_SIZE(fields); field++) + if (!strcmp(key, fields[field])) { + handle->ambiguous_180hz |= !!(seen & (1U << field)); + seen |= 1U << field; + } if (IS_ERR(key)) ret = PTR_ERR(key); else if (is_virtual) ret = skip(it.handle); - else if (!strcmp(key, "HorizontalAttributes")) + else if (!strcmp(key, "HorizontalAttributes")) { + duplicate_scope |= have_horiz; + have_horiz = true; ret = parse_dimension(it.handle, &horiz, external_sdr); - else if (!strcmp(key, "VerticalAttributes")) + } else if (!strcmp(key, "VerticalAttributes")) { + duplicate_scope |= have_vert; + have_vert = true; ret = parse_dimension(it.handle, &vert, external_sdr); - else if (!strcmp(key, "MinimumVariableRefreshRate")) + } else if (!strcmp(key, "MinimumVariableRefreshRate")) ret = parse_int_bound(it.handle, &min_vrr, 0, U32_MAX); else if (!strcmp(key, "MaximumVariableRefreshRate")) ret = parse_int_bound(it.handle, &max_vrr, 0, U32_MAX); else if (!strcmp(key, "ColorModes")) - ret = parse_color_modes(it.handle, out, external_sdr, &limits); + ret = parse_color_modes(it.handle, out, external_sdr, &limits, + handle->automatic_1080p180_rgb8 ? + &unsafe_rgb8 : NULL); else if (!strcmp(key, "ID")) ret = parse_int_bound(it.handle, &id, 0, U32_MAX); - else if (!strcmp(key, "IsVirtual")) + else if (!strcmp(key, "IsVirtual")) { + duplicate_scope |= have_virtual; + have_virtual = true; ret = parse_bool(it.handle, &is_virtual); - else if (external_sdr && !strcmp(key, "IsPreferred")) { + } else if (external_sdr && !strcmp(key, "IsPreferred")) { if (have_preferred) ret = -EINVAL; else @@ -726,6 +786,27 @@ static int parse_mode(struct dcp_parse_ctx *handle, return ret; } } + /* + * Never replace a normal candidate or create a timing/color pairing. + * Only the bounded automatic policy may publish or use this candidate. + * Its EDID identity and successful link callbacks must remain current. + * The observed 2080x1106 total and 11796583/65536 Hz are deliberate: + * do not widen this to arbitrary nominal-180 Hz blanking or rates. + */ + if (external_sdr && !internal && + handle->automatic_1080p180_rgb8 && + !handle->ambiguous_180hz && + out->sdr_rgb.score < 0 && unsafe_rgb8.score >= 0 && + !duplicate_scope && have_horiz && have_vert && have_virtual && !is_virtual && + have_interlaced && !interlaced && !notch_height && + horiz.active == 1920 && vert.active == 1080 && + horiz.total == 2080 && vert.total == 1106 && + vert.precise_sync_rate == 11796583 && + horiz.pixel_repetition == 0 && vert.pixel_repetition == 0 && + horiz.sync_polarity >= 0 && vert.sync_polarity >= 0) { + out->sdr_rgb = unsafe_rgb8; + out->automatic_rgb8_quirk = handle->automatic_1080p180_rgb8; + } if (out->sdr_rgb.score >= 0) best_color_mode = out->sdr_rgb.id; else if (out->sdr_444.score >= 0) @@ -765,8 +846,12 @@ static int parse_mode(struct dcp_parse_ctx *handle, out->vrr = true; } - /* Refresh rates are reported by DCP as 16.16 fixed-point Hz. */ - if (min_vrr && max_vrr > min_vrr) { + /* + * Refresh rates are reported by DCP as 16.16 fixed-point Hz. + * External H17P skips adaptive-sync programming: keep the fixed timing + * available, but do not advertise its sink VRR range as driver support. + */ + if (!external_sdr && min_vrr && max_vrr > min_vrr) { out->min_vrr = min_vrr; out->max_vrr = max_vrr; out->vrr = true; @@ -802,7 +887,8 @@ static int parse_mode(struct dcp_parse_ctx *handle, if (external_sdr && horiz.sync_polarity >= 0 && vert.sync_polarity >= 0) mode->flags = (horiz.sync_polarity ? DRM_MODE_FLAG_PHSYNC : DRM_MODE_FLAG_NHSYNC) | (vert.sync_polarity ? DRM_MODE_FLAG_PVSYNC : DRM_MODE_FLAG_NVSYNC); - out->metadata_timing_valid = external_sdr && have_preferred && preferred && + out->metadata_timing_valid = external_sdr && !out->automatic_rgb8_quirk && + have_preferred && preferred && have_interlaced && !interlaced && horiz.pixel_repetition == 0 && vert.pixel_repetition == 0 && horiz.sync_polarity >= 0 && vert.sync_polarity >= 0; drm_mode_set_name(mode); @@ -817,11 +903,110 @@ static int parse_mode(struct dcp_parse_ctx *handle, return 0; } +/* + * Inspect command identities before filtering can discard conflicting siblings. + * Only automatic exceptions use this conservative receipt-wide check. + */ +static bool dcp_180hz_color_ids_current(struct dcp_parse_ctx *handle, u32 id) +{ + static const char * const fields[] = { + "ID", "Depth", "DynamicRange", "EOTF", "PixelEncoding", "Colorimetry", + }; + struct iterator colors; + + if (iterator_begin(handle, &colors, false)) + return false; + for (; colors.idx < colors.len; colors.idx++) { + struct color_mode color = { .id = -1, .depth = -1, .dynamic_range = -1, + .eotf = -1, .pixel_encoding = -1, .colorimetry = -1 }; + s64 *values[] = { &color.id, &color.depth, &color.dynamic_range, + &color.eotf, &color.pixel_encoding, &color.colorimetry }; + bool is_virtual = true, virtual_seen = false; + unsigned int seen = 0; + struct iterator entry; + + if (iterator_begin(handle, &entry, true)) + return false; + for (; entry.idx < entry.len; entry.idx++) { + char *key = parse_string(handle); + unsigned int field; + int ret; + + if (IS_ERR(key)) + return false; + for (field = 0; field < ARRAY_SIZE(fields); field++) + if (!strcmp(key, fields[field])) + break; + if (field < ARRAY_SIZE(fields)) { + ret = seen & (1U << field) ? -EINVAL : + parse_int_bound(handle, values[field], 0, U32_MAX); + seen |= 1U << field; + } else if (!strcmp(key, "IsVirtual")) { + ret = virtual_seen ? -EINVAL : parse_bool(handle, &is_virtual); + virtual_seen = true; + } else { + ret = skip(handle); + } + kfree(key); + if (ret) + return false; + } + if (color.id == id && + (is_virtual || !virtual_seen || color.depth != 8 || + color.dynamic_range != DCP_COLOR_YCBCR_RANGE_FULL || + color.eotf != DCP_EOTF_SDR_GAMMA || + color.pixel_encoding != DCP_COLOR_FORMAT_RGB || + color.colorimetry != DCP_COLORIMETRY_SRGB)) + return false; + } + return true; +} + +static bool dcp_180hz_ids_current(struct dcp_parse_ctx handle, + const struct dcp_display_mode *mode) +{ + struct iterator timings; + unsigned int matches = 0; + + if (iterator_begin(&handle, &timings, false)) + return false; + for (; timings.idx < timings.len; timings.idx++) { + struct iterator entry; + + if (iterator_begin(&handle, &entry, true)) + return false; + for (; entry.idx < entry.len; entry.idx++) { + char *key = parse_string(&handle); + int ret = 0; + + if (IS_ERR(key)) + return false; + if (!strcmp(key, "ID")) { + s64 id; + + ret = parse_int_bound(&handle, &id, 0, U32_MAX); + if (!ret && id == mode->timing_mode_id) + matches++; + } else if (!strcmp(key, "ColorModes")) { + if (!dcp_180hz_color_ids_current(&handle, mode->color_mode_id)) + ret = -EINVAL; + } else { + ret = skip(&handle); + } + kfree(key); + if (ret || matches > 1) + return false; + } + } + return matches == 1; +} + struct dcp_display_mode *enumerate_modes(struct dcp_parse_ctx *handle, unsigned int *count, int width_mm, int height_mm, unsigned notch_height, bool internal, bool external_sdr) { + struct dcp_parse_ctx catalog = *handle; struct iterator it; int ret; struct dcp_display_mode *mode, *modes; @@ -862,7 +1047,7 @@ struct dcp_display_mode *enumerate_modes(struct dcp_parse_ctx *handle, /* Process a successful mode */ (*count)++; - if (score > best_score) { + if (!mode->automatic_rgb8_quirk && score > best_score) { best_score = score; best_mode = mode; } @@ -871,6 +1056,17 @@ struct dcp_display_mode *enumerate_modes(struct dcp_parse_ctx *handle, if (best_mode != NULL) best_mode->mode.type |= DRM_MODE_TYPE_PREFERRED; + if (handle->automatic_1080p180_rgb8) { + unsigned int i, kept = 0; + + for (i = 0; i < *count; i++) { + if (modes[i].automatic_rgb8_quirk && + !dcp_180hz_ids_current(catalog, &modes[i])) + continue; + modes[kept++] = modes[i]; + } + *count = kept; + } return modes; } @@ -1084,7 +1280,7 @@ static int parse_metadata_product(struct dcp_parse_ctx *handle, struct dcp_displ for (unsigned int i = 0; i < 3; i++) if (out->manufacturer[i] < 'A' || out->manufacturer[i] > 'Z') return -EINVAL; - if ((seen & 32) && legacy != ((out->manufacturer[0] - '@') << 10 | + if ((seen & 32) && legacy != (u32)((out->manufacturer[0] - '@') << 10 | (out->manufacturer[1] - '@') << 5 | (out->manufacturer[2] - '@'))) return -EINVAL; diff --git a/drivers/gpu/drm/apple/parser.h b/drivers/gpu/drm/apple/parser.h index 0392b09751ac9..3b21263154754 100644 --- a/drivers/gpu/drm/apple/parser.h +++ b/drivers/gpu/drm/apple/parser.h @@ -8,6 +8,7 @@ #include #include "dcp-display-metadata.h" +#include "dcp-metadata-epoch.h" struct apple_dcp; @@ -15,6 +16,9 @@ struct dcp_parse_ctx { struct apple_dcp *dcp; const void *blob; u32 pos, len; + /* External H17P candidates require separate publication/lookup proof. */ + bool automatic_1080p180_rgb8; + bool ambiguous_180hz; }; enum dcp_color_eotf { @@ -97,6 +101,13 @@ struct dcp_display_mode { struct dcp_color_mode best; bool vrr; bool metadata_timing_valid; + bool automatic_rgb8_quirk; + u64 automatic_connection_sequence; + u64 automatic_catalog_sequence; + bool automatic_service_pending; + /* Immutable receipt provenance; never retag a cached exception. */ + struct dcp_metadata_ticket unsafe_rgb8_ticket; + u64 unsafe_rgb8_service_epoch; }; struct dimension { diff --git a/drivers/gpu/drm/apple/trace.h b/drivers/gpu/drm/apple/trace.h index 03f23ecf0a78e..cbb3f4080fd57 100644 --- a/drivers/gpu/drm/apple/trace.h +++ b/drivers/gpu/drm/apple/trace.h @@ -76,34 +76,6 @@ { DPTX_APCALL_DEVICE_BUSY_TIMEOUT, "device_busy_timeout" }, \ { DPTX_APCALL_DEVICE_NOT_STARTED, "device_not_started" }) -TRACE_EVENT(iomfb_metadata, - TP_PROTO(struct apple_dcp *dcp, const char *phase, u64 generation, - u64 ticket_generation, u32 flags, u32 received, u32 length), - TP_ARGS(dcp, phase, generation, ticket_generation, flags, received, length), - TP_STRUCT__entry( - __string(devname, dev_name(dcp->dev)) - __string(phase, phase) - __field(u64, generation) - __field(u64, ticket_generation) - __field(u32, flags) - __field(u32, received) - __field(u32, length) - ), - TP_fast_assign( - __assign_str(devname); - __assign_str(phase); - __entry->generation = generation; - __entry->ticket_generation = ticket_generation; - __entry->flags = flags; - __entry->received = received; - __entry->length = length; - ), - TP_printk("%s: %s generation=%llu ticket=%llu flags=%#x received=%u length=%u", - __get_str(devname), __get_str(phase), __entry->generation, - __entry->ticket_generation, __entry->flags, - __entry->received, __entry->length) -); - TRACE_EVENT(dcp_recv_msg, TP_PROTO(struct apple_dcp *dcp, u8 endpoint, u64 message), TP_ARGS(dcp, endpoint, message), @@ -233,126 +205,6 @@ TRACE_EVENT( print_epic_category(__entry->category), __entry->subtype, __entry->tag)); -/* Raw prefix before any protocol-specific decode; no peer address is followed. */ -TRACE_EVENT(afk_recv_header, - TP_PROTO(struct apple_dcp_afkep *ep, u32 channel, u32 type, - size_t data_size, const void *data), - TP_ARGS(ep, channel, type, data_size, data), - TP_STRUCT__entry(__string(devname, dev_name(ep->dcp->dev)) - __field(u8, endpoint) - __field(u32, channel) - __field(u32, type) - __field(size_t, data_size) - __field(u8, captured) - __array(u8, header, 32)), - TP_fast_assign(__assign_str(devname); - __entry->endpoint = ep->endpoint; - __entry->channel = channel; - __entry->type = type; - __entry->data_size = data_size; - __entry->captured = data ? min_t(size_t, data_size, 32) : 0; - memset(__entry->header, 0, sizeof(__entry->header)); - if (__entry->captured) - memcpy(__entry->header, data, __entry->captured); - ), - TP_printk("%s: endpoint 0x%x channel 0x%x type 0x%x size %zu header[%u]=%s", - __get_str(devname), __entry->endpoint, __entry->channel, - __entry->type, __entry->data_size, __entry->captured, - __print_hex(__entry->header, __entry->captured)) -); - -TRACE_EVENT(afk_ring_snapshot, - TP_PROTO(struct apple_dcp_afkep *ep, bool rx_ready, u32 rx_rptr, - u32 rx_wptr, u32 rx_size, bool tx_ready, u32 tx_rptr, - u32 tx_wptr, u32 tx_size), - TP_ARGS(ep, rx_ready, rx_rptr, rx_wptr, rx_size, - tx_ready, tx_rptr, tx_wptr, tx_size), - TP_STRUCT__entry(__string(devname, dev_name(ep->dcp->dev)) - __field(u8, endpoint) - __field(bool, rx_ready) - __field(u32, rx_rptr) - __field(u32, rx_wptr) - __field(u32, rx_size) - __field(bool, tx_ready) - __field(u32, tx_rptr) - __field(u32, tx_wptr) - __field(u32, tx_size)), - TP_fast_assign(__assign_str(devname); - __entry->endpoint = ep->endpoint; - __entry->rx_ready = rx_ready; - __entry->rx_rptr = rx_rptr; - __entry->rx_wptr = rx_wptr; - __entry->rx_size = rx_size; - __entry->tx_ready = tx_ready; - __entry->tx_rptr = tx_rptr; - __entry->tx_wptr = tx_wptr; - __entry->tx_size = tx_size; - ), - TP_printk("%s: endpoint 0x%x rx_ready %u rx_rptr %#x rx_wptr %#x rx_size %#x tx_ready %u tx_rptr %#x tx_wptr %#x tx_size %#x", - __get_str(devname), __entry->endpoint, __entry->rx_ready, - __entry->rx_rptr, __entry->rx_wptr, __entry->rx_size, - __entry->tx_ready, __entry->tx_rptr, __entry->tx_wptr, - __entry->tx_size) -); - -TRACE_EVENT(iomfb_ctm, - TP_PROTO(struct apple_dcp *dcp, u32 location, const void *matrix, u8 matrix_null), - TP_ARGS(dcp, location, matrix, matrix_null), - TP_STRUCT__entry(__string(devname, dev_name(dcp->dev)) - __field(u32, location) - __field(u8, matrix_null) - __array(u64, matrix, 9)), - TP_fast_assign(__assign_str(devname); - __entry->location = location; - __entry->matrix_null = matrix_null; - if (matrix) - memcpy(__entry->matrix, matrix, sizeof(__entry->matrix)); - else - memset(__entry->matrix, 0, sizeof(__entry->matrix)); - ), - TP_printk("%s: location %u null %u matrix=%s", - __get_str(devname), __entry->location, __entry->matrix_null, - __print_array(__entry->matrix, 9, sizeof(u64))) -); - -TRACE_EVENT(iomfb_control_result, - TP_PROTO(struct apple_dcp *dcp, u32 method, bool valid, u32 result), - TP_ARGS(dcp, method, valid, result), - TP_STRUCT__entry(__string(devname, dev_name(dcp->dev)) - __field(u32, method) - __field(bool, valid) - __field(u32, result)), - TP_fast_assign(__assign_str(devname); - __entry->method = method; - __entry->valid = valid; - __entry->result = result; - ), - TP_printk("%s: A%u reply_valid %u result 0x%08x", - __get_str(devname), __entry->method, __entry->valid, __entry->result) -); - -TRACE_EVENT(iomfb_surface, - TP_PROTO(struct apple_dcp *dcp, u32 index, u32 format, u8 xfer_func, - u8 colorspace, u8 premultiplied), - TP_ARGS(dcp, index, format, xfer_func, colorspace, premultiplied), - TP_STRUCT__entry(__string(devname, dev_name(dcp->dev)) - __field(u32, index) - __field(u32, format) - __field(u8, xfer_func) - __field(u8, colorspace) - __field(u8, premultiplied)), - TP_fast_assign(__assign_str(devname); - __entry->index = index; - __entry->format = format; - __entry->xfer_func = xfer_func; - __entry->colorspace = colorspace; - __entry->premultiplied = premultiplied; - ), - TP_printk("%s: surface %u format 0x%08x xfer %u colorspace %u source_premul %u", - __get_str(devname), __entry->index, __entry->format, - __entry->xfer_func, __entry->colorspace, __entry->premultiplied) -); - TRACE_EVENT(iomfb_callback, TP_PROTO(struct apple_dcp *dcp, int tag, const char *name), TP_ARGS(dcp, tag, name), diff --git a/drivers/media/platform/apple/isp/isp-drv.c b/drivers/media/platform/apple/isp/isp-drv.c index 71ba9c7939470..76c28c0dc9f7c 100644 --- a/drivers/media/platform/apple/isp/isp-drv.c +++ b/drivers/media/platform/apple/isp/isp-drv.c @@ -309,12 +309,39 @@ static const char * isp_fw2str(enum isp_firmware_version version) #define ISP_FW_VERSION_MAX_LEN 5 static enum isp_firmware_version isp_read_fw_version(struct device *dev, - const char *name) + const char *name, + enum isp_fw_abi abi) { u32 ver[ISP_FW_VERSION_MAX_LEN]; - int len = of_property_read_variable_u32_array(dev->of_node, name, ver, - ISP_FW_VERSION_MIN_LEN, - ISP_FW_VERSION_MAX_LEN); + int size, len; + + /* The array reader truncates trailing bytes; require complete cells. */ + if (of_find_property(dev->of_node, name, &size) && size % sizeof(u32)) { + dev_warn(dev, "could not parse %s: invalid length %d\n", name, size); + return ISP_FIRMWARE_V_UNKNOWN; + } + + len = of_property_read_variable_u32_array(dev->of_node, name, ver, + ISP_FW_VERSION_MIN_LEN, + ISP_FW_VERSION_MAX_LEN); + + /* H17 command layouts come from match data, not the legacy version table. */ + if (abi == ISP_FW_ABI_H17) { + switch (len) { + case 3: + dev_info(dev, "%s: %u.%u.%u (H17 interface)\n", + name, ver[0], ver[1], ver[2]); + return ISP_FIRMWARE_V_UNKNOWN; + case 4: + dev_info(dev, "%s: %u.%u.%u.%u (H17 interface)\n", + name, ver[0], ver[1], ver[2], ver[3]); + return ISP_FIRMWARE_V_UNKNOWN; + case 5: + dev_info(dev, "%s: %u.%u.%u.%u.%u (H17 interface)\n", + name, ver[0], ver[1], ver[2], ver[3], ver[4]); + return ISP_FIRMWARE_V_UNKNOWN; + } + } switch (len) { case -EINVAL: @@ -355,8 +382,8 @@ isp_check_firmware_version(struct apple_isp *isp) enum isp_firmware_version version, compat; /* firmware version is just informative */ - version = isp_read_fw_version(dev, "apple,firmware-version"); - compat = isp_read_fw_version(dev, "apple,firmware-compat"); + version = isp_read_fw_version(dev, "apple,firmware-version", isp->hw->fw_abi); + compat = isp_read_fw_version(dev, "apple,firmware-compat", isp->hw->fw_abi); dev_dbg(dev, "ISP firmware-compat: %s (FW: %s)\n", isp_fw2str(compat), isp_fw2str(version)); diff --git a/drivers/phy/apple/Kconfig b/drivers/phy/apple/Kconfig index 8409b67f6d1ec..26cd09c05497c 100644 --- a/drivers/phy/apple/Kconfig +++ b/drivers/phy/apple/Kconfig @@ -3,6 +3,7 @@ config PHY_APPLE_ATC tristate "Apple Type-C PHY" depends on (ARM64 && ARCH_APPLE) || (COMPILE_TEST && !GENERIC_ATOMIC64) depends on TYPEC + depends on USB || !USB select GENERIC_PHY select APPLE_TUNABLE help diff --git a/drivers/phy/apple/atc-j700-dp.h b/drivers/phy/apple/atc-j700-dp.h index ead9ea6be3f3a..a07f1125c2c43 100644 --- a/drivers/phy/apple/atc-j700-dp.h +++ b/drivers/phy/apple/atc-j700-dp.h @@ -3,6 +3,159 @@ #ifndef APPLE_ATC_J700_DP_H #define APPLE_ATC_J700_DP_H +/* Masks are the union of writes in the public J700 DP start/voltage/stop paths. */ +struct j700_usb_field { + u32 offset, mask; +}; + +static const struct j700_usb_field j700_usb_fields[] = { + { 0x0008, 0x00015540 }, + { 0x000c, 0x00000055 }, + { 0x010c, 0x00000555 }, + { 0x0200, 0x0000000f }, + { 0x9160, 0x00000001 }, + { 0x91d4, 0x00000003 }, + { 0x91dc, 0x00018000 }, + { 0x9210, 0x0000c00f }, + { 0x9244, 0x0007ffff }, + { 0x9248, 0x10000000 }, + { 0xb030, 0x00000c00 }, + { 0xb064, 0x0000fc0c }, + { 0xb068, 0x000001e0 }, + { 0xb06c, 0x0001ee00 }, + { 0xb0a4, 0x00000003 }, + { 0xb0a8, 0x00ff8000 }, + { 0xb0d4, 0x001fff80 }, + { 0xb0d8, 0x00000007 }, + { 0xb0dc, 0x00000018 }, + { 0xc050, 0x0000c00f }, + { 0xc0d0, 0x0017ffff }, + { 0xc0d4, 0x10000000 }, + { 0xd004, 0x33000000 }, + { 0xd00c, 0x07000000 }, + { 0xd010, 0x00000600 }, + { 0xd01c, 0x00c00000 }, + { 0xd044, 0xffffffff }, +}; + +static_assert(ARRAY_SIZE(j700_usb_fields) == 27, "USB context storage size"); + +static void j700_usb_field(unsigned int index, unsigned int lane, u32 *offset, u32 *mask) +{ + *offset = j700_usb_fields[index].offset; + *mask = j700_usb_fields[index].mask; + if (index < 4) + *mask <<= index == 3 ? 4 * lane : lane; + else + *offset += 0x7000 * lane; +} + +/* Capture before the first DP tunable or idle write; never replace a retained context. */ +static int j700_usb_capture(struct apple_atcphy *p, enum atcphy_mode mode) +{ + const struct atcphy_mode_configuration *cfg = atcphy_get_mode_config(p, mode); + + if (!p->hw->has_t8140_dp || !p->fixed_usb2) + return 0; + if (p->usb_off_pending || + p->pipe_state != ATCPHY_PIPEHANDLER_STATE_DUMMY || p->pipe_pending) + return -EBUSY; + for (unsigned int lane = 0; lane < 2; lane++) { + if (!cfg->dp_lane[lane] || (p->dp_usb_context_valid & BIT(lane))) + continue; + for (unsigned int i = 0; i < ARRAY_SIZE(j700_usb_fields); i++) { + u32 offset, mask; + + j700_usb_field(i, lane, &offset, &mask); + p->dp_usb_context[lane][i] = core_read32(p, offset) & mask; + } + p->dp_usb_context_valid |= BIT(lane); + } + return 0; +} + +/* Same control write/delay/poll sequence, retaining its final sample for diagnostics. */ +static int j700_usb_field_update(struct apple_atcphy *p, u32 offset, u32 mask, + u32 value, u32 *observed) +{ + core_mask32(p, offset, mask, value); + usleep_range(10, 20); + return readl_poll_timeout_atomic(p->regs.core + offset, *observed, + (*observed & mask) == value, 1, 100); +} + +/* Restore only DP-owned bits, with the PIPE parked and the DP PMA idle. */ +static int j700_usb_restore(struct apple_atcphy *p, enum atcphy_mode mode) +{ + const struct atcphy_mode_configuration *cfg = atcphy_get_mode_config(p, mode); + const unsigned int power_order[] = { 2, 1, 0, 3 }; + const unsigned int receiver_order[] = { 4, 0, 6, 2 }; + int ret; + + if (!p->hw->has_t8140_dp || !p->fixed_usb2) + return 0; + if (p->usb_off_pending && mode != APPLE_ATCPHY_MODE_OFF) + return -EBUSY; + if (p->pipe_state != ATCPHY_PIPEHANDLER_STATE_DUMMY || p->pipe_pending) + return -EBUSY; + if (p->dp_owned_pmas || p->dp_idle_pmas) + return -EBUSY; + if (mode == APPLE_ATCPHY_MODE_OFF && p->usb_off_drained) + return 0; + for (unsigned int lane = 0; lane < 2; lane++) { + if ((mode != APPLE_ATCPHY_MODE_OFF && + cfg->lane_mode[lane] != ACIOPHY_LANE_MODE_USB3) || + !(p->dp_usb_context_valid & BIT(lane))) + continue; + if (p->dp_usb_context_dirty & BIT(lane)) { + for (unsigned int i = 0; i < ARRAY_SIZE(j700_usb_fields); i++) { + u32 offset, mask, observed, value = p->dp_usb_context[lane][i]; + + if (i == 1 || i == 3) + continue; + j700_usb_field(i, lane, &offset, &mask); + core_mask32(p, offset, mask, value); + observed = core_read32(p, offset) & mask; + if (observed != value) { + dev_dbg(p->dev, "USB restore stage=static pma=%u reg=%#x mask=%#x expected=%#x observed=%#x\n", + lane, offset, mask, value, observed); + return -EIO; + } + } + } + /* Preserve the public PMA power bit order, using saved values. */ + for (unsigned int i = 0; i < ARRAY_SIZE(power_order); i++) { + u32 mask = BIT(4 * lane + power_order[i]); + u32 value = p->dp_usb_context[lane][3] & mask, observed; + + ret = j700_usb_field_update(p, 0x200, mask, value, &observed); + if (ret) { + dev_dbg(p->dev, "USB restore stage=power pma=%u reg=0x200 mask=%#x expected=%#x observed=%#x ret=%d\n", + lane, mask, value, observed & mask, ret); + return ret; + } + } + /* Restore receiver enables, then release its two idle controls. */ + for (unsigned int i = 0; i < ARRAY_SIZE(receiver_order); i++) { + u32 mask = BIT(lane + receiver_order[i]); + u32 value = p->dp_usb_context[lane][1] & mask, observed; + + ret = j700_usb_field_update(p, 0xc, mask, value, &observed); + if (ret) { + dev_dbg(p->dev, "USB restore stage=receiver pma=%u reg=0xc mask=%#x expected=%#x observed=%#x ret=%d\n", + lane, mask, value, observed & mask, ret); + return ret; + } + } + /* OFF keeps the shadow until domain shutdown succeeds. */ + if (mode != APPLE_ATCPHY_MODE_OFF) { + p->dp_usb_context_dirty &= ~BIT(lane); + p->dp_usb_context_valid &= ~BIT(lane); + } + } + return 0; +} + /* Retry only the idle operations claimed by an incomplete route change. */ static int j700_dp_idle_pending(struct apple_atcphy *p) { @@ -269,6 +422,11 @@ static int j700_dp_start(struct apple_atcphy *p, unsigned int pmas, int rate, bo if (!pmas || (pmas & ~3U) || rate < 0 || (unsigned int)rate >= ARRAY_SIZE(parameters)) return -EINVAL; + if (p->hw->has_t8140_dp && p->fixed_usb2) { + if ((p->dp_usb_context_valid & pmas) != pmas) + return -EIO; + p->dp_usb_context_dirty |= pmas; + } /* Claim before the first write: failed setup must still be cleaned up. */ p->dp_owned_pmas = pmas; p->dp_link_rate = -1; @@ -320,7 +478,7 @@ static int j700_dp_configure(struct apple_atcphy *p, const struct phy_configure_ 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) + if (p->mode_dirty || p->pipe_pending || p->usb_off_pending) return -EBUSY; if (p->mode != APPLE_ATCPHY_MODE_DP && p->mode != APPLE_ATCPHY_MODE_USB3_DP) return -EINVAL; diff --git a/drivers/phy/apple/atc.c b/drivers/phy/apple/atc.c index ea04dec0f0d05..adb2ecdb310f6 100644 --- a/drivers/phy/apple/atc.c +++ b/drivers/phy/apple/atc.c @@ -33,12 +33,16 @@ #include #include #include +#include #include #include +#include #include #include #include #include +#include +#include #include #include #include @@ -654,9 +658,15 @@ 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 + * @mode_dirty: 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_usb_context_valid: Physical PMAs retaining their pre-DP USB field values + * @dp_usb_context_dirty: PMAs whose static fields were programmed by DP + * @usb_off_pending: Failed full OFF must finish before a new powered epoch + * @usb_off_drained: Masked recovery data was discharged before OFF began + * @pipe_pending: PIPE handoff still requires acknowledged dummy parking + * @dp_usb_context: Saved bits from the public per-PMA DP write masks * @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 @@ -722,6 +732,9 @@ struct apple_atcphy { bool mode_dirty; u8 dp_idle_pmas; u8 dp_owned_pmas; + u8 dp_usb_context_valid, dp_usb_context_dirty; + u32 dp_usb_context[2][27]; + bool usb_off_pending, usb_off_drained, pipe_pending; bool dp_spread_spectrum; u32 dp_presets[5][16]; bool dp_presets_valid[5]; @@ -1011,6 +1024,13 @@ static const struct atcphy_mode_configuration *atcphy_get_mode_config(struct app return &atcphy_modes[mode].normal; } +/* Readback only; callers hold the PHY mutex and keep the fixed host live. */ +static inline u32 core_read32(struct apple_atcphy *atcphy, u32 reg) +{ + return readl(atcphy->regs.core + reg); +} + +/* Static configuration fields already read/modified by the public DP path. */ static void j700_dp_tunables(struct apple_atcphy *atcphy) { apple_tunable_apply(atcphy->regs.axi2af, atcphy->tunables.axi2af); @@ -1083,17 +1103,22 @@ static int atcphy_pipehandler_lock(struct apple_atcphy *atcphy) { int ret; u32 reg; + u32 requested = readl(atcphy->regs.pipehandler + PIPEHANDLER_LOCK_REQ); - if (readl(atcphy->regs.pipehandler + PIPEHANDLER_LOCK_REQ) & PIPEHANDLER_LOCK_EN) { - dev_warn(atcphy->dev, "Pipehandler already locked\n"); - return 0; + dev_dbg(atcphy->dev, "PIPE request: req=%#x mode=%u pipe=%u host=%u dirty=%u\n", + requested, atcphy->mode, atcphy->pipe_state, + atcphy->host_active, atcphy->mode_dirty); + if (requested & PIPEHANDLER_LOCK_EN) { + dev_dbg(atcphy->dev, "Pipehandler lock already requested\n"); + } else { + set32(atcphy->regs.pipehandler + PIPEHANDLER_LOCK_REQ, PIPEHANDLER_LOCK_EN); } - set32(atcphy->regs.pipehandler + PIPEHANDLER_LOCK_REQ, PIPEHANDLER_LOCK_EN); - ret = readl_poll_timeout(atcphy->regs.pipehandler + PIPEHANDLER_LOCK_ACK, reg, reg & PIPEHANDLER_LOCK_EN, 10, PIPEHANDLER_LOCK_ACK_TIMEOUT_US); if (ret) { + dev_dbg(atcphy->dev, "PIPE lock timeout: ack=%#x mode=%u pipe=%u dirty=%u\n", + reg, atcphy->mode, atcphy->pipe_state, atcphy->mode_dirty); clear32(atcphy->regs.pipehandler + PIPEHANDLER_LOCK_REQ, 1); dev_warn(atcphy->dev, "Pipehandler lock not acked.\n"); } @@ -1109,8 +1134,11 @@ static int atcphy_pipehandler_unlock(struct apple_atcphy *atcphy) clear32(atcphy->regs.pipehandler + PIPEHANDLER_LOCK_REQ, PIPEHANDLER_LOCK_EN); ret = readl_poll_timeout(atcphy->regs.pipehandler + PIPEHANDLER_LOCK_ACK, reg, !(reg & PIPEHANDLER_LOCK_EN), 10, PIPEHANDLER_LOCK_ACK_TIMEOUT_US); - if (ret) + if (ret) { + dev_dbg(atcphy->dev, "PIPE unlock timeout: ack=%#x mode=%u pipe=%u dirty=%u\n", + reg, atcphy->mode, atcphy->pipe_state, atcphy->mode_dirty); dev_warn(atcphy->dev, "Pipehandler lock release not acked.\n"); + } return ret; } @@ -1156,9 +1184,9 @@ static void atcphy_clear_nonselected_phy_reset(struct apple_atcphy *atcphy) PIPEHANDLER_NATIVE_RESET); } -static void atcphy_configure_pipehandler_usb3_host_t8122(struct apple_atcphy *atcphy) +static int atcphy_configure_pipehandler_usb3_host_t8122(struct apple_atcphy *atcphy) { - int ret; + int ret, error = 0; u32 reg; core_set32(atcphy, ACIOPHY_BIST_CFG1, ACIOPHY_BIST_CFG1_CLOCK_EN); @@ -1171,15 +1199,25 @@ static void atcphy_configure_pipehandler_usb3_host_t8122(struct apple_atcphy *at core_set32(atcphy, ACIOPHY_BIST_CFG1, ACIOPHY_BIST_CFG1_USB_EN); ret = readl_poll_timeout(atcphy->regs.core + ACIOPHY_BIST_CFG1, reg, (reg & ACIOPHY_BIST_CFG1_USB_EN), 10, 10000); - if (ret) + if (ret) { + if (!error) + error = ret; + dev_dbg(atcphy->dev, "BIST USB_EN phase=1 value=%#x mode=%u pipe=%u\n", + reg, atcphy->mode, atcphy->pipe_state); dev_warn(atcphy->dev, "Timed out waiting for ACIOPHY_BIST_CFG1_USB_EN\n"); + } core_clear32(atcphy, ACIOPHY_BIST_CFG1, ACIOPHY_BIST_CFG1_USB_EN); ret = readl_poll_timeout(atcphy->regs.core + ACIOPHY_BIST_MAC_USBPHY_STATUS2, reg, !(reg & ACIOPHY_BIST_MAC_USBPHY_STATUS2_LN0_PHY_STATUS2), 10, 10000); - if (ret) + if (ret) { + if (!error) + error = ret; + dev_dbg(atcphy->dev, "BIST STATUS2 phase=1 value=%#x swap=%u mode=%u pipe=%u\n", + reg, atcphy->swap_lanes, atcphy->mode, atcphy->pipe_state); dev_warn(atcphy->dev, "Timed out waiting for ACIOPHY_BIST_MAC_USBPHY_STATUS2_LN0_PHY_STATUS2\n"); + } core_set32(atcphy, ACIOPHY_BIST_CFG2, ACIOPHY_BIST_CFG2_LN0_PHY_STATUS_RE); udelay(10); @@ -1192,15 +1230,25 @@ static void atcphy_configure_pipehandler_usb3_host_t8122(struct apple_atcphy *at core_set32(atcphy, ACIOPHY_BIST_CFG1, ACIOPHY_BIST_CFG1_USB_EN); ret = readl_poll_timeout(atcphy->regs.core + ACIOPHY_BIST_CFG1, reg, (reg & ACIOPHY_BIST_CFG1_USB_EN), 10, 10000); - if (ret) + if (ret) { + if (!error) + error = ret; + dev_dbg(atcphy->dev, "BIST USB_EN phase=2 value=%#x mode=%u pipe=%u\n", + reg, atcphy->mode, atcphy->pipe_state); dev_warn(atcphy->dev, "Timed out waiting for ACIOPHY_BIST_CFG1_USB_EN 2nd toggle\n"); + } core_clear32(atcphy, ACIOPHY_BIST_CFG1, ACIOPHY_BIST_CFG1_USB_EN); ret = readl_poll_timeout(atcphy->regs.core + ACIOPHY_BIST_MAC_USBPHY_STATUS2, reg, !(reg & ACIOPHY_BIST_MAC_USBPHY_STATUS2_LN0_PHY_STATUS2), 10, 10000); - if (ret) + if (ret) { + if (!error) + error = ret; + dev_dbg(atcphy->dev, "BIST STATUS2 phase=2 value=%#x swap=%u mode=%u pipe=%u\n", + reg, atcphy->swap_lanes, atcphy->mode, atcphy->pipe_state); dev_warn(atcphy->dev, "Timed out waiting for ACIOPHY_BIST_MAC_USBPHY_STATUS2_LN0_PHY_STATUS2 2nd time\n"); + } core_set32(atcphy, ACIOPHY_BIST_CFG2, ACIOPHY_BIST_CFG2_LN0_PHY_STATUS_RE); udelay(10); core_clear32(atcphy, ACIOPHY_BIST_CFG2, ACIOPHY_BIST_CFG2_LN0_PHY_STATUS_RE); @@ -1212,16 +1260,22 @@ static void atcphy_configure_pipehandler_usb3_host_t8122(struct apple_atcphy *at core_set32(atcphy, ACIOPHY_BIST_CFG1, ACIOPHY_BIST_CFG1_USB_EN); ret = readl_poll_timeout(atcphy->regs.core + ACIOPHY_BIST_CFG1, reg, (reg & ACIOPHY_BIST_CFG1_USB_EN), 10, 10000); - if (ret) + if (ret) { + if (!error) + error = ret; + dev_dbg(atcphy->dev, "BIST USB_EN phase=3 value=%#x mode=%u pipe=%u\n", + reg, atcphy->mode, atcphy->pipe_state); dev_warn(atcphy->dev, "Timed out waiting for ACIOPHY_BIST_CFG1_USB_EN 3rd toggle\n"); + } core_clear32(atcphy, ACIOPHY_BIST_CFG1, ACIOPHY_BIST_CFG1_USB_EN); core_clear32(atcphy, ACIOPHY_BIST_CFG1, ACIOPHY_BIST_CFG1_CLOCK_EN); + return error; } -static void atcphy_configure_pipehandler_usb3_host_t8103(struct apple_atcphy *atcphy) +static int atcphy_configure_pipehandler_usb3_host_t8103(struct apple_atcphy *atcphy) { - int ret; + int ret, error = 0; u32 reg; core_set32(atcphy, ACIOPHY_TOP_BIST_PHY_CFG0, @@ -1229,9 +1283,12 @@ static void atcphy_configure_pipehandler_usb3_host_t8103(struct apple_atcphy *at core_set32(atcphy, ACIOPHY_TOP_BIST_OV_CFG, ACIOPHY_TOP_BIST_OV_CFG_LN0_RESET_N_OV); ret = readl_poll_timeout(atcphy->regs.core + ACIOPHY_TOP_PHY_STAT, reg, !(reg & ACIOPHY_TOP_PHY_STAT_LN0_UNK23), 10, 10000); - if (ret) + if (ret) { + if (!error) + error = ret; dev_warn(atcphy->dev, "Timed out waiting for ACIOPHY_TOP_PHY_STAT_LN0_UNK23\n"); + } core_set32(atcphy, ACIOPHY_TOP_BIST_READ_CTRL, ACIOPHY_TOP_BIST_READ_CTRL_LN0_PHY_STATUS_RE); @@ -1252,15 +1309,21 @@ static void atcphy_configure_pipehandler_usb3_host_t8103(struct apple_atcphy *at ret = readl_poll_timeout(atcphy->regs.core + ACIOPHY_TOP_PHY_STAT, reg, (reg & ACIOPHY_TOP_PHY_STAT_LN0_UNK0), 10, 10000); - if (ret) + if (ret) { + if (!error) + error = ret; dev_warn(atcphy->dev, "timed out waiting for ACIOPHY_TOP_PHY_STAT_LN0_UNK0\n"); + } ret = readl_poll_timeout(atcphy->regs.core + ACIOPHY_TOP_PHY_STAT, reg, !(reg & ACIOPHY_TOP_PHY_STAT_LN0_UNK23), 10, 10000); - if (ret) + if (ret) { + if (!error) + error = ret; dev_warn(atcphy->dev, "timed out waiting for ACIOPHY_TOP_PHY_STAT_LN0_UNK23\n"); + } atcphy_clear_nonselected_phy_reset(atcphy); @@ -1270,6 +1333,7 @@ static void atcphy_configure_pipehandler_usb3_host_t8103(struct apple_atcphy *at ACIOPHY_TOP_BIST_CIOPHY_CFG1_CLK_EN); core_set32(atcphy, ACIOPHY_TOP_BIST_CIOPHY_CFG1, ACIOPHY_TOP_BIST_CIOPHY_CFG1_BIST_EN); + return error; } static int atcphy_configure_pipehandler_usb3(struct apple_atcphy *atcphy, bool host) @@ -1301,14 +1365,28 @@ static int atcphy_configure_pipehandler_usb3(struct apple_atcphy *atcphy, bool h /* BIST dance */ if (atcphy->hw->gen == ATCPHY_GENERATION_T8103) - atcphy_configure_pipehandler_usb3_host_t8103(atcphy); + ret = atcphy_configure_pipehandler_usb3_host_t8103(atcphy); else - atcphy_configure_pipehandler_usb3_host_t8122(atcphy); + ret = atcphy_configure_pipehandler_usb3_host_t8122(atcphy); + if (ret) { + int cleanup; + + clear32(atcphy->regs.pipehandler + PIPEHANDLER_OVERRIDE, + PIPEHANDLER_OVERRIDE_RXVALID | PIPEHANDLER_OVERRIDE_RXDETECT); + cleanup = atcphy_pipehandler_unlock(atcphy); + if (cleanup) + dev_warn(atcphy->dev, "PIPE cleanup after BIST: %d\n", cleanup); + /* Native BIST changed state; a retry must rebuild after parking. */ + atcphy->mode_dirty = true; + return ret; + } } /* Configure PIPE mux to USB3 PHY */ atcphy_pipehandler_set_mux(atcphy, PIPEHANDLER_MUX_CTRL_DATA_USB3, PIPEHANDLER_MUX_CTRL_CLK_USB3); + /* Track the selected backend even if its completion ACK fails. */ + atcphy->pipe_state = ATCPHY_PIPEHANDLER_STATE_USB3; /* Remove link detection override */ clear32(atcphy->regs.pipehandler + PIPEHANDLER_OVERRIDE, PIPEHANDLER_OVERRIDE_RXVALID); @@ -1317,8 +1395,11 @@ static int atcphy_configure_pipehandler_usb3(struct apple_atcphy *atcphy, bool h /* Pipehandler was only locked when the BIST sequence was applied for host mode */ if (host) { ret = atcphy_pipehandler_unlock(atcphy); - if (ret) + if (ret) { dev_warn(atcphy->dev, "Failed to unlock pipehandler"); + atcphy->mode_dirty = true; + return ret; + } } return 0; @@ -1379,11 +1460,18 @@ static void atcphy_enable_dummy_phy(struct apple_atcphy *atcphy) static int atcphy_configure_pipehandler_dummy(struct apple_atcphy *atcphy) { + bool live_usb3 = atcphy->pipe_pending || + (atcphy->hw->has_t8140_dp && atcphy->fixed_usb2 && atcphy->host_active && + atcphy->pipe_state == ATCPHY_PIPEHANDLER_STATE_USB3); int ret; ret = atcphy_pipehandler_check(atcphy); - if (ret) + if (ret) { + if (live_usb3) + if (atcphy->pipe_state != ATCPHY_PIPEHANDLER_STATE_DUMMY) + atcphy->mode_dirty = true; return ret; + } /* Force disable link detection */ clear32(atcphy->regs.pipehandler + PIPEHANDLER_OVERRIDE_VALUES, @@ -1392,16 +1480,30 @@ static int atcphy_configure_pipehandler_dummy(struct apple_atcphy *atcphy) set32(atcphy->regs.pipehandler + PIPEHANDLER_OVERRIDE, PIPEHANDLER_OVERRIDE_RXDETECT); ret = atcphy_pipehandler_lock(atcphy); - if (ret) + if (ret) { + /* A live controller must acknowledge before its backend changes. */ + if (live_usb3) { + if (atcphy->pipe_state != ATCPHY_PIPEHANDLER_STATE_DUMMY) + atcphy->mode_dirty = true; + return ret; + } dev_warn(atcphy->dev, "Failed to lock pipehandler"); + } /* Switch to dummy PHY */ atcphy_pipehandler_set_mux(atcphy, PIPEHANDLER_MUX_CTRL_DATA_DUMMY, PIPEHANDLER_MUX_CTRL_CLK_DUMMY); ret = atcphy_pipehandler_unlock(atcphy); - if (ret) + if (ret) { + /* The mux changed, but the handoff has not completed. Retain ownership. */ + if (live_usb3) { + if (atcphy->pipe_state != ATCPHY_PIPEHANDLER_STATE_DUMMY) + atcphy->mode_dirty = true; + return ret; + } dev_warn(atcphy->dev, "Failed to unlock pipehandler"); + } mask32(atcphy->regs.pipehandler + PIPEHANDLER_NONSELECTED_OVERRIDE, PIPEHANDLER_NATIVE_POWER_DOWN, FIELD_PREP(PIPEHANDLER_NATIVE_POWER_DOWN, 2)); @@ -1415,8 +1517,7 @@ static int atcphy_configure_pipehandler_dummy(struct apple_atcphy *atcphy) /* * Route the PIPE to the backend the current mode needs. The state is recorded - * whether or not the sequence succeeded: the PIPE is configured once per mode, - * and a failed attempt is not retried on the next set_mode call. + * only after the sequence succeeds, so a failed attempt remains retryable. */ static int atcphy_configure_pipehandler(struct apple_atcphy *atcphy, bool host) { @@ -1427,6 +1528,7 @@ static int atcphy_configure_pipehandler(struct apple_atcphy *atcphy, bool host) switch (state) { case ATCPHY_PIPEHANDLER_STATE_USB3: + atcphy->pipe_pending = true; ret = atcphy_configure_pipehandler_usb3(atcphy, host); break; case ATCPHY_PIPEHANDLER_STATE_USB4: @@ -1436,25 +1538,35 @@ static int atcphy_configure_pipehandler(struct apple_atcphy *atcphy, bool host) ret = atcphy_configure_pipehandler_dummy(atcphy); break; } - atcphy->pipe_state = state; + if (!ret) { + atcphy->pipe_state = state; + atcphy->pipe_pending = false; + } return ret; } /* Route the PIPE back to the dummy backend unless it is there already */ -static void atcphy_park_pipehandler(struct apple_atcphy *atcphy) +static int atcphy_park_pipehandler(struct apple_atcphy *atcphy) { int ret; lockdep_assert_held(&atcphy->lock); - if (atcphy->pipe_state == ATCPHY_PIPEHANDLER_STATE_DUMMY) - return; + dev_dbg(atcphy->dev, "PIPE park: mode=%u pipe=%u host=%u dirty=%u owned=%u idle=%u\n", + atcphy->mode, atcphy->pipe_state, atcphy->host_active, atcphy->mode_dirty, + atcphy->dp_owned_pmas, atcphy->dp_idle_pmas); + if (atcphy->pipe_state == ATCPHY_PIPEHANDLER_STATE_DUMMY && !atcphy->pipe_pending) + return 0; ret = atcphy_configure_pipehandler_dummy(atcphy); - if (ret) + if (ret) { dev_warn(atcphy->dev, "Failed to switch PIPE to dummy: %d\n", ret); + return ret; + } atcphy->pipe_state = ATCPHY_PIPEHANDLER_STATE_DUMMY; + atcphy->pipe_pending = false; + return 0; } static void atcphy_setup_pipehandler(struct apple_atcphy *atcphy) @@ -1464,6 +1576,7 @@ static void atcphy_setup_pipehandler(struct apple_atcphy *atcphy) atcphy_pipehandler_set_mux(atcphy, PIPEHANDLER_MUX_CTRL_DATA_DUMMY, PIPEHANDLER_MUX_CTRL_CLK_DUMMY); atcphy->pipe_state = ATCPHY_PIPEHANDLER_STATE_DUMMY; + atcphy->pipe_pending = false; if (!atcphy->hw->has_usb4) atcphy_enable_dummy_phy(atcphy); } @@ -1959,6 +2072,9 @@ static int atcphy_dp_configure(struct apple_atcphy *atcphy, enum atcphy_dp_link_ u32 reg; guard(mutex)(&atcphy->lock); + /* A failed transition may have asserted the PHY resets already. */ + if (atcphy->mode_dirty || atcphy->pipe_pending) + return -EBUSY; /* the AUSPLL is in use as the Thunderbolt DP tunnel pixel clock */ if (atcphy->tunnel_clock_on || atcphy->tunnel_saved) return -EBUSY; @@ -2687,6 +2803,13 @@ static int atcphy_configure(struct apple_atcphy *atcphy, enum atcphy_mode mode) u32 reg; lockdep_assert_held(&atcphy->lock); + if (atcphy->pipe_pending) + return -EBUSY; + if (atcphy->usb_off_pending && mode != APPLE_ATCPHY_MODE_OFF) + return -EBUSY; + if (atcphy->hw->has_t8140_dp && atcphy->fixed_usb2 && + atcphy->pipe_state != ATCPHY_PIPEHANDLER_STATE_DUMMY) + return -EBUSY; if (atcphy->hw->has_t8140_dp && atcphy->dp_owned_pmas && (mode != atcphy->mode || atcphy->mode_dirty)) { atcphy->mode_dirty = true; @@ -2711,25 +2834,55 @@ static int atcphy_configure(struct apple_atcphy *atcphy, enum atcphy_mode mode) return ret; } /* Keep failed transitions retryable even when the requested mode is unchanged. */ - if (atcphy->hw->has_t8140_dp) - atcphy->mode_dirty = true; + 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) { + if (atcphy->hw->has_t8140_dp && atcphy->fixed_usb2 && + (atcphy->dp_usb_context_valid || atcphy->usb_off_pending)) { + atcphy->usb_off_pending = true; + if (!atcphy->usb_off_drained) { + ret = j700_usb_restore(atcphy, APPLE_ATCPHY_MODE_OFF); + if (ret) + return ret; + atcphy->usb_off_drained = true; + } + } ret = atcphy_power_off(atcphy); if (ret) return ret; + /* Recovery data was discharged before OFF, not discarded while dirty. */ + atcphy->dp_usb_context_valid = 0; + atcphy->dp_usb_context_dirty = 0; + atcphy->usb_off_pending = false; + atcphy->usb_off_drained = false; atcphy->mode = mode; atcphy->mode_dirty = false; return 0; } + /* + * Fixed-hub SAFE keeps USB2 up rather than taking the block OFF. + * With PIPE parked and DP owners released, reinitialize USB3 before + * programming its lanes; the existing sequence releases this reset. + */ + if (atcphy->hw->has_t8140_dp && atcphy->fixed_usb2) + core_clear32(atcphy, ATCPHY_POWER_CTRL, ATCPHY_POWER_PHY_RESET_N); + ret = atcphy_power_on(atcphy); if (ret) return ret; + if (atcphy->hw->has_t8140_dp && atcphy->fixed_usb2) { + ret = j700_usb_capture(atcphy, mode); + if (!ret) + ret = j700_usb_restore(atcphy, mode); + if (ret) + return ret; + } + atcphy_apply_tunables(atcphy, mode); if (atcphy->hw->gen == ATCPHY_GENERATION_T8103) { @@ -2836,10 +2989,23 @@ static int atcphy_usb2_init(struct phy *phy) { struct apple_atcphy *atcphy = phy_get_drvdata(phy); - if (!atcphy->fixed_usb2) - return 0; - guard(mutex)(&atcphy->lock); + if (atcphy->usb_off_pending) + return -EBUSY; + if (atcphy->pipe_pending && + (atcphy->mode == APPLE_ATCPHY_MODE_OFF || atcphy->mode_dirty)) { + int ret = atcphy_park_pipehandler(atcphy); + + if (ret) + return ret; + } + + /* A delayed shutdown may have completed after the last Type-C event. */ + if (!atcphy->fixed_usb2) { + if (atcphy->mode == APPLE_ATCPHY_MODE_OFF || atcphy->mode_dirty) + return atcphy_configure(atcphy, atcphy->typec_mode); + return 0; + } /* * dwc3 initialises its PHYs after releasing its reset, and asserting that @@ -2850,7 +3016,7 @@ static int atcphy_usb2_init(struct phy *phy) * the Type-C mux last asked for, USB2 until a cable event says otherwise. */ set32(atcphy->regs.usb2phy + USB2PHY_SIG, USB2PHY_SIG_HOST); - if (atcphy->mode == APPLE_ATCPHY_MODE_OFF) + if (atcphy->mode == APPLE_ATCPHY_MODE_OFF || atcphy->mode_dirty) return atcphy_configure(atcphy, atcphy->typec_mode); atcphy_usb2_power_on(atcphy); @@ -2867,15 +3033,20 @@ static const struct phy_ops apple_atc_usb2_phy_ops = { static int atcphy_usb3_power_off(struct phy *phy) { struct apple_atcphy *atcphy = phy_get_drvdata(phy); + int ret; guard(mutex)(&atcphy->lock); - atcphy_park_pipehandler(atcphy); + ret = atcphy_park_pipehandler(atcphy); + if (ret) + return ret; + if (atcphy->mode != APPLE_ATCPHY_MODE_OFF || atcphy->mode_dirty) { + ret = atcphy_configure(atcphy, APPLE_ATCPHY_MODE_OFF); + if (ret) + return ret; + } atcphy->host_active = false; - if (atcphy->mode != APPLE_ATCPHY_MODE_OFF) - atcphy_configure(atcphy, APPLE_ATCPHY_MODE_OFF); - return 0; } @@ -2885,6 +3056,9 @@ static int atcphy_usb3_set_mode(struct phy *phy, enum phy_mode mode, int submode guard(mutex)(&atcphy->lock); + if (atcphy->usb_off_pending) + return -EBUSY; + switch (mode) { case PHY_MODE_USB_HOST: atcphy->host_active = true; @@ -2902,7 +3076,17 @@ static int atcphy_usb3_set_mode(struct phy *phy, enum phy_mode mode, int submode * PIPE handler once. Nothing needs to be done either when the PIPE is already routed to * the backend the current mode uses, which includes the dummy backend of the USB2 modes. */ - if (atcphy->pipe_state == atcphy_modes[atcphy->mode].pipehandler_state) + /* A failed lane/domain transition must be restored before routing PIPE. */ + if (atcphy->mode_dirty) + return -EBUSY; + if (atcphy->pipe_pending) { + int ret = atcphy_park_pipehandler(atcphy); + + if (ret) + return ret; + } + if (atcphy->pipe_state == atcphy_modes[atcphy->mode].pipehandler_state && + !atcphy->pipe_pending) return 0; return atcphy_configure_pipehandler(atcphy, mode == PHY_MODE_USB_HOST); @@ -3122,11 +3306,16 @@ static void _atcphy_dwc3_reset_assert(struct apple_atcphy *atcphy) static int atcphy_dwc3_reset_assert(struct reset_controller_dev *rcdev, unsigned long id) { struct apple_atcphy *atcphy = container_of(rcdev, struct apple_atcphy, rcdev); + int ret; guard(mutex)(&atcphy->lock); + /* Do not reset the controller or USB2 PHY if PIPE parking fails. */ + ret = atcphy_park_pipehandler(atcphy); + if (ret) + return ret; + _atcphy_dwc3_reset_assert(atcphy); - atcphy_park_pipehandler(atcphy); atcphy->host_active = false; atcphy_usb2_power_off(atcphy); @@ -3172,19 +3361,30 @@ static int atcphy_probe_rcdev(struct apple_atcphy *atcphy) static int atcphy_sw_set(struct typec_switch_dev *sw, enum typec_orientation orientation) { struct apple_atcphy *atcphy = typec_switch_get_drvdata(sw); + bool swap_lanes; guard(mutex)(&atcphy->lock); switch (orientation) { case TYPEC_ORIENTATION_NONE: - break; + return 0; case TYPEC_ORIENTATION_NORMAL: - atcphy->swap_lanes = false; + swap_lanes = false; break; case TYPEC_ORIENTATION_REVERSE: - atcphy->swap_lanes = true; + swap_lanes = true; break; + default: + return -EINVAL; } + /* The mode alone cannot describe a route after its orientation changes. */ + if (atcphy->swap_lanes != swap_lanes) { + atcphy->swap_lanes = swap_lanes; + atcphy->mode_dirty = true; + } + dev_dbg(atcphy->dev, "orientation=%u mode=%u dirty=%u pipe=%u host=%u\n", + orientation, atcphy->mode, atcphy->mode_dirty, + atcphy->pipe_state, atcphy->host_active); return 0; } @@ -3210,7 +3410,208 @@ static int atcphy_probe_switch(struct apple_atcphy *atcphy) return devm_add_action_or_reset(atcphy->dev, atcphy_typec_switch_unregister, sw); } -static int atcphy_mux_set(struct typec_mux_dev *mux, struct typec_mux_state *state) +/* + * The fixed USB2 hub keeps the host controller active, but its independent + * SuperSpeed root hub may autosuspend while PIPE still needs it awake. + * Own a bounded root-hub resume reference across a Type-C handoff. Resume + * precedes the ATC lock: USB PM can call back into the PHY on recovery. + */ +struct atcphy_usb3_pm { + struct platform_device *controller; + struct device *host; + struct usb_device *root; + struct usb_device *link; +}; + +#if IS_REACHABLE(CONFIG_USB) +/* The root's runtime state does not cover selective suspend of its port. */ +static int atcphy_usb3_link_get(struct atcphy_usb3_pm *pm) +{ + struct usb_device *link; + int ret; + + /* The graph identifies the controller, not a port in a multiport root. */ + if (pm->root->maxchild != 1) + return pm->root->maxchild ? -ENODEV : -EAGAIN; + link = usb_hub_find_child(pm->root, 1); + if (!link) + return 0; + pm->link = usb_get_dev(link); + usb_lock_device(pm->link); + if (READ_ONCE(pm->link->state) == USB_STATE_NOTATTACHED) { + ret = 0; + goto unlock_link; + } + ret = pm_runtime_resume_and_get(&pm->link->dev); + if (ret < 0) + goto unlock_link; + if (READ_ONCE(pm->link->state) == USB_STATE_NOTATTACHED) { + pm_runtime_put_noidle(&pm->link->dev); + ret = -ENODEV; + goto unlock_link; + } + return 0; + +unlock_link: + usb_unlock_device(pm->link); + usb_put_dev(pm->link); + pm->link = NULL; + return ret; +} + +static void atcphy_usb3_link_put(struct atcphy_usb3_pm *pm) +{ + if (!pm->link) + return; + usb_mark_last_busy(pm->link); + pm_runtime_put_autosuspend(&pm->link->dev); + usb_unlock_device(pm->link); + usb_put_dev(pm->link); + pm->link = NULL; +} + +static int atcphy_usb3_pm_find(struct usb_device *udev, void *data) +{ + struct atcphy_usb3_pm *pm = data; + + if (udev->parent || udev->speed < USB_SPEED_SUPER || + udev->bus->sysdev != &pm->controller->dev) + return 0; + + pm->root = usb_get_dev(udev); + return 1; +} +#endif + +static int atcphy_usb3_pm_get(struct apple_atcphy *atcphy, struct atcphy_usb3_pm *pm) +{ +#if IS_REACHABLE(CONFIG_USB) + struct device_node *ep __free(device_node) = NULL; + struct device_node *ctrl __free(device_node) = NULL; + struct usb_hcd *hcd; + int ret; +#endif + + if (!atcphy->fixed_usb2 || !atcphy->hw->has_t8140_dp) + return 0; + +#if IS_REACHABLE(CONFIG_USB) + ep = of_graph_get_endpoint_by_regs(atcphy->np, 1, -1); + ctrl = ep ? of_graph_get_remote_port_parent(ep) : NULL; + if (!ctrl) + return -ENODEV; + pm->controller = of_find_device_by_node(ctrl); + if (!pm->controller) + return -EAGAIN; + + /* Driver core takes this lock before DWC3 removal touches SUSPHY. */ + device_lock(&pm->controller->dev); + if (!pm->controller->dev.driver) { + ret = -EAGAIN; + goto put_controller; + } + if (strcmp(pm->controller->dev.driver->name, "dwc3-apple")) { + ret = -ENODEV; + goto put_controller; + } + + /* Firmware identity, not a bus number, binds this PHY to its root hub. */ + usb_for_each_dev(pm, atcphy_usb3_pm_find); + if (!pm->root) { + ret = -EAGAIN; + goto put_controller; + } + + /* Also serialize direct xHCI unbind, before it marks the HCD removing. */ + pm->host = get_device(pm->root->bus->controller); + device_lock(pm->host); + if (!pm->host->driver) { + ret = -EAGAIN; + goto put_root; + } + if (pm->host->parent != &pm->controller->dev || + strcmp(pm->host->driver->name, "xhci-hcd")) { + ret = -ENODEV; + goto put_root; + } + + /* USB destruction follows the platform lifecycle locks, not vice versa. */ + usb_lock_device(pm->root); + hcd = bus_to_hcd(pm->root->bus); + if (READ_ONCE(pm->root->state) == USB_STATE_NOTATTACHED || HCD_DEAD(hcd)) { + ret = -ENODEV; + goto unlock_root; + } + ret = pm_runtime_resume_and_get(&pm->root->dev); + if (ret < 0) + goto unlock_root; + if (READ_ONCE(pm->root->state) == USB_STATE_NOTATTACHED || + HCD_DEAD(hcd) || !HCD_HW_ACCESSIBLE(hcd)) { + pm_runtime_put_noidle(&pm->root->dev); + ret = -ENODEV; + goto unlock_root; + } + /* Resume the attached upstream link before taking the ATC mutex. */ + ret = atcphy_usb3_link_get(pm); + if (ret) { + usb_mark_last_busy(pm->root); + pm_runtime_put_autosuspend(&pm->root->dev); + goto unlock_root; + } + if (READ_ONCE(pm->root->state) == USB_STATE_NOTATTACHED || + READ_ONCE(hcd->state) != HC_STATE_RUNNING || + HCD_DEAD(hcd) || !HCD_HW_ACCESSIBLE(hcd)) { + atcphy_usb3_link_put(pm); + pm_runtime_put_noidle(&pm->root->dev); + ret = -ENODEV; + goto unlock_root; + } + return 0; + +unlock_root: + usb_unlock_device(pm->root); +put_root: + device_unlock(pm->host); + put_device(pm->host); + pm->host = NULL; + usb_put_dev(pm->root); + pm->root = NULL; +put_controller: + device_unlock(&pm->controller->dev); + put_device(&pm->controller->dev); + pm->controller = NULL; + return ret; +#else + /* typec_mux_set() skips EOPNOTSUPP, which would report a false success. */ + return -ENODEV; +#endif +} + +static void atcphy_usb3_pm_put(struct atcphy_usb3_pm *pm) +{ +#if IS_REACHABLE(CONFIG_USB) + if (!pm->root) + return; + + /* Release child ownership before the root: keep the lock order hierarchical. */ + atcphy_usb3_link_put(pm); + + /* Restore normal runtime policy after both success and failed parking. */ + usb_mark_last_busy(pm->root); + pm_runtime_put_autosuspend(&pm->root->dev); + usb_unlock_device(pm->root); + usb_put_dev(pm->root); + device_unlock(pm->host); + put_device(pm->host); + device_unlock(&pm->controller->dev); + put_device(&pm->controller->dev); + pm->host = NULL; + pm->root = NULL; + pm->controller = NULL; +#endif +} + +static int atcphy_mux_set_locked(struct typec_mux_dev *mux, struct typec_mux_state *state) { struct apple_atcphy *atcphy = typec_mux_get_drvdata(mux); enum atcphy_mode target_mode; @@ -3281,8 +3682,20 @@ 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 && !atcphy->mode_dirty) + if (atcphy->mode == target_mode && !atcphy->mode_dirty) { + /* Retry a failed PIPE setup without disturbing the configured lanes. */ + if (atcphy->fixed_usb2 && atcphy->host_active && + (atcphy->pipe_state != atcphy_modes[target_mode].pipehandler_state || + atcphy->pipe_pending)) { + if (atcphy->pipe_pending) { + ret = atcphy_park_pipehandler(atcphy); + if (ret) + return ret; + } + return atcphy_configure_pipehandler(atcphy, true); + } return 0; + } switch (target_mode) { case APPLE_ATCPHY_MODE_TBT: @@ -3313,23 +3726,32 @@ static int atcphy_mux_set(struct typec_mux_dev *mux, struct typec_mux_state *sta /* * The controller of a port behind a fixed hub stays up across mode * changes, so the PIPE has to follow the mode from here rather than - * from a later set_mode call: park it before the lanes change to a - * mode without a USB3 backend, and route it to the new backend - * afterwards while dwc3 is up in host mode. + * from a later set_mode call. Even another USB3 mode can move the + * backend to the other PMA when orientation changes. Park before + * rebuilding the lanes, then reconnect while dwc3 is in host mode. */ - if (pipe_state != ATCPHY_PIPEHANDLER_STATE_USB3) - atcphy_park_pipehandler(atcphy); + ret = atcphy_park_pipehandler(atcphy); + if (ret) + return ret; ret = atcphy_configure(atcphy, target_mode); if (ret) return ret; + if (atcphy->host_active && atcphy->pipe_state != pipe_state) ret = atcphy_configure_pipehandler(atcphy, true); return ret; } + /* A failed USB3 handoff must finish parking before ordinary-port lane work. */ + if (atcphy->pipe_pending) { + ret = atcphy_park_pipehandler(atcphy); + if (ret) + return ret; + } + /* * If the pipehandler is still/already up here there's a bug somewhere so make sure to * complain loudly. We can still try to switch modes and hope for the best though, @@ -3339,6 +3761,20 @@ static int atcphy_mux_set(struct typec_mux_dev *mux, struct typec_mux_state *sta return atcphy_configure(atcphy, target_mode); } +static int atcphy_mux_set(struct typec_mux_dev *mux, struct typec_mux_state *state) +{ + struct apple_atcphy *atcphy = typec_mux_get_drvdata(mux); + struct atcphy_usb3_pm pm = {}; + int ret; + + ret = atcphy_usb3_pm_get(atcphy, &pm); + if (ret) + return ret; + ret = atcphy_mux_set_locked(mux, state); + atcphy_usb3_pm_put(&pm); + return ret; +} + static void atcphy_typec_mux_unregister(void *data) { typec_mux_unregister(data); diff --git a/drivers/soc/apple/dt.rs b/drivers/soc/apple/dt.rs index 48a4732008967..51c83d1249cdf 100644 --- a/drivers/soc/apple/dt.rs +++ b/drivers/soc/apple/dt.rs @@ -400,7 +400,14 @@ unsafe fn add_u32( } } -pub(crate) fn enable_spi_sensor(base: u64, cs: u32) -> Result<()> { +// The public J700 DTS enables SPI2. A disabled controller at handoff is a +// firmware hardware-presence decision; older platforms still need activation. +fn sensor_controller_refused(controller: &DtNode) -> bool { + crate::profile::machine_has(b"apple,j700") && !controller.is_available() +} + +/// Returns false when the bootloader explicitly withheld J700's controller. +pub(crate) fn enable_spi_sensor(base: u64, cs: u32) -> Result { // A machine that describes the sensor in its own device tree (compatible // apple,mesa-fingerprint) is handled machine-agnostically: enable that node // and its SPI controller wherever they sit, with the power/IRQ GPIOs coming @@ -411,6 +418,9 @@ pub(crate) fn enable_spi_sensor(base: u64, cs: u32) -> Result<()> { pr_err!("apple_sep: sensor node has no parent SPI controller\n"); ENODEV })?; + if sensor_controller_refused(&controller) { + return Ok(false); + } pr_info!( "apple_sep: sensor node present in the device tree; enabling it and its SPI bus\n" ); @@ -425,7 +435,7 @@ pub(crate) fn enable_spi_sensor(base: u64, cs: u32) -> Result<()> { queued = true; } Ok(queued) - }); + }).map(|()| true); } let controller = node_at_address(base).ok_or_else(|| { @@ -436,6 +446,9 @@ pub(crate) fn enable_spi_sensor(base: u64, cs: u32) -> Result<()> { ENODEV })?; + if sensor_controller_refused(&controller) { + return Ok(false); + } let controller_ok = controller.is_available(); let existing = controller.child_with_reg(cs); pr_info!( @@ -447,7 +460,7 @@ pub(crate) fn enable_spi_sensor(base: u64, cs: u32) -> Result<()> { ); if controller_ok && existing.is_some() { - return Ok(()); + return Ok(true); } if existing.is_some() && !controller_ok { @@ -456,7 +469,7 @@ pub(crate) fn enable_spi_sensor(base: u64, cs: u32) -> Result<()> { return with_changeset(|cs_handle| { queue_status_okay(cs_handle, &controller)?; Ok(true) - }); + }).map(|()| true); } pr_info!( @@ -470,7 +483,7 @@ pub(crate) fn enable_spi_sensor(base: u64, cs: u32) -> Result<()> { } queue_sensor_node(cs_handle, &controller)?; Ok(true) - }) + }).map(|()| true) } /// Resolve a reserved-memory region by name on the SEP node's device. diff --git a/drivers/soc/apple/profile.rs b/drivers/soc/apple/profile.rs index 82ad95613c4b1..85ed9c4b03cec 100644 --- a/drivers/soc/apple/profile.rs +++ b/drivers/soc/apple/profile.rs @@ -344,7 +344,7 @@ static_assert!(T8112.sensor.controller_base == T8103.sensor.controller_base); /// /// The root property is a NUL-separated list, so whole entries are compared /// instead of substrings: `apple,t8103` cannot match a longer unrelated value. -fn machine_has(compatible: &[u8]) -> bool { +pub(crate) fn machine_has(compatible: &[u8]) -> bool { let Some(root) = of::root() else { return false; }; diff --git a/drivers/soc/apple/refkey.rs b/drivers/soc/apple/refkey.rs index 4d54246bf4f68..d8e6a5c9a67c8 100644 --- a/drivers/soc/apple/refkey.rs +++ b/drivers/soc/apple/refkey.rs @@ -246,6 +246,7 @@ impl SepData { } pub(crate) fn refkey_seal_trusted(&self, key: &[u8]) -> Result> { + self.wait_for_trusted_ready()?; self.ensure_machine_refkey()?; let guard = self.machine_refkey.lock(); let mk = guard.as_ref().ok_or(ENODEV)?; @@ -253,6 +254,7 @@ impl SepData { } pub(crate) fn refkey_unseal_trusted(&self, sealed: &[u8]) -> Result> { + self.wait_for_trusted_ready()?; self.ensure_machine_refkey()?; let blob = { let guard = self.machine_refkey.lock(); diff --git a/drivers/soc/apple/rtkit.c b/drivers/soc/apple/rtkit.c index 85d70c46015db..15baa7e4193f3 100644 --- a/drivers/soc/apple/rtkit.c +++ b/drivers/soc/apple/rtkit.c @@ -608,6 +608,17 @@ static void apple_rtkit_oslog_rx(struct apple_rtkit *rtk, u64 msg) apple_rtkit_common_rx_get_buffer(rtk, &rtk->oslog_buffer, APPLE_RTKIT_EP_OSLOG, msg); break; + case 3: + case 4: + case 5: + /* + * OpenBSD's public RTKit consumer recognizes these opaque events + * without an ACK. Keep their payload visible without interpreting + * it as a buffer address or a decoded firmware error. + */ + dev_info(rtk->dev, "RTKit: OSLog event %u: %016llx (undecoded)\n", + type, msg); + break; default: dev_warn(rtk->dev, "RTKit: Unknown oslog message: %llx\n", msg); diff --git a/drivers/soc/apple/sbio.rs b/drivers/soc/apple/sbio.rs index c999300558637..cff44167e20cd 100644 --- a/drivers/soc/apple/sbio.rs +++ b/drivers/soc/apple/sbio.rs @@ -304,14 +304,24 @@ impl SepData { return; } - if let Err(e) = dt::enable_spi_sensor(sensor::CONTROLLER_BASE, sensor::CHIP_SELECT) { - dev_warn!( - self.dev, - "sensor: could not enable the SPI bus at 0x{:x} or create the sensor node ({:?}); no sensor\n", - sensor::CONTROLLER_BASE, - e - ); - return; + match dt::enable_spi_sensor(sensor::CONTROLLER_BASE, sensor::CHIP_SELECT) { + Ok(true) => {}, + Ok(false) => { + dev_info!( + self.dev, + "sensor: SPI controller withheld by the J700 firmware handoff; Touch ID unavailable\n" + ); + return; + } + Err(e) => { + dev_warn!( + self.dev, + "sensor: could not enable the SPI bus at 0x{:x} or create the sensor node ({:?}); no sensor\n", + sensor::CONTROLLER_BASE, + e + ); + return; + } } let bound = sensor::is_bound(); @@ -1984,7 +1994,7 @@ impl SepData { Some(n) if (self.templates_restored.load(Relaxed) || self.cold_restore_candidate.load(Relaxed)) && self.prove_restore() => { self.templates_restored.store(true, Relaxed); - dev_warn!( + dev_info!( self.dev, "Touch ID: restored {} enrolled identity/identities; enrolment survived reboot\n", n diff --git a/drivers/soc/apple/sep.rs b/drivers/soc/apple/sep.rs index c953d54e6c11d..c5b12c8214416 100644 --- a/drivers/soc/apple/sep.rs +++ b/drivers/soc/apple/sep.rs @@ -562,6 +562,8 @@ struct SepData { #[pin] endpoints: Mutex, + #[pin] + trusted_ready: CondVar, #[pin] control: Mutex, @@ -822,6 +824,7 @@ impl SepData { mbox <- new_mutex!(None), shmem <- new_mutex!(Some(buf)), endpoints <- new_mutex!(EndpointTable::new()), + trusted_ready <- new_condvar!("SepData::trusted_ready"), control <- new_mutex!(control::ControlState::new()), control_wq <- new_condvar!("SepData::control_wq"), sks_seq: Atomic::new(0), @@ -1911,7 +1914,7 @@ impl SepData { ); diag::attach(false); diag::bringup_ended(); - this.phase.store(PHASE_READY, Relaxed); + this.finish_bringup(); return; } dev_info!( @@ -1933,7 +1936,7 @@ impl SepData { e ); diag::bringup_ended(); - this.phase.store(PHASE_READY, Relaxed); + this.finish_bringup(); return; } @@ -1974,10 +1977,50 @@ impl SepData { this.run_bringup(); diag::bringup_ended(); - this.phase.store(PHASE_READY, Relaxed); + this.finish_bringup(); Self::arm_settle(this); } + // Publish completion under the same lock that trusted consumers wait on. + // PHASE_READY ends discovery/initialisation; it is not proof of success. + fn finish_bringup(&self) { + let _guard = self.endpoints.lock(); + self.phase.store(PHASE_READY, Relaxed); + self.trusted_ready.notify_all(); + } + + /// Trusted-key callbacks can run as soon as probe publishes the source, + /// while endpoint discovery and key-store initialisation are still running. + /// Wait only in those consumers, leaving bringup free to establish the key. + pub(crate) fn wait_for_trusted_ready(&self) -> Result<()> { + let mut remaining = time::msecs_to_jiffies(EXCHANGE_TIMEOUT_MS); + let mut guard = self.endpoints.lock(); + loop { + if self.shutting_down.load(Relaxed) { + return Err(ENODEV); + } + if self.phase.load(Relaxed) == PHASE_READY { + // A completed but failed attach must not look ready. + if !guard.eps.iter().any(|e| e.id == proto::EP_SKS) { + return Err(ENODEV); + } + if !self.sks_initialized.load(Relaxed) { + return Err(EIO); + } + return Ok(()); + } + if remaining == 0 { + dev_err!(self.dev, "trusted-keys: key-store bringup timed out\n"); + return Err(ETIMEDOUT); + } + match self.trusted_ready.wait_interruptible_timeout(&mut guard, remaining) { + CondVarTimeoutResult::Woken { jiffies } => remaining = jiffies, + CondVarTimeoutResult::Signal { .. } => return Err(ERESTARTSYS), + CondVarTimeoutResult::Timeout => remaining = 0, + } + } + } + fn endpoint_present(&self, id: u8) -> bool { self.endpoints.lock().eps.iter().any(|e| e.id == id) } @@ -2049,6 +2092,10 @@ impl SepData { fn remove(&self) { self.shutting_down.store(true, Relaxed); + { + let _guard = self.endpoints.lock(); + self.trusted_ready.notify_all(); + } bio::release(&mut self.bio_session.lock()); self.worker_gone.notify_all(); self.rng_shutdown.store(true, Relaxed); diff --git a/drivers/soc/apple/sks.rs b/drivers/soc/apple/sks.rs index f1fabe2791a76..d7a203f9e0b15 100644 --- a/drivers/soc/apple/sks.rs +++ b/drivers/soc/apple/sks.rs @@ -341,6 +341,7 @@ impl SepData { cpx_encryption_mode, } = self.profile.key_store else { + self.sks_initialized.store(true, Relaxed); return true; }; diff --git a/drivers/soc/apple/trusted.rs b/drivers/soc/apple/trusted.rs index 6162bc8af9d3c..3c7f512e2865b 100644 --- a/drivers/soc/apple/trusted.rs +++ b/drivers/soc/apple/trusted.rs @@ -199,6 +199,9 @@ unsafe extern "C" fn tk_get_random(key: *mut u8, key_len: usize) -> c_int { if key_len == 0 { return 0; } + if let Err(e) = sep.wait_for_trusted_ready() { + return e.to_errno(); + } // SAFETY: the framework guarantees `key` is writable for `key_len` bytes. let buf = unsafe { core::slice::from_raw_parts_mut(key, key_len) }; match sep.sep_random(buf) { diff --git a/drivers/usb/dwc3/core.c b/drivers/usb/dwc3/core.c index 6f3c00b089371..c2bbcbcf3797a 100644 --- a/drivers/usb/dwc3/core.c +++ b/drivers/usb/dwc3/core.c @@ -995,14 +995,20 @@ static void dwc3_clk_disable(struct dwc3 *dwc) clk_disable_unprepare(dwc->bus_clk); } -void dwc3_core_exit(struct dwc3 *dwc) +void dwc3_core_exit_after_phy_power_off(struct dwc3 *dwc) { - dwc3_event_buffers_cleanup(dwc); - dwc3_phy_power_off(dwc); dwc3_phy_exit(dwc); dwc3_clk_disable(dwc); reset_control_assert(dwc->reset); } +EXPORT_SYMBOL_GPL(dwc3_core_exit_after_phy_power_off); + +void dwc3_core_exit(struct dwc3 *dwc) +{ + dwc3_event_buffers_cleanup(dwc); + dwc3_phy_power_off(dwc); + dwc3_core_exit_after_phy_power_off(dwc); +} EXPORT_SYMBOL_GPL(dwc3_core_exit); static bool dwc3_core_is_valid(struct dwc3 *dwc) @@ -2410,14 +2416,16 @@ static int dwc3_probe(struct platform_device *pdev) return dwc3_core_probe(&probe_data); } -void dwc3_core_remove(struct dwc3 *dwc) +static void dwc3_core_remove_common(struct dwc3 *dwc, bool core_exited) { pm_runtime_get_sync(dwc->dev); - dwc3_core_exit_mode(dwc); + if (!core_exited) + dwc3_core_exit_mode(dwc); dwc3_debugfs_exit(dwc); - dwc3_core_exit(dwc); + if (!core_exited) + dwc3_core_exit(dwc); dwc3_ulpi_exit(dwc); pm_runtime_allow(dwc->dev); @@ -2438,8 +2446,19 @@ void dwc3_core_remove(struct dwc3 *dwc) power_supply_put(dwc->usb_psy); } } + +void dwc3_core_remove(struct dwc3 *dwc) +{ + dwc3_core_remove_common(dwc, false); +} EXPORT_SYMBOL_GPL(dwc3_core_remove); +void dwc3_core_remove_no_shutdown(struct dwc3 *dwc) +{ + dwc3_core_remove_common(dwc, true); +} +EXPORT_SYMBOL_GPL(dwc3_core_remove_no_shutdown); + static void dwc3_remove(struct platform_device *pdev) { dwc3_core_remove(platform_get_drvdata(pdev)); diff --git a/drivers/usb/dwc3/dwc3-apple.c b/drivers/usb/dwc3/dwc3-apple.c index 232a4de95bb75..e1d1104db98a3 100644 --- a/drivers/usb/dwc3/dwc3-apple.c +++ b/drivers/usb/dwc3/dwc3-apple.c @@ -14,6 +14,7 @@ #include #include #include +#include #include "dwc3-apple-state.h" #include "glue.h" @@ -96,6 +97,11 @@ * @fixed_hub: A fixed hub sits on the controller's USB2 port, the controller stays in host mode * @lock: Mutex for synchronizing access * @state: Current state of the controller, see enum dwc3_apple_state and the comment above + * @core_active: Core still owns initialized PHYs + * @core_exit_pending: Event buffers have been cleaned for this shutdown + * @reset_pending: Controller reset must complete before another start + * @usb2_power_off_pending: Core-owned USB2 references still to release + * @usb3_power_off_pending: Core-owned USB3 references still to release */ struct dwc3_apple { struct dwc3 dwc; @@ -113,6 +119,11 @@ struct dwc3_apple { struct mutex lock; enum dwc3_apple_state state; + bool core_active; + bool core_exit_pending; + bool reset_pending; + unsigned long usb2_power_off_pending; + unsigned long usb3_power_off_pending; }; #define to_dwc3_apple(d) container_of((d), struct dwc3_apple, dwc) @@ -254,6 +265,52 @@ static int dwc3_apple_core_init(struct dwc3_apple *appledwc) return ret; } +/* Keep failed releases owned by the glue, without repeating completed teardown. */ +static int dwc3_apple_core_stop(struct dwc3_apple *appledwc) +{ + struct dwc3 *dwc = &appledwc->dwc; + int i, ret; + + lockdep_assert_held(&appledwc->lock); + if (appledwc->core_active) { + if (!appledwc->core_exit_pending) { + dwc3_enable_susphy(dwc, true); + dwc3_event_buffers_cleanup(dwc); + appledwc->core_exit_pending = true; + } + for (i = 0; i < dwc->num_usb3_ports; i++) { + if (!(appledwc->usb3_power_off_pending & BIT(i))) + continue; + ret = phy_power_off(dwc->usb3_generic_phy[i]); + if (ret) + return ret; + appledwc->usb3_power_off_pending &= ~BIT(i); + } + for (i = 0; i < dwc->num_usb2_ports; i++) { + if (!(appledwc->usb2_power_off_pending & BIT(i))) + continue; + ret = phy_power_off(dwc->usb2_generic_phy[i]); + if (ret) + return ret; + appledwc->usb2_power_off_pending &= ~BIT(i); + } + usb_phy_set_suspend(dwc->usb3_phy, 1); + usb_phy_set_suspend(dwc->usb2_phy, 1); + dwc3_core_exit_after_phy_power_off(dwc); + appledwc->core_active = false; + appledwc->core_exit_pending = false; + } + if (appledwc->reset_pending) { + ret = reset_control_assert(appledwc->reset); + if (ret) + return ret; + appledwc->reset_pending = false; + } + dev_pm_syscore_device(appledwc->dev, false); + + return 0; +} + /* * Bring the core up for the given role: configure the USB2 PHY, release the reset and probe * (on the first call) or initialise the core. @@ -265,6 +322,11 @@ static int dwc3_apple_core_start(struct dwc3_apple *appledwc, enum dwc3_apple_st lockdep_assert_held(&appledwc->lock); + /* A failed exit must finish before any new PHY reference is acquired. */ + ret = dwc3_apple_core_stop(appledwc); + if (ret) + return ret; + /* * The USB2 PHY on this platform must be configured for device mode before dwc3 tries to * access it. Otherwise, the new configuration will sometimes only take affect after the @@ -297,11 +359,14 @@ static int dwc3_apple_core_start(struct dwc3_apple *appledwc, enum dwc3_apple_st return ret; } + appledwc->reset_pending = true; + /* Keep register access powered until both PHY shutdown and reset finish. */ + dev_pm_syscore_device(appledwc->dev, true); ret = dwc3_apple_core_init(appledwc); if (ret) { - ret_reset = reset_control_assert(appledwc->reset); + ret_reset = dwc3_apple_core_stop(appledwc); if (ret_reset) - dev_warn(appledwc->dev, "Failed to assert reset, err=%d\n", ret_reset); + dev_warn(appledwc->dev, "Failed to finish core shutdown, err=%d\n", ret_reset); return ret; } @@ -310,6 +375,9 @@ static int dwc3_apple_core_start(struct dwc3_apple *appledwc, enum dwc3_apple_st * configure some unknown Apple-specific settings and then bring up xhci or gadget mode. */ dwc3_apple_setup_cio(appledwc); + appledwc->core_active = true; + appledwc->usb2_power_off_pending = BIT(appledwc->dwc.num_usb2_ports) - 1; + appledwc->usb3_power_off_pending = BIT(appledwc->dwc.num_usb3_ports) - 1; return 0; } @@ -386,51 +454,32 @@ static int dwc3_apple_init(struct dwc3_apple *appledwc, enum dwc3_apple_state st return 0; core_exit: - dwc3_core_exit(&appledwc->dwc); - ret_reset = reset_control_assert(appledwc->reset); + ret_reset = dwc3_apple_core_stop(appledwc); if (ret_reset) - dev_warn(appledwc->dev, "Failed to assert reset, err=%d\n", ret_reset); + dev_warn(appledwc->dev, "Failed to finish core shutdown, err=%d\n", ret_reset); return ret; } static int dwc3_apple_exit(struct dwc3_apple *appledwc) { - int ret = 0; - lockdep_assert_held(&appledwc->lock); switch (appledwc->state) { case DWC3_APPLE_PROBE_PENDING: case DWC3_APPLE_NO_CABLE: - /* Nothing to do if we're already off */ - return 0; + break; case DWC3_APPLE_DEVICE: - dev_pm_syscore_device(appledwc->dev, false); dwc3_gadget_exit(&appledwc->dwc); + appledwc->state = DWC3_APPLE_NO_CABLE; break; case DWC3_APPLE_HOST: - dev_pm_syscore_device(appledwc->dev, false); dwc3_host_exit(&appledwc->dwc); + appledwc->state = DWC3_APPLE_NO_CABLE; break; } - /* - * This platform requires SUSPHY to be enabled in order to properly power down the PHY - * and switch dwc3's PIPE interface back to a dummy PHY (i.e. no USB3 support and USB2 via - * a different PHY connected through ULPI). - */ - dwc3_enable_susphy(&appledwc->dwc, true); - dwc3_core_exit(&appledwc->dwc); - appledwc->state = DWC3_APPLE_NO_CABLE; - - ret = reset_control_assert(appledwc->reset); - if (ret) { - dev_err(appledwc->dev, "Failed to assert reset, err=%d\n", ret); - return ret; - } - - return 0; + return dwc3_apple_core_stop(appledwc); } static int dwc3_usb_role_switch_set(struct usb_role_switch *sw, enum usb_role role) @@ -596,9 +645,11 @@ static int dwc3_apple_probe(struct platform_device *pdev) scoped_guard(mutex, &appledwc->lock) ret = dwc3_apple_init(appledwc, DWC3_APPLE_HOST); if (ret) { - /* dwc3_apple_init() has already undone the core initialisation */ + /* A failed cleanup remains visible even when probe cannot be retried. */ + if (appledwc->core_active || appledwc->reset_pending) + dev_err(dev, "Host startup left pending PHY/reset cleanup\n"); if (appledwc->state != DWC3_APPLE_PROBE_PENDING) - dwc3_core_remove(&appledwc->dwc); + dwc3_core_remove_no_shutdown(&appledwc->dwc); usb_role_switch_unregister(appledwc->role_sw); return dev_err_probe(dev, ret, "Failed to start host mode\n"); } @@ -620,9 +671,10 @@ static void dwc3_apple_remove(struct platform_device *pdev) * dwc3_core_probe was never called and there's hence no need to call dwc3_core_remove. * dwc3_apple_exit can be called unconditionally because it checks the state itself. */ - dwc3_apple_exit(appledwc); + if (dwc3_apple_exit(appledwc)) + dev_err(appledwc->dev, "Removal left pending PHY/reset cleanup\n"); if (appledwc->state != DWC3_APPLE_PROBE_PENDING) - dwc3_core_remove(&appledwc->dwc); + dwc3_core_remove_no_shutdown(&appledwc->dwc); } static const struct of_device_id dwc3_apple_of_match[] = { diff --git a/drivers/usb/dwc3/glue.h b/drivers/usb/dwc3/glue.h index d738e1739ae01..db5f32df712bf 100644 --- a/drivers/usb/dwc3/glue.h +++ b/drivers/usb/dwc3/glue.h @@ -108,6 +108,26 @@ int dwc3_core_init(struct dwc3 *dwc); */ void dwc3_core_exit(struct dwc3 *dwc); +/** + * dwc3_core_exit_after_phy_power_off - Finish a glue-owned core shutdown + * @dwc: Pointer to DWC3 controller context + * + * The glue must first stop host/gadget operation, clean up event buffers, + * release every core-owned generic PHY power reference and suspend legacy + * USB PHYs. This finishes PHY exit, clock disable and reset exactly once. + */ +void dwc3_core_exit_after_phy_power_off(struct dwc3 *dwc); + +/** + * dwc3_core_remove_no_shutdown - Release resources without a second shutdown + * @dwc: Pointer to DWC3 controller context + * + * The glue owns host/gadget and core shutdown, including reporting any + * failure to finish it. This releases software resources without repeating + * those operations or discarding another generic PHY reference. + */ +void dwc3_core_remove_no_shutdown(struct dwc3 *dwc); + /** * dwc3_host_init - Initialize host mode operation * @dwc: Pointer to DWC3 controller context diff --git a/drivers/usb/typec/mux.c b/drivers/usb/typec/mux.c index 7e327118cbd71..a6dd8284ff1b7 100644 --- a/drivers/usb/typec/mux.c +++ b/drivers/usb/typec/mux.c @@ -13,6 +13,8 @@ #include #include #include +#include +#include #include "class.h" #include "mux.h" @@ -259,11 +261,60 @@ EXPORT_SYMBOL_GPL(typec_switch_get_drvdata); /* ------------------------------------------------------------------------- */ +/* Class, altmode and controller handles for one connector share this domain. */ +struct typec_mux_context { + struct list_head list; + struct fwnode_handle *fwnode; + struct mutex lock; + unsigned int users; +}; + +static LIST_HEAD(typec_mux_contexts); +static DEFINE_MUTEX(typec_mux_contexts_lock); +DEFINE_STATIC_SRCU(typec_mux_srcu); + struct typec_mux { struct typec_mux_dev *mux_devs[TYPEC_MUX_MAX_DEVS]; unsigned int num_mux_devs; + struct typec_mux_context *context; + struct typec_mux_state transaction; + struct task_struct *owner; + int srcu_index; + unsigned int prepared; + bool applied; }; +static struct typec_mux_context *typec_mux_context_get(struct fwnode_handle *fwnode) +{ + struct typec_mux_context *context; + + guard(mutex)(&typec_mux_contexts_lock); + list_for_each_entry(context, &typec_mux_contexts, list) { + if (context->fwnode == fwnode) { + context->users++; + return context; + } + } + context = kzalloc_obj(*context); + if (!context) + return ERR_PTR(-ENOMEM); + context->fwnode = fwnode_handle_get(fwnode); + context->users = 1; + mutex_init(&context->lock); + list_add_tail(&context->list, &typec_mux_contexts); + return context; +} + +static void typec_mux_context_put(struct typec_mux_context *context) +{ + guard(mutex)(&typec_mux_contexts_lock); + if (--context->users) + return; + list_del(&context->list); + fwnode_handle_put(context->fwnode); + kfree(context); +} + static int mux_fwnode_match(struct device *dev, const void *fwnode) { if (!is_typec_mux_dev(dev)) @@ -310,16 +361,20 @@ struct typec_mux *fwnode_typec_mux_get(struct fwnode_handle *fwnode) int count; int err; int i; + int index; mux = kzalloc_obj(*mux); if (!mux) return ERR_PTR(-ENOMEM); + /* Unregister must drain lookup and module-reference conversion too. */ + index = srcu_read_lock(&typec_mux_srcu); count = fwnode_connection_find_matches(fwnode, "mode-switch", NULL, typec_mux_match, (void **)mux_devs, ARRAY_SIZE(mux_devs)); if (count <= 0) { + srcu_read_unlock(&typec_mux_srcu, index); kfree(mux); return NULL; } @@ -332,12 +387,26 @@ struct typec_mux *fwnode_typec_mux_get(struct fwnode_handle *fwnode) } for (i = 0; i < count; i++) { - WARN_ON(!try_module_get(mux_devs[i]->dev.parent->driver->owner)); + if (READ_ONCE(mux_devs[i]->removed) || + !try_module_get(mux_devs[i]->owner)) { + err = -ENODEV; + while (i--) + module_put(mux_devs[i]->owner); + goto put_mux_devs; + } mux->mux_devs[i] = mux_devs[i]; } mux->num_mux_devs = count; + mux->context = typec_mux_context_get(fwnode); + if (IS_ERR(mux->context)) { + err = PTR_ERR(mux->context); + for (i = 0; i < count; i++) + module_put(mux_devs[i]->owner); + goto put_mux_devs; + } + srcu_read_unlock(&typec_mux_srcu, index); return mux; put_mux_devs: @@ -346,6 +415,7 @@ struct typec_mux *fwnode_typec_mux_get(struct fwnode_handle *fwnode) put_device(&mux_devs[i]->dev); } + srcu_read_unlock(&typec_mux_srcu, index); kfree(mux); return ERR_PTR(err); @@ -368,31 +438,162 @@ void typec_mux_put(struct typec_mux *mux) for (i = 0; i < mux->num_mux_devs; i++) { mux_dev = mux->mux_devs[i]; - module_put(mux_dev->dev.parent->driver->owner); + module_put(mux_dev->owner); put_device(&mux_dev->dev); } + typec_mux_context_put(mux->context); kfree(mux); } EXPORT_SYMBOL_GPL(typec_mux_put); -int typec_mux_set(struct typec_mux *mux, struct typec_mux_state *state) +static void typec_mux_unlock(struct typec_mux *mux) +{ + int index = mux->srcu_index; + + mux->prepared = 0; + mux->applied = false; + mux->owner = NULL; + mutex_unlock(&mux->context->lock); + srcu_read_unlock(&typec_mux_srcu, index); +} + +void typec_mux_abort(struct typec_mux *mux) { - struct typec_mux_dev *mux_dev; unsigned int i; - int ret; + + if (IS_ERR_OR_NULL(mux)) + return; + if (WARN_ON_ONCE(mux->owner != current)) + return; + for (i = mux->prepared; i > 0; i--) { + struct typec_mux_dev *dev = mux->mux_devs[i - 1]; + + if (dev->abort) + dev->abort(dev, &mux->transaction); + } + typec_mux_unlock(mux); +} +EXPORT_SYMBOL_GPL(typec_mux_abort); + +int typec_mux_begin(struct typec_mux *mux, struct typec_mux_state *state) +{ + unsigned int i; + int index, ret; if (IS_ERR_OR_NULL(mux)) return 0; + index = srcu_read_lock(&typec_mux_srcu); + /* Nonblocking acquisition also rejects same-domain forwarding recursion. */ + if (!mutex_trylock(&mux->context->lock)) { + srcu_read_unlock(&typec_mux_srcu, index); + return -EBUSY; + } + mux->srcu_index = index; + mux->owner = current; + mux->transaction = *state; + for (i = 0; i < mux->num_mux_devs; i++) { + struct typec_mux_dev *dev = mux->mux_devs[i]; + if (READ_ONCE(dev->removed)) { + typec_mux_unlock(mux); + return -ENODEV; + } + } for (i = 0; i < mux->num_mux_devs; i++) { - mux_dev = mux->mux_devs[i]; + struct typec_mux_dev *dev = mux->mux_devs[i]; + + mux->prepared = i + 1; + if (!dev->prepare) + continue; + ret = dev->prepare(dev, &mux->transaction); + if (ret) + goto abort; + } + return 0; +abort: + typec_mux_abort(mux); + return ret; +} +EXPORT_SYMBOL_GPL(typec_mux_begin); - ret = mux_dev->set(mux_dev, state); - if (ret && ret != -EOPNOTSUPP) +int typec_mux_apply(struct typec_mux *mux) +{ + unsigned int i; + int ret; + + if (IS_ERR_OR_NULL(mux)) + return 0; + if (WARN_ON_ONCE(mux->owner != current)) + return -EINVAL; + if (mux->applied) + return 0; + for (i = 0; i < mux->num_mux_devs; i++) { + struct typec_mux_dev *dev = mux->mux_devs[i]; + + ret = dev->set(dev, &mux->transaction); + if (ret && ret != -EOPNOTSUPP) { + typec_mux_abort(mux); return ret; + } + } + mux->applied = true; + return 0; +} +EXPORT_SYMBOL_GPL(typec_mux_apply); + +int typec_mux_end(struct typec_mux *mux) +{ + unsigned int i; + int ret; + + if (IS_ERR_OR_NULL(mux)) + return 0; + ret = typec_mux_apply(mux); + if (ret) + return ret; + /* Finalizers can fail, but must not release their software reservation. */ + for (i = 0; i < mux->num_mux_devs; i++) { + struct typec_mux_dev *dev = mux->mux_devs[i]; + + if (!dev->finish) + continue; + ret = dev->finish(dev, &mux->transaction); + if (ret) + goto abort; } + /* Only this no-fail publication phase may release owner reservations. */ + for (i = 0; i < mux->num_mux_devs; i++) { + struct typec_mux_dev *dev = mux->mux_devs[i]; + if (dev->commit) + dev->commit(dev, &mux->transaction); + } + typec_mux_unlock(mux); return 0; +abort: + typec_mux_abort(mux); + return ret; +} +EXPORT_SYMBOL_GPL(typec_mux_end); + +/* Preparation-only callers leave recovery ownership pending, never committed. */ +int typec_mux_prepare(struct typec_mux *mux, struct typec_mux_state *state) +{ + int ret = typec_mux_begin(mux, state); + + if (!ret) + typec_mux_abort(mux); + return ret; +} +EXPORT_SYMBOL_GPL(typec_mux_prepare); + +int typec_mux_set(struct typec_mux *mux, struct typec_mux_state *state) +{ + int ret = typec_mux_begin(mux, state); + + if (ret) + return ret; + return typec_mux_end(mux); } EXPORT_SYMBOL_GPL(typec_mux_set); @@ -429,7 +630,12 @@ typec_mux_register(struct device *parent, const struct typec_mux_desc *desc) if (!mux_dev) return ERR_PTR(-ENOMEM); + mux_dev->owner = parent->driver ? parent->driver->owner : NULL; mux_dev->set = desc->set; + mux_dev->prepare = desc->prepare; + mux_dev->finish = desc->finish; + mux_dev->commit = desc->commit; + mux_dev->abort = desc->abort; device_initialize(&mux_dev->dev); mux_dev->dev.parent = parent; @@ -462,8 +668,12 @@ EXPORT_SYMBOL_GPL(typec_mux_register); */ void typec_mux_unregister(struct typec_mux_dev *mux_dev) { - if (!IS_ERR_OR_NULL(mux_dev)) + if (!IS_ERR_OR_NULL(mux_dev)) { + WRITE_ONCE(mux_dev->removed, true); + /* Callbacks may nest through forwarders; SRCU permits that nesting. */ + synchronize_srcu(&typec_mux_srcu); device_unregister(&mux_dev->dev); + } } EXPORT_SYMBOL_GPL(typec_mux_unregister); diff --git a/drivers/usb/typec/mux.h b/drivers/usb/typec/mux.h index 12e1ac47a11f1..42e44211fd553 100644 --- a/drivers/usb/typec/mux.h +++ b/drivers/usb/typec/mux.h @@ -12,8 +12,14 @@ struct typec_switch_dev { }; struct typec_mux_dev { + struct module *owner; struct device dev; typec_mux_set_fn_t set; + typec_mux_set_fn_t prepare; + typec_mux_set_fn_t finish; + typec_mux_notify_fn_t commit; + typec_mux_notify_fn_t abort; + bool removed; }; #define to_typec_switch_dev(_dev_) container_of(_dev_, struct typec_switch_dev, dev) diff --git a/drivers/usb/typec/tipd/core.c b/drivers/usb/typec/tipd/core.c index 36a6a0abc349e..f7fbee64a344d 100644 --- a/drivers/usb/typec/tipd/core.c +++ b/drivers/usb/typec/tipd/core.c @@ -212,10 +212,11 @@ static int tps6598x_read_partner_identity(struct tps6598x *tps) return 0; } -static void tps6598x_set_data_role(struct tps6598x *tps, +static int tps6598x_set_data_role(struct tps6598x *tps, enum typec_data_role role, bool connected) { enum usb_role role_val; + int ret; if (role == TYPEC_HOST) role_val = USB_ROLE_HOST; @@ -225,8 +226,11 @@ static void tps6598x_set_data_role(struct tps6598x *tps, if (!connected) role_val = USB_ROLE_NONE; - usb_role_switch_set_role(tps->role_sw, role_val); + ret = usb_role_switch_set_role(tps->role_sw, role_val); + if (ret) + return ret; typec_set_data_role(tps->port, role); + return 0; } static int tps6598x_connect(struct tps6598x *tps, u32 status) @@ -259,7 +263,9 @@ static int tps6598x_connect(struct tps6598x *tps, u32 status) else typec_set_orientation(tps->port, TYPEC_ORIENTATION_NORMAL); typec_set_mode(tps->port, TYPEC_STATE_USB); - tps6598x_set_data_role(tps, TPS_STATUS_TO_TYPEC_DATAROLE(status), true); + ret = tps6598x_set_data_role(tps, TPS_STATUS_TO_TYPEC_DATAROLE(status), true); + if (ret) + return ret; tps->partner = typec_register_partner(tps->port, &desc); if (IS_ERR(tps->partner)) @@ -273,7 +279,7 @@ static int tps6598x_connect(struct tps6598x *tps, u32 status) return 0; } -static void tps6598x_disconnect(struct tps6598x *tps, u32 status) +static void tps6598x_drop_partner(struct tps6598x *tps, u32 status) { if (!IS_ERR(tps->partner)) typec_unregister_partner(tps->partner); @@ -281,11 +287,37 @@ static void tps6598x_disconnect(struct tps6598x *tps, u32 status) typec_set_pwr_opmode(tps->port, TYPEC_PWR_MODE_USB); typec_set_pwr_role(tps->port, TPS_STATUS_TO_TYPEC_PORTROLE(status)); typec_set_vconn_role(tps->port, TPS_STATUS_TO_TYPEC_VCONN(status)); - typec_set_orientation(tps->port, TYPEC_ORIENTATION_NONE); - typec_set_mode(tps->port, TYPEC_STATE_SAFE); - tps6598x_set_data_role(tps, TPS_STATUS_TO_TYPEC_DATAROLE(status), false); +} + +static int tps6598x_disconnect(struct tps6598x *tps, u32 status) +{ + struct typec_mux_state safe = { .mode = TYPEC_STATE_SAFE }; + bool prepared = tps->remove_safe_prepared; + int ret; + + tps->remove_safe_prepared = false; + tps6598x_drop_partner(tps, status); + if (!prepared) { + typec_set_orientation(tps->port, TYPEC_ORIENTATION_NONE); + ret = typec_mux_begin(tps->mux, &safe); + if (ret) + return ret; + /* Park while the host still supplies the PIPE backend clock. */ + ret = typec_mux_apply(tps->mux); + if (ret) + return ret; + } + ret = tps6598x_set_data_role(tps, TPS_STATUS_TO_TYPEC_DATAROLE(status), false); + if (ret) { + typec_mux_abort(tps->mux); + return ret; + } + ret = typec_mux_end(tps->mux); + if (ret) + return ret; power_supply_changed(tps->psy); + return 0; } int tps6598x_exec_cmd_tmo(struct tps6598x *tps, const char *cmd, @@ -529,7 +561,9 @@ static void tps6598x_handle_plug_event(struct tps6598x *tps, u32 status) if (ret) dev_err(tps->dev, "failed to register partner\n"); } else { - tps6598x_disconnect(tps, status); + ret = tps6598x_disconnect(tps, status); + if (ret) + dev_err(tps->dev, "hardware disconnect failed: %d\n", ret); } } @@ -776,6 +810,23 @@ static void cd321x_update_work(struct work_struct *work) /* A different dock must release the old routing before reconfiguration. */ was_disconnected |= partner_changed; + /* + * Report sink absence before any fallible hardware transition. A failed + * shutdown retains the route/PHY owner, but cannot keep the sink present. + * Every Type-C port references the same external DCP connector. Only the + * port which successfully acquired the shared display route may report a + * disconnect. A second DP sink can negotiate Alt Mode even though its mux + * request is rejected with -EBUSY; treating that sink as the connector + * owner would tear down the display carried by the first port. + */ + if (IS_REACHABLE(CONFIG_DRM) && cd321x->connector_fwnode && + dp_route_was_active && + (!new_connected || was_disconnected || !dp_connected || !dp_hpd || + dp_hpd_changed || dp_mode_changed)) { + drm_connector_oob_hotplug_event(cd321x->connector_fwnode, + connector_status_disconnected); + } + /* * ACIO carries the tunneled PCIe reset handshake over the still-live * Type-C data path. Close a disappearing USB4/TBT session before dropping @@ -798,20 +849,6 @@ static void cd321x_update_work(struct work_struct *work) goto retry; } - /* - * Every Type-C port references the same external DCP connector. Only the - * port which successfully acquired the shared display route may report a - * disconnect. A second DP sink can negotiate Alt Mode even though its mux - * request is rejected with -EBUSY; treating that sink as the connector - * owner would tear down the display carried by the first port. - */ - if (IS_REACHABLE(CONFIG_DRM) && cd321x->connector_fwnode && - dp_route_was_active && - (!new_connected || was_disconnected || !dp_connected || !dp_hpd || - dp_hpd_changed || dp_mode_changed)) { - drm_connector_oob_hotplug_event(cd321x->connector_fwnode, connector_status_disconnected); - } - /* Process partner disconnection or change */ if (!new_connected || partner_changed) { if (!IS_ERR(tps->partner)) @@ -1510,7 +1547,17 @@ cd321x_register_port(struct tps6598x *tps, struct fwnode_handle *fwnode) cd321x->state.alt = NULL; cd321x->state.mode = TYPEC_STATE_SAFE; cd321x->state.data = NULL; - cd321x->state_valid = !typec_set_mode(tps->port, TYPEC_STATE_SAFE); + ret = typec_set_mode(tps->port, TYPEC_STATE_SAFE); + cd321x->state_valid = !ret; + /* + * A fixed host can register its SuperSpeed root after the mux provider. + * Wait for that consumer at probe instead of exhausting the cable retry + * budget before it exists. Host startup does not depend on this port. + */ + if (ret == -EAGAIN) { + ret = -EPROBE_DEFER; + goto err_put_switch; + } cd321x->tbt_notifier.notifier_call = cd321x_tbt_notify; ret = typec_thunderbolt_switch_register_notifier(cd321x->tbt_switch, @@ -1525,6 +1572,8 @@ cd321x_register_port(struct tps6598x *tps, struct fwnode_handle *fwnode) typec_thunderbolt_switch_put(cd321x->tbt_switch); cd321x->tbt_switch = NULL; err_unregister_mux: + fwnode_handle_put(cd321x->connector_fwnode); + cd321x->connector_fwnode = NULL; typec_mux_put(cd321x->mux); cd321x->mux = NULL; err_unregister_altmodes: @@ -1549,6 +1598,8 @@ cd321x_unregister_port(struct tps6598x *tps) struct cd321x *cd321x = container_of(tps, struct cd321x, tps); cd321x_stop_work(cd321x); + fwnode_handle_put(cd321x->connector_fwnode); + cd321x->connector_fwnode = NULL; typec_thunderbolt_switch_put(cd321x->tbt_switch); cd321x->tbt_switch = NULL; typec_mux_put(cd321x->mux); @@ -1966,12 +2017,13 @@ tps25750_register_port(struct tps6598x *tps, struct fwnode_handle *fwnode) return 0; } -static void cd321x_remove(struct tps6598x *tps) +static int cd321x_remove(struct tps6598x *tps) { struct cd321x *cd321x = container_of(tps, struct cd321x, tps); struct typec_thunderbolt_switch_data tbt_switch_data = { .state = TYPEC_THUNDERBOLT_SWITCH_OFF, }; + struct typec_mux_state safe = { .mode = TYPEC_STATE_SAFE }; int ret; cd321x_stop_work(cd321x); @@ -1988,10 +2040,24 @@ static void cd321x_remove(struct tps6598x *tps) ret = typec_thunderbolt_switch_set(cd321x->tbt_switch, &tbt_switch_data); if (ret) - dev_warn(tps->dev, - "failed to close Thunderbolt/USB4 session on remove: %d\n", - ret); - usb_role_switch_set_role(tps->role_sw, USB_ROLE_NONE); + return ret; + /* Display quiescence must precede even an ordinary host-role shutdown. */ + typec_set_orientation(tps->port, TYPEC_ORIENTATION_NONE); + ret = typec_mux_begin(tps->mux, &safe); + if (ret) + return ret; + /* Park while the host still supplies the PIPE backend clock. */ + ret = typec_mux_apply(tps->mux); + if (ret) + return ret; + ret = usb_role_switch_set_role(tps->role_sw, USB_ROLE_NONE); + if (ret) { + typec_mux_abort(tps->mux); + return ret; + } + /* Same task/handle completes without another post-shutdown set callback. */ + tps->remove_safe_prepared = true; + return 0; } @@ -2000,7 +2066,8 @@ int tipd_init(struct tps6598x *tps) struct fwnode_handle *fwnode; u32 status; u32 vid; - int ret; + int ret, cleanup_ret; + bool reset_allowed = true; tps->reset = devm_gpiod_get_optional(tps->dev, "reset", GPIOD_OUT_LOW); if (IS_ERR(tps->reset)) @@ -2073,19 +2140,32 @@ int tipd_init(struct tps6598x *tps) if (ret) goto err_role_put; - ret = tps->data->register_port(tps, fwnode); - if (ret) + tps->mux = fwnode_typec_mux_get(fwnode); + if (IS_ERR(tps->mux)) { + ret = PTR_ERR(tps->mux); + /* No provider reference means rollback cannot prove SAFE. */ + reset_allowed = false; goto err_role_put; + } + + ret = tps->data->register_port(tps, fwnode); + if (ret) { + /* No live port remains to prove SAFE on failed registration. */ + reset_allowed = false; + goto err_mux_put; + } if (status & TPS_STATUS_PLUG_PRESENT) { ret = -EINVAL; if (!tps6598x_read_power_status(tps)) - goto err_unregister_port; + goto err_disconnect; if (!tps->data->read_data_status(tps)) - goto err_unregister_port; + goto err_disconnect; ret = tps->data->connect(tps, status); - if (ret) + if (ret) { dev_err(tps->dev, "failed to register partner\n"); + goto err_disconnect; + } } if (tps->irq) { @@ -2118,9 +2198,14 @@ int tipd_init(struct tps6598x *tps) return 0; err_disconnect: - tps6598x_disconnect(tps, 0); -err_unregister_port: + cleanup_ret = tps6598x_disconnect(tps, 0); + if (cleanup_ret) { + dev_err(tps->dev, "probe hardware rollback failed: %d\n", cleanup_ret); + reset_allowed = false; + } tps->data->unregister_port(tps); +err_mux_put: + typec_mux_put(tps->mux); err_role_put: usb_role_switch_put(tps->role_sw); err_fwnode_put: @@ -2129,7 +2214,8 @@ int tipd_init(struct tps6598x *tps) tps6598x_write64(tps, TPS_REG_INT_MASK1, 0); err_reset_controller: /* Reset PD controller to remove any applied patch */ - tps->data->reset(tps); + if (reset_allowed) + tps->data->reset(tps); return ret; } @@ -2137,6 +2223,8 @@ EXPORT_SYMBOL_GPL(tipd_init); void tipd_remove(struct tps6598x *tps) { + int ret = 0; + tipd_debugfs_unregister(tps); if (!tps->irq) @@ -2145,16 +2233,23 @@ void tipd_remove(struct tps6598x *tps) devm_free_irq(tps->dev, tps->irq, tps); if (tps->data->remove) - tps->data->remove(tps); + ret = tps->data->remove(tps); - tps6598x_disconnect(tps, 0); + if (!ret) + ret = tps6598x_disconnect(tps, 0); + else + tps6598x_drop_partner(tps, 0); + if (ret) + dev_err(tps->dev, "hardware teardown failed on remove: %d\n", ret); tps->data->unregister_port(tps); + typec_mux_put(tps->mux); usb_role_switch_put(tps->role_sw); /* Reset PD controller to remove any applied patch */ - tps->data->reset(tps); + if (!ret) + tps->data->reset(tps); - if (tps->reset) + if (!ret && tps->reset) gpiod_set_value_cansleep(tps->reset, 1); } EXPORT_SYMBOL_GPL(tipd_remove); diff --git a/drivers/usb/typec/tipd/tps6598x.h b/drivers/usb/typec/tipd/tps6598x.h index 3a807f1def6b2..ef3f7ff6531d9 100644 --- a/drivers/usb/typec/tipd/tps6598x.h +++ b/drivers/usb/typec/tipd/tps6598x.h @@ -295,7 +295,7 @@ struct tipd_data { irq_handler_t irq_handler; u64 irq_mask1; size_t tps_struct_size; - void (*remove)(struct tps6598x *tps); + int (*remove)(struct tps6598x *tps); int (*register_port)(struct tps6598x *tps, struct fwnode_handle *node); void (*unregister_port)(struct tps6598x *tps); void (*ready)(struct tps6598x *tps); @@ -313,6 +313,9 @@ struct tipd_data { }; struct tps6598x { + struct typec_mux *mux; + /* CD remove leaves one open SAFE transaction for generic completion. */ + bool remove_safe_prepared; struct device *dev; struct regmap *regmap; struct mutex lock; /* device lock */ diff --git a/include/linux/usb/typec_mux.h b/include/linux/usb/typec_mux.h index d58d60fcae21f..a18e4e0141adf 100644 --- a/include/linux/usb/typec_mux.h +++ b/include/linux/usb/typec_mux.h @@ -86,6 +86,9 @@ struct typec_mux_state { void *data; }; +typedef void (*typec_mux_notify_fn_t)(struct typec_mux_dev *mux, + struct typec_mux_state *state); + typedef int (*typec_mux_set_fn_t)(struct typec_mux_dev *mux, struct typec_mux_state *state); @@ -94,12 +97,26 @@ struct typec_mux_desc { typec_mux_set_fn_t set; const char *name; void *drvdata; + /* Quiesce consumers before any provider changes the shared route. */ + typec_mux_set_fn_t prepare; + /* Fallible finalization retains ownership; commit cannot fail. */ + typec_mux_set_fn_t finish; + typec_mux_notify_fn_t commit; + typec_mux_notify_fn_t abort; }; #if IS_ENABLED(CONFIG_TYPEC) struct typec_mux *fwnode_typec_mux_get(struct fwnode_handle *fwnode); void typec_mux_put(struct typec_mux *mux); +/* begin/end must run in the same task; failure retains provider recovery state. */ +/* Same-task begin/end; retain the handle and state pointees until end/abort. */ +int typec_mux_begin(struct typec_mux *mux, struct typec_mux_state *state); +/* Apply the providers while retaining the transaction and ownership. */ +int typec_mux_apply(struct typec_mux *mux); +int typec_mux_end(struct typec_mux *mux); +void typec_mux_abort(struct typec_mux *mux); +int typec_mux_prepare(struct typec_mux *mux, struct typec_mux_state *state); int typec_mux_set(struct typec_mux *mux, struct typec_mux_state *state); struct typec_mux_dev * @@ -118,6 +135,21 @@ static inline struct typec_mux *fwnode_typec_mux_get(struct fwnode_handle *fwnod static inline void typec_mux_put(struct typec_mux *mux) {} +static inline int typec_mux_begin(struct typec_mux *mux, struct typec_mux_state *state) +{ + return 0; +} + +static inline int typec_mux_apply(struct typec_mux *mux) { return 0; } + +static inline int typec_mux_end(struct typec_mux *mux) { return 0; } +static inline void typec_mux_abort(struct typec_mux *mux) {} + +static inline int typec_mux_prepare(struct typec_mux *mux, struct typec_mux_state *state) +{ + return 0; +} + static inline int typec_mux_set(struct typec_mux *mux, struct typec_mux_state *state) { return 0; diff --git a/tools/diagnostics/apple-atc-readback.py b/tools/diagnostics/apple-atc-readback.py deleted file mode 100644 index 6929e38064e25..0000000000000 --- a/tools/diagnostics/apple-atc-readback.py +++ /dev/null @@ -1,170 +0,0 @@ -#!/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 mmap -import os -from pathlib import Path -import struct -import tempfile -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 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('> (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') - 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 = [] - power = None - lanes = None - fd = os.open('/dev/mem', os.O_RDONLY | os.O_CLOEXEC) - try: - for address, (mask, value) in sorted(expected.items()): - mismatch = (read_word(fd, address) ^ 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)}) - 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, '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())) - - def test_read_mapping(self): - with tempfile.TemporaryFile() as stream: - stream.truncate(2 * mmap.PAGESIZE) - stream.seek(mmap.PAGESIZE + 4) - stream.write(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() diff --git a/tools/diagnostics/apple-dptx-training-test.c b/tools/diagnostics/apple-dptx-training-test.c deleted file mode 100644 index fd2bed592f4c3..0000000000000 --- a/tools/diagnostics/apple-dptx-training-test.c +++ /dev/null @@ -1,191 +0,0 @@ -// 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; -typedef uint64_t u64; -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 endpoint { bool wire_v2; }; -struct apple_epic_service { struct dptx_port *cookie; struct endpoint *ep; }; -static unsigned int calls; -static int failure; -#define PHY_MODE_DP 1 -#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) -#define min(a, b) ((a) < (b) ? (a) : (b)) -#define min_t(t, a, b) min((t)(a), (t)(b)) -#define trace_dptxport_apcall(port, ...) ((void)(port)) -#define dev_dbg(...) ((void)0) -#define dev_warn_ratelimited(...) ((void)0) -#define print_hex_dump_debug(...) ((void)0) -#define DUMP_PREFIX_OFFSET 0 - -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 -#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) -{ - (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 endpoint ep = { .wire_v2 = true }; - struct apple_epic_service service = { .cookie = &port, .ep = &ep }; - 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 < 27; command++) { - int expected = group ? -EOPNOTSUPP : 7; - - if (group == 0 && command == 3) - expected = -EOPNOTSUPP; - 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); - } - } - /* Real unified dispatcher: legacy replies retain fields/canaries and - * never enter native PHY training. All original v2 cases above stay. - */ - ep.wire_v2 = false; - for (unsigned int command = 3; command <= 4; command++) { - for (unsigned int length = 48; length <= 64; length += 16) { - unsigned int before = calls; - memset(request, 0xa5, 80); - memset(output, 0xc3, sizeof(output)); - assert(!dptxport_group_call(&service, 1, command, - request, length, reply, length)); - for (unsigned int i = 0; i < length; i++) { - bool cleared = command == 3 ? i < 4 : - (i >= 16 && i < 20) || i >= 32; - assert(reply[i] == (cleared ? 0 : 0xa5)); - } - assert(output[0] == 0xc3 && output[length + 1] == 0xc3); - assert(calls == before); - } - } - assert(dptxport_group_call(&service, 0, 3, request, 80, reply, 80) == 7); - ep.wire_v2 = true; - 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)); - assert(!get_unaligned_le32(reply + 16)); -#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); - return 0; -} diff --git a/tools/diagnostics/apple-j700-dp-test.c b/tools/diagnostics/apple-j700-dp-test.c deleted file mode 100644 index 23caf5df299c0..0000000000000 --- a/tools/diagnostics/apple-j700-dp-test.c +++ /dev/null @@ -1,276 +0,0 @@ -// 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 dev_dbg 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, dp_idle_pmas; - bool dp_spread_spectrum, swap_lanes, mode_dirty, 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 u32 transactions[2048], transaction_masks[2048]; -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; - assert(writes < ARRAY_SIZE(transactions)); - transaction_masks[writes] = mask; - transactions[writes++] = off; - 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; -} - -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 }; - 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); - check_transmitter_order(pin_mask); - 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++; - } - } - } - 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(); - 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/display-attributes-fixture.json b/tools/diagnostics/display-attributes-fixture.json deleted file mode 100644 index 9324ae496d516..0000000000000 --- a/tools/diagnostics/display-attributes-fixture.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "MaxHorizontalImageSize": 60, - "MaxVerticalImageSize": 33, - "Chromaticity": { - "Red": {"X": 44608, "Y": 20288}, - "Green": {"X": 16960, "Y": 40448}, - "Blue": {"X": 9344, "Y": 5184} - }, - "DefaultWhitePoint": {"X": 20544, "Y": 21568, "Gamma": 144179}, - "WhitePoints": [{"X": 20544, "Y": 21568, "Gamma": 144179}], - "DefaultColorSpaceIsSRGB": true, - "SupportsPQEOTF": true, - "ProductAttributes": { - "LegacyManufacturerID": 1138, - "ProductID": 2895, - "SerialNumber": 305419896, - "WeekOfManufacture": 7, - "YearOfManufacture": 2013, - "AlphanumericSerialNumber": "ANON000000001", - "ProductName": "QG270 S3", - "ManufacturerID": "ACR" - } -} diff --git a/tools/diagnostics/test-apple-afk-admission.py b/tools/diagnostics/test-apple-afk-admission.py deleted file mode 100644 index feb21910537dd..0000000000000 --- a/tools/diagnostics/test-apple-afk-admission.py +++ /dev/null @@ -1,118 +0,0 @@ -#!/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, closed; 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)') diff --git a/tools/diagnostics/test-apple-atc-aux.py b/tools/diagnostics/test-apple-atc-aux.py deleted file mode 100644 index 00f29b0a410b3..0000000000000 --- a/tools/diagnostics/test-apple-atc-aux.py +++ /dev/null @@ -1,125 +0,0 @@ -#!/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_', '#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 - 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 -#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); - } - 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; -} -''' -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) diff --git a/tools/diagnostics/test-apple-atc-unwind.py b/tools/diagnostics/test-apple-atc-unwind.py deleted file mode 100644 index 0494b16fb3bc7..0000000000000 --- a/tools/diagnostics/test-apple-atc-unwind.py +++ /dev/null @@ -1,295 +0,0 @@ -#!/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 deleted file mode 100644 index 3d6e4cb5593e0..0000000000000 --- a/tools/diagnostics/test-apple-dcp-readiness.py +++ /dev/null @@ -1,246 +0,0 @@ -#!/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) diff --git a/tools/diagnostics/test-apple-display-metadata-lifetime.py b/tools/diagnostics/test-apple-display-metadata-lifetime.py deleted file mode 100644 index fa1ac9605f2c4..0000000000000 --- a/tools/diagnostics/test-apple-display-metadata-lifetime.py +++ /dev/null @@ -1,461 +0,0 @@ -#!/usr/bin/env python3 -# SPDX-License-Identifier: GPL-2.0-only -"""Exercise actual metadata epoch/cache and connector raw-EDID precedence paths.""" -from pathlib import Path -import os -import shlex -import subprocess -import tempfile - -root = Path(__file__).resolve().parents[2] -base = root / 'drivers/gpu/drm/apple' -dcp = (base / 'dcp.c').read_text() -iomfb = (base / 'iomfb.c').read_text() - - -def function(text, signature): - start = text.index(signature) - opening = text.index('{', start) - while ';' in text[start:opening]: - start = text.index(signature, start + 1) - opening = text.index('{', start) - end, depth = opening + 1, 1 - while depth: - depth += (text[end] == '{') - (text[end] == '}') - end += 1 - return text[start:end] - - -metadata = function((base / 'dcp-display-metadata.h').read_text(), 'struct dcp_display_metadata {') + ';' -epoch = (base / 'dcp-metadata-epoch.h').read_text().replace('#include ', '') -cache = dcp[dcp.index('static bool dcp_firmware_metadata_enabled('):dcp.index('void dcp_set_dimensions(')] -getter = function(iomfb, 'int dcp_get_modes(') -physical_current = function((base / 'epic/dpavservep.c').read_text(), 'bool dcpavserv_edid_current(') -service_current = function((base / 'epic/dpavservep.c').read_text(), 'bool dcpavserv_edid_service_current(') -cleanup = function(iomfb, 'static void dcp_clear_connector_edid(') + '\n' + function(iomfb, 'static void dcp_consume_edid_invalidation(') -hotplug = function(iomfb, 'void dcp_hotplug(') -disconnect = function(dcp, 'static void disconnected_hpd_event(') -chunk_callbacks = function((base / 'iomfb_template.c').read_text(), 'static u8 dcpep_cb_prop_start(') + '\n' + function((base / 'iomfb_template.c').read_text(), 'static u8 dcpep_cb_prop_chunk(') -# Check the actual ingress/egress hooks as well as executing the shared helpers. -assert dcp.count('dcp_firmware_metadata_connection(dcp, dcp->typec_generation, true)') == 2 -assert dcp.count('dcp_firmware_metadata_connection(dcp, dcp->typec_generation, false)') == 3 -assert dcp.count('dcp_firmware_metadata_connection(dcp, READ_ONCE(dcp->typec_generation), false)') == 2 -template = (base / 'iomfb_template.c').read_text() -assert 'dcp_firmware_metadata_begin(dcp);' in template -assert 'dcp_firmware_metadata_hotplug(dcp, !!(*connected));' in template -assert 'dcp_firmware_metadata_attributes(dcp, ret ? NULL : &metadata);' in template -assert 'dcp->chunks.received += req->length;' in template -assert 'dcp_property_complete(dcp->chunks.data, dcp->chunks.received,' in template -prefix = r''' -#include -#include -#include -#include -#include -#include -#include -#include -#include -typedef uint8_t u8; -typedef uint16_t u16; -typedef uint32_t u32; -typedef uint64_t u64; -#define BIT(n) (1u << (n)) -static unsigned int trace_observations; -#define trace_iomfb_metadata(d,p,g,t,f,r,l) do { \ - (void)(d); (void)(p); (void)(g); (void)(t); (void)(f); (void)(r); (void)(l); \ - trace_observations++; \ -} while (0) -#define READ_ONCE(x) (x) -#define WRITE_ONCE(x,v) ((x)=(v)) -#define xchg(p,v) __atomic_exchange_n(p,v,__ATOMIC_SEQ_CST) -#define lockdep_assert_held(p) assert(mutex_held(p)) -#define ERR_PTR(n) ((void *)(intptr_t)(n)) -#define PTR_ERR(p) ((int)(intptr_t)(p)) -#define IS_ERR(p) ((uintptr_t)(p) > (uintptr_t)-4096) -#define IS_ERR_OR_NULL(p) (!(p) || IS_ERR(p)) -#define DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL 2 -#define DCP_FIRMWARE_H17P 2 -#define DRM_MODE_TYPE_PREFERRED 2 -#define DCP_FW_VER 26 -#define DCP_FW_VERSION(a,b,c) (a) -#define SZ_1M (1024*1024) -#define GFP_KERNEL 0 -#define kzalloc(n,f) calloc(1,n) -#define dev_warn(...) ((void)0) -#define dev_err(...) ((void)0) -#define dev_info(...) ((void)0) -#define dev_dbg(...) ((void)0) -#define container_of(p,t,m) ((t *)((char *)(p) - offsetof(t,m))) -typedef struct { bool held; } spinlock_t; -#define spin_lock_irqsave(p, flags) do { assert(!(p)->held); (p)->held=true; (flags)=0; } while (0) -#define spin_unlock_irqrestore(p, flags) do { (void)(flags); assert((p)->held); (p)->held=false; } while (0) -struct drm_display_mode { unsigned int type; int clock; }; -struct dcp_display_mode { struct drm_display_mode mode; bool vrr, metadata_timing_valid; u32 min_vrr, max_vrr; }; -struct work_struct { unsigned int scheduled; }; -struct mutex { pthread_mutex_t native; }; -static _Thread_local struct mutex *held_mutex; -static _Thread_local bool modeset_held; -static _Thread_local struct mutex *held_stack[4]; -static _Thread_local unsigned int held_depth; -static bool mutex_held(struct mutex *m) { for(unsigned int i=0;inative)); held_stack[held_depth++]=m; held_mutex=m; } -static void mutex_unlock(struct mutex *m) { assert(held_mutex==m); assert(held_depth); held_depth--; held_mutex=held_depth ? held_stack[held_depth-1] : NULL; assert(!pthread_mutex_unlock(&m->native)); } -static struct mutex *acquire(struct mutex *m) { mutex_lock(m); return m; } -static void release(struct mutex **m) { if (*m) mutex_unlock(*m); } -static struct mutex *once(struct mutex *m) { mutex_unlock(m); return NULL; } -#define scoped_guard(kind,value) for(struct mutex *g __attribute__((cleanup(release)))=acquire(value); g; g=once(g)) -struct test_blob { unsigned int identity, references, magic; }; -struct drm_device { void *dev; struct { struct mutex mutex; } mode_config; }; -struct drm_connector { struct drm_device *dev; void *state; struct test_blob *edid_blob_ptr; struct { struct { unsigned int min_vfreq, max_vfreq; } monitor_range; } display_info; }; -struct dcp_set_dcpav_prop_chunk_req { u8 data[4096]; u32 offset, length; }; -struct apple_dcp; -struct platform_device { struct apple_dcp *data; }; -struct drm_edid { unsigned int identity; }; -struct apple_connector { - struct drm_connector base; - struct platform_device *dcp; - const struct drm_edid *drm_edid; - bool drm_edid_firmware_derived; - bool edid_invalidation_pending; - u64 drm_edid_generation, drm_edid_service_epoch; - bool connected; - struct work_struct hotplug_wq; -}; -#define to_apple_connector(p) container_of(p, struct apple_connector, base) -#define platform_get_drvdata(p) ((p)->data) -''' -state = r''' -struct apple_dcp { - void *dev; - struct mutex hpd_mutex; - struct { struct mutex lock; bool enabled; void *service; u64 epoch; } dcpavserv; - bool typec_cable_connected, quiescing; - u64 typec_generation; - struct { int iomfb_method_profile; } hw; - struct apple_connector *connector; - struct { void *data; size_t length, received; bool contiguous, payload_complete; struct dcp_metadata_ticket metadata_ticket; } chunks; - struct { - spinlock_t lock; - struct dcp_metadata_epoch epoch; - struct dcp_display_metadata attributes; - struct drm_display_mode preferred; - struct dcp_metadata_ticket attributes_ticket, timing_ticket; - bool attributes_valid, timing_valid, hpd_seen; - } metadata; - struct dcp_display_mode *modes; - unsigned int nr_modes; - int fw_compat; - struct { bool valid; } mode_state; - struct { bool wire_v2; } *dcpavservep; -}; -static struct apple_dcp *current_for_update; -static unsigned int allocations, raw_reads, mode_adds, clears, property_identity, blob_allocations, notifications, retrains; -static bool invalidate_during_update; -static bool update_error; -static bool raw_available; -static bool detach_during_read, replug_during_read; -static bool retire_after_read; -static const struct drm_edid *allocate(unsigned int identity) -{ - struct drm_edid *edid = malloc(sizeof(*edid)); assert(edid); - edid->identity = identity; allocations++; return edid; -} -static void drm_edid_free(const struct drm_edid *edid) -{ if (edid) { assert(allocations); allocations--; free((void *)edid); } } -static const struct drm_edid *dcp_metadata_edid(const struct dcp_display_metadata *m, - const struct drm_display_mode *mode) -{ assert(mode->clock); return allocate(m->serial); } -static void schedule_work(struct work_struct *w) { w->scheduled++; } -static struct drm_display_mode *drm_mode_duplicate(struct drm_device *d, const struct drm_display_mode *m) -{ (void)d; struct drm_display_mode *copy=malloc(sizeof(*copy)); *copy=*m; return copy; } -static void drm_mode_probed_add(struct drm_connector *c, struct drm_display_mode *m) -{ (void)c; mode_adds++; free(m); } -static void drm_connector_set_vrr_capable_property(struct drm_connector *c, bool v) -{ (void)c; (void)v; } -static bool dcp_has_panel(struct apple_dcp *p) { (void)p; return false; } -static const struct drm_edid *dcpavserv_copy_edid(struct apple_dcp *p, u64 generation, u64 *epoch) -{ - assert(!mutex_held(&p->hpd_mutex) && !mutex_held(&p->dcpavserv.lock)); - *epoch=p->dcpavserv.epoch; - if (retire_after_read) { p->dcpavserv.epoch+=2; p->connector->edid_invalidation_pending=true; } - (void)generation; raw_reads++; - if (detach_during_read) { - mutex_lock(&p->hpd_mutex); p->typec_generation++; - p->typec_cable_connected=replug_during_read; - mutex_unlock(&p->hpd_mutex); - } - return raw_available ? allocate(0xfeed) : ERR_PTR(-ENODEV); -} -static int drm_edid_connector_update(struct drm_connector *c, const struct drm_edid *e) -{ - assert(mutex_held(&c->dev->mode_config.mutex)); - if (e && e->identity==0xfeed) { - assert(mutex_held(¤t_for_update->dcpavserv.lock)); - if(current_for_update->hw.iomfb_method_profile==DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) - assert(mutex_held(¤t_for_update->hpd_mutex)); - } - if (e && update_error) return -ENOMEM; - struct test_blob *old=c->edid_blob_ptr; - struct test_blob *next=NULL; - if (e) { - next=malloc(sizeof(*next)); assert(next); - *next=(struct test_blob){ .identity=e->identity, .references=1, .magic=0xcafe }; - blob_allocations++; - } else clears++; - if (old) { assert(old->magic==0xcafe && old->references==1); old->magic=0; free(old); blob_allocations--; } - c->edid_blob_ptr=next; - property_identity=e ? e->identity : 0; - if (invalidate_during_update && e && e->identity!=0xfeed) current_for_update->metadata.epoch.invalidated=true; - return 0; -} -static void dcp_update_connector_dimensions(struct apple_dcp *p, struct drm_connector *c) { (void)p; assert(mutex_held(&c->dev->mode_config.mutex)); } -static bool dcp_is_usb4_output(struct apple_dcp *p) { (void)p; return false; } -static bool apple_dp_tunnel_t602x(void) { return false; } -#define DRM_MODE_LINK_STATUS_BAD 1 -static void drm_connector_set_link_status_property(struct drm_connector *c, int status) { (void)c; (void)status; } -static int dcp_retrain_active_crtc(struct apple_connector *c) -{ - assert(held_mutex!=&c->base.dev->mode_config.mutex); - retrains++; - return 0; -} -static void drm_kms_helper_connector_hotplug_event(struct drm_connector *c) -{ - assert(held_mutex!=&c->dev->mode_config.mutex); - mutex_lock(&c->dev->mode_config.mutex); - if (c->edid_blob_ptr) assert(c->edid_blob_ptr->magic==0xcafe); - notifications++; - mutex_unlock(&c->dev->mode_config.mutex); -} -static void dcp_retry_placeholder_edid(struct apple_dcp *p, const struct drm_edid *e) -{ (void)p; assert(e->identity==0xfeed); } -''' -suffix = r''' -static struct apple_dcp p; -static struct apple_connector c; -static struct platform_device platform; -static struct drm_device drm={ .mode_config.mutex.native=PTHREAD_MUTEX_INITIALIZER }; -static struct dcp_display_mode modes[1] = {{ .mode={ .type=DRM_MODE_TYPE_PREFERRED, .clock=148500 }, .metadata_timing_valid=true }}; -static struct dcp_display_metadata attrs = { .serial=1234 }; -static int probe(struct drm_connector *connector) -{ - mutex_lock(&connector->dev->mode_config.mutex); - int ret=dcp_get_modes(connector); - mutex_unlock(&connector->dev->mode_config.mutex); - return ret; -} -static void initial(void) -{ - mutex_lock(&drm.mode_config.mutex); - drm_edid_free(c.drm_edid); - c.base.dev=&drm; - drm_edid_connector_update(&c.base,NULL); - mutex_unlock(&drm.mode_config.mutex); - free(p.chunks.data); - assert(!allocations); - memset(&p,0,sizeof(p)); memset(&c,0,sizeof(c)); - assert(!pthread_mutex_init(&p.hpd_mutex.native,NULL)); - assert(!pthread_mutex_init(&p.dcpavserv.lock.native,NULL)); - p.dcpavserv.enabled=true; p.dcpavserv.service=&p; p.dcpavserv.epoch=1; - p.typec_cable_connected=true; p.typec_generation=1; - detach_during_read=replug_during_read=retire_after_read=false; - p.hw.iomfb_method_profile=DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL; - p.fw_compat=DCP_FIRMWARE_H17P; - static typeof(*p.dcpavservep) av={true}; p.dcpavservep=&av; - p.connector=&c; p.modes=modes; p.nr_modes=1; - platform.data=&p; c.dcp=&platform; c.base.dev=&drm; c.connected=true; - p.mode_state.valid=true; - raw_reads=mode_adds=clears=property_identity=0; raw_available=false; - current_for_update=&p; invalidate_during_update=false; update_error=false; modes[0].metadata_timing_valid=true; -} -static void begin(bool complete) -{ - dcp_firmware_metadata_begin(&p); - p.chunks.length=100; p.chunks.received=complete ? 100 : 99; - p.chunks.payload_complete=complete; -} -static bool eligible(void) -{ - u64 generation; - const struct drm_edid *edid=dcp_firmware_edid(&p,&generation); - bool valid=!IS_ERR(edid); - if (valid) { assert(generation==1 && edid->identity==attrs.serial); drm_edid_free(edid); } - return valid; -} -static void publish(void) -{ - begin(true); dcp_firmware_metadata_attributes(&p,&attrs); - begin(true); dcp_firmware_metadata_timing(&p); -} -static void *probe_thread(void *unused) -{ - (void)unused; - for(unsigned int i=0;i<1000;i++) { - mutex_lock(&drm.mode_config.mutex); - c.connected=true; raw_available=!!(i&1); - assert(dcp_get_modes(&c.base)==1); - mutex_unlock(&drm.mode_config.mutex); - } - return NULL; -} -static void *hotplug_thread(void *unused) -{ - (void)unused; - for(unsigned int i=0;i<1000;i++) { - mutex_lock(&drm.mode_config.mutex); c.connected=false; mutex_unlock(&drm.mode_config.mutex); - dcp_hotplug(&c.hotplug_wq); - } - return NULL; -} -static void *reader_thread(void *unused) -{ - (void)unused; - for(unsigned int i=0;i<2000;i++) { - mutex_lock(&drm.mode_config.mutex); - struct test_blob *b=c.base.edid_blob_ptr; - if(b) { - assert(b->magic==0xcafe && b->references==1); - b->references++; - assert(b->identity==1234 || b->identity==0xfeed); - b->references--; - } - mutex_unlock(&drm.mode_config.mutex); - } - return NULL; -} -int main(void) -{ - initial(); begin(true); /* A pre-association blob cannot become current later. */ - dcp_firmware_metadata_connection(&p,1,true); - dcp_firmware_metadata_attributes(&p,&attrs); - begin(true); dcp_firmware_metadata_timing(&p); - assert(!eligible()); - begin(true); dcp_firmware_metadata_attributes(&p,&attrs); - assert(eligible() && c.hotplug_wq.scheduled); - /* Raw failure admits fallback, but fallback never blocks a later raw read. */ - assert(probe(&c.base)==1 && c.drm_edid_firmware_derived && raw_reads==1); - assert(!c.drm_edid && property_identity==1234 && !allocations && mode_adds==1); - raw_available=true; - assert(probe(&c.base)==1 && !c.drm_edid_firmware_derived && raw_reads==2); - assert(c.drm_edid->identity==0xfeed && mode_adds==2); - assert(probe(&c.base)==1 && raw_reads==2); /* A raw cache remains authoritative. */ - - initial(); dcp_firmware_metadata_connection(&p,1,true); - begin(false); dcp_firmware_metadata_attributes(&p,&attrs); - begin(true); dcp_firmware_metadata_timing(&p); assert(!eligible()); - begin(true); p.chunks.contiguous=false; dcp_firmware_metadata_attributes(&p,&attrs); - assert(!eligible()); - begin(true); dcp_firmware_metadata_attributes(&p,&attrs); assert(eligible()); - assert(probe(&c.base)==1 && c.drm_edid_firmware_derived); - dcp_firmware_metadata_connection(&p,2,false); - dcp_firmware_metadata_connection(&p,3,true); - publish(); assert(!eligible()); /* First connection only, even with later complete records. */ - assert(probe(&c.base)==1 && !c.drm_edid && !c.drm_edid_firmware_derived && clears && !allocations); - - initial(); dcp_firmware_metadata_connection(&p,1,true); begin(true); - dcp_firmware_metadata_connection(&p,2,true); /* Change while a transfer is in flight. */ - dcp_firmware_metadata_attributes(&p,&attrs); assert(!eligible()); - initial(); dcp_firmware_metadata_connection(&p,1,true); - dcp_firmware_metadata_hotplug(&p,false); /* Initial no-HPD state is not a second attachment. */ - publish(); assert(eligible()); - dcp_firmware_metadata_hotplug(&p,true); - dcp_firmware_metadata_hotplug(&p,false); assert(!eligible()); - initial(); p.hw.iomfb_method_profile=0; - dcp_firmware_metadata_connection(&p,1,true); publish(); assert(!eligible()); - assert(probe(&c.base)==1 && !c.drm_edid); - initial(); dcp_firmware_metadata_connection(&p,1,true); - modes[0].metadata_timing_valid=false; publish(); assert(!eligible()); - initial(); dcp_firmware_metadata_connection(&p,1,true); publish(); - invalidate_during_update=true; - assert(probe(&c.base)==1 && !c.drm_edid_firmware_derived && !property_identity && !allocations); - initial(); dcp_firmware_metadata_connection(&p,1,true); publish(); update_error=true; - assert(probe(&c.base)==1 && !c.drm_edid_firmware_derived && !allocations); - initial(); dcp_firmware_metadata_connection(&p,1,true); - u32 length=64; - struct dcp_set_dcpav_prop_chunk_req chunk={ .length=32 }; - assert(dcpep_cb_prop_start(&p,&length)); - assert(dcpep_cb_prop_chunk(&p,&chunk)); - assert(p.chunks.received==32 && p.chunks.contiguous); - dcp_firmware_metadata_attributes(&p,&attrs); assert(!p.metadata.attributes_valid); - chunk.offset=32; assert(dcpep_cb_prop_chunk(&p,&chunk)); - p.chunks.payload_complete=true; /* Structural validator tested separately. */ - dcp_firmware_metadata_attributes(&p,&attrs); assert(p.metadata.attributes_valid); - initial(); dcp_firmware_metadata_connection(&p,1,true); - assert(dcpep_cb_prop_start(&p,&length)); - assert(dcpep_cb_prop_chunk(&p,&chunk)); /* Out-of-order first chunk. */ - chunk.offset=0; assert(dcpep_cb_prop_chunk(&p,&chunk)); - assert(!p.chunks.contiguous); - dcp_firmware_metadata_attributes(&p,&attrs); assert(!p.metadata.attributes_valid); - chunk.offset=UINT32_MAX; chunk.length=8; - assert(!dcpep_cb_prop_chunk(&p,&chunk)); - initial(); dcp_firmware_metadata_connection(&p,1,true); publish(); - assert(probe(&c.base)==1); - c.dcp=NULL; - dcp_hotplug(&c.hotplug_wq); - assert(!c.drm_edid_firmware_derived && !c.base.edid_blob_ptr); - c.dcp=&platform; c.connected=true; c.base.state=&p; p.mode_state.valid=false; - dcp_hotplug(&c.hotplug_wq); assert(retrains); - p.mode_state.valid=true; - assert(probe(&c.base)==1); - modeset_held=true; disconnected_hpd_event(&c); modeset_held=false; - assert(!c.connected && c.edid_invalidation_pending && c.base.edid_blob_ptr); - dcp_hotplug(&c.hotplug_wq); - assert(!c.edid_invalidation_pending && !c.drm_edid_firmware_derived && !c.base.edid_blob_ptr); - /* A fast reconnect cannot retain a raw EDID from the previous attachment. */ - c.connected=true; raw_available=true; - assert(probe(&c.base)==1 && c.drm_edid); - unsigned int previous_reads=raw_reads; - modeset_held=true; disconnected_hpd_event(&c); modeset_held=false; - c.connected=true; - assert(probe(&c.base)==1 && raw_reads==previous_reads+1); - assert(!c.edid_invalidation_pending); - /* Worker-first ordering also consumes the invalidation after reconnect. */ - modeset_held=true; disconnected_hpd_event(&c); modeset_held=false; - c.connected=true; - dcp_hotplug(&c.hotplug_wq); - assert(!c.drm_edid && !c.edid_invalidation_pending); - assert(probe(&c.base)==1 && c.drm_edid); - initial(); dcp_firmware_metadata_connection(&p,1,true); publish(); - pthread_t a,b,reader; - assert(!pthread_create(&a,NULL,probe_thread,NULL)); - assert(!pthread_create(&b,NULL,hotplug_thread,NULL)); - assert(!pthread_create(&reader,NULL,reader_thread,NULL)); - assert(!pthread_join(a,NULL) && !pthread_join(b,NULL) && !pthread_join(reader,NULL)); - initial(); raw_available=true; detach_during_read=true; - assert(probe(&c.base)==1 && !c.drm_edid && !property_identity && !allocations); - assert(raw_reads==1 && !p.typec_cable_connected); - assert(probe(&c.base)==1 && raw_reads==1); /* No request while disconnected. */ - initial(); raw_available=true; detach_during_read=replug_during_read=true; - assert(probe(&c.base)==1 && !c.drm_edid && !property_identity && !allocations); - detach_during_read=false; - assert(probe(&c.base)==1 && c.drm_edid && c.drm_edid_generation==2); - p.typec_generation++; /* Missed worker: generation alone rejects the old cache. */ - assert(probe(&c.base)==1 && raw_reads==3 && c.drm_edid_generation==3); - initial(); raw_available=true; - assert(probe(&c.base)==1 && c.drm_edid && c.drm_edid_service_epoch==1); - p.dcpavserv.epoch+=2; /* Replacement without physical generation or worker. */ - assert(probe(&c.base)==1 && raw_reads==2 && c.drm_edid_service_epoch==3); - p.dcpavserv.enabled=false; p.dcpavserv.epoch++; - c.edid_invalidation_pending=true; raw_available=false; - assert(probe(&c.base)==1 && !c.drm_edid && !property_identity && !allocations); - initial(); raw_available=true; retire_after_read=true; - assert(probe(&c.base)==1 && !c.drm_edid && !property_identity && !allocations); - assert(p.typec_generation==1); /* Replacement in the check/publication gap. */ - retire_after_read=false; - assert(probe(&c.base)==1 && c.drm_edid_service_epoch==3); - assert(!c.edid_invalidation_pending); /* Old CLOSE cannot erase the fresh cache. */ - dcp_hotplug(&c.hotplug_wq); assert(c.drm_edid); - initial(); assert(!blob_allocations && trace_observations); - puts("PASS: mutex-serialized EDID/blob get_modes, hotplug, sysfs reader and same-epoch publication, first-connection scope, raw precedence, delayed disconnect/replug rejection, physical and service cache invalidation, service replacement publication gap, no added modes"); - return 0; -} -''' -with tempfile.TemporaryDirectory(prefix='dcp-metadata-lifetime-') as tmp: - tmp = Path(tmp) - (tmp / 'test.c').write_text(prefix + metadata + epoch + state + cache + chunk_callbacks + cleanup + physical_current + service_current + getter + hotplug + disconnect + suffix) - command = shlex.split(os.environ.get('CC','clang')) - command += ['-std=gnu11','-Wall','-Wextra','-Werror','-Wno-sign-compare','-O1','-g', - '-fsanitize=address,undefined','-fno-omit-frame-pointer','-pthread'] - subprocess.run(command + [str(tmp / 'test.c'),'-o',str(tmp / 'test')],check=True) - subprocess.run([str(tmp / 'test')],check=True) diff --git a/tools/diagnostics/test-apple-display-metadata.py b/tools/diagnostics/test-apple-display-metadata.py deleted file mode 100644 index 8a5e34e6c006f..0000000000000 --- a/tools/diagnostics/test-apple-display-metadata.py +++ /dev/null @@ -1,393 +0,0 @@ -#!/usr/bin/env python3 -# SPDX-License-Identifier: GPL-2.0-only -"""Exercise the actual metadata parser/builder and independently decode base EDID.""" -import argparse -import copy -import json -import os -from pathlib import Path -import shlex -import struct -import subprocess -import tempfile - -root = Path(__file__).resolve().parents[2] -base = root / 'drivers/gpu/drm/apple' -args = argparse.ArgumentParser(description=__doc__) -args.add_argument('--attributes-json', type=Path, - default=Path(__file__).with_name('display-attributes-fixture.json')) -args.add_argument('--timings-bin', type=Path, help='Optional captured TimingElements binary (not embedded in public fixtures)') -args.add_argument('--property-bin', type=Path, action='append', default=[], - help='Optional captured property to validate against delivered extent') -args = args.parse_args() -fixture = json.loads(args.attributes_json.read_text()) - - -def block(text, marker): - start = text.index(marker) - opening = text.index('{', start) - end, depth = opening + 1, 1 - while depth: - depth += (text[end] == '{') - (text[end] == '}') - end += 1 - return text[start:end] - - -class ObjectPairs(list): - pass - - -def wire(value, out): - out += b'\0' * (-len(out) % 4) - if isinstance(value, bool): - out += struct.pack('', '') -timing_fixture = [{ - "HorizontalAttributes": {"Total": 2200, "FrontPorch": 88, "SyncWidth": 44, - "Active": 1920, "PreciseSyncRate": 4423680, "SyncPolarity": 1, "PixelRepetition": 0}, - "VerticalAttributes": {"Total": 1125, "FrontPorch": 4, "SyncWidth": 5, - "Active": 1080, "PreciseSyncRate": 3932160, "SyncPolarity": 1, "PixelRepetition": 0}, - "ColorModes": [{"ID": 85, "Score": 1, "IsVirtual": False, "PixelEncoding": 0, - "Colorimetry": 10, "Depth": 10, "DynamicRange": 0, "EOTF": 0}], - "ID": 62, "Score": 16224, "IsPreferred": True, "IsInterlaced": False, "IsVirtual": False, -}] -timing_raw = bytearray(struct.pack(' -#include -#include -#include -#include -#include -#include -#include -typedef uint8_t u8; -typedef uint16_t u16; -typedef uint32_t u32; -typedef uint64_t u64; -typedef int64_t s64; -typedef u16 __le16; -typedef u16 __be16; -typedef u32 __le32; -#define __packed __attribute__((packed)) -#undef static_assert -#define static_assert(c) _Static_assert(c, #c) -#define IS_ENABLED(x) 0 -#define U32_MAX UINT32_MAX -#define U16_MAX UINT16_MAX -#define S64_MAX INT64_MAX -#define S64_MIN INT64_MIN -#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) -#define round_up(n, a) (((n) + (a) - 1) & ~((a) - 1)) -#define ERR_PTR(n) ((void *)(intptr_t)(n)) -#define PTR_ERR(p) ((int)(intptr_t)(p)) -#define IS_ERR(p) ((uintptr_t)(p) > (uintptr_t)-4096) -#define IS_ERR_OR_NULL(p) (!(p) || IS_ERR(p)) -#define GFP_KERNEL 0 -#define kmalloc(n, f) malloc(n) -#define kcalloc(n, s, f) calloc(n,s) -#define kfree(p) free(p) -#define mul_u32_u32(a, b) ((u64)(u32)(a) * (u32)(b)) -#define DIV_ROUND_CLOSEST_ULL(n, d) (((n) + (d) / 2) / (d)) -#define DIV_ROUND_CLOSEST(n, d) (((n) + (d) / 2) / (d)) -#define trace_iomfb_color_mode(...) ((void)0) -#define trace_iomfb_parse_mode_fail(...) ((void)0) -#define trace_iomfb_parse_mode_success(...) ((void)0) -#define trace_iomfb_timing_mode(...) ((void)0) -#define DRM_MODE_TYPE_DRIVER 1 -#define DRM_MODE_TYPE_PREFERRED 2 -#define DRM_MODE_FLAG_PHSYNC 1 -#define DRM_MODE_FLAG_NHSYNC 2 -#define DRM_MODE_FLAG_PVSYNC 4 -#define DRM_MODE_FLAG_NVSYNC 8 -#define cpu_to_le16(v) (v) -#define cpu_to_le32(v) (v) -#define cpu_to_be16(v) __builtin_bswap16(v) -struct drm_display_mode { - unsigned int type, flags; - int clock, vdisplay, vsync_start, vsync_end, vtotal; - int hdisplay, hsync_start, hsync_end, htotal, width_mm, height_mm; -}; -static void drm_mode_set_name(struct drm_display_mode *mode) { (void)mode; } -''' -stubs = r''' -struct drm_edid { struct edid raw; }; -static const struct drm_edid *drm_edid_alloc(const void *data, size_t size) -{ - assert(size == 128); - struct drm_edid *result = malloc(sizeof(*result)); - memcpy(&result->raw, data, size); - return result; -} -static void drm_edid_free(const struct drm_edid *edid) { free((void *)edid); } -static bool drm_edid_valid(const struct drm_edid *edid) -{ - const u8 *raw = (const u8 *)&edid->raw; - unsigned int sum = 0; - for (unsigned int i = 0; i < 128; i++) sum += raw[i]; - return !(sum & 255) && !raw[126] && !memcmp(raw, "\0\xff\xff\xff\xff\xff\xff\0", 8); -} -''' -suffix = r''' -static void epochs(void) -{ - struct dcp_metadata_epoch e = {0}; - struct dcp_metadata_ticket before = dcp_metadata_begin(&e); - assert(!dcp_metadata_matches(&e, before)); - dcp_metadata_epoch_connection(&e, 1, true); - struct dcp_metadata_ticket current = dcp_metadata_begin(&e); - assert(dcp_metadata_matches(&e, current)); - assert(!dcp_metadata_matches(&e, before)); /* Never retag pre-association metadata. */ - dcp_metadata_epoch_connection(&e, 1, true); - assert(dcp_metadata_matches(&e, current)); - dcp_metadata_epoch_connection(&e, 2, false); - assert(!dcp_metadata_matches(&e, current)); - dcp_metadata_epoch_connection(&e, 3, true); - assert(!dcp_metadata_matches(&e, dcp_metadata_begin(&e))); /* Later attachment ineligible. */ - e = (struct dcp_metadata_epoch){0}; - dcp_metadata_epoch_connection(&e, 9, true); - current = dcp_metadata_begin(&e); - dcp_metadata_epoch_connection(&e, 10, true); /* A route change also invalidates. */ - assert(!dcp_metadata_matches(&e, current)); -} -int main(int argc, char **argv) -{ - if (argc == 3 && !strcmp(argv[1], "--property")) { - FILE *f=fopen(argv[2],"rb"); assert(f && !fseek(f,0,SEEK_END)); - long n=ftell(f); assert(n>20 && !fseek(f,0,SEEK_SET)); - unsigned char *b=malloc(n); assert(fread(b,n,1,f)==1); fclose(f); - assert(dcp_property_complete(b,n-1,n)); - assert(dcp_property_complete(b,n-1,n-1)); - for(unsigned int cut=1;cut<=16;cut++) - assert(!dcp_property_complete(b,n-1-cut,n-cut)); - assert(!dcp_property_complete(b,n-1,n+1)); - free(b); return 0; - } - assert(argc == 4 || argc == 5); - /* A missing final string byte must not be supplied by zero allocation. */ - unsigned char incomplete[]={0xd3,0,0,0,1,0,0,0x82,1,0,0,0x89,0x42}; - assert(dcp_property_complete(incomplete,sizeof(incomplete),sizeof(incomplete))); - assert(!dcp_property_complete(incomplete,12,13)); - assert(!dcp_property_complete(NULL,0,0)); - epochs(); - timing_proof(); - FILE *f = fopen(argv[1], "rb"); - assert(f && !fseek(f, 0, SEEK_END)); - long length = ftell(f); - assert(length > 0 && !fseek(f, 0, SEEK_SET)); - void *data = malloc(length); - assert(fread(data, length, 1, f) == 1); - fclose(f); - struct dcp_parse_ctx ctx; - struct dcp_display_metadata metadata, unchanged; - memset(&metadata, 0xa5, sizeof(metadata)); unchanged = metadata; - assert(!parse(data, length, &ctx)); - int ret = parse_display_metadata(&ctx, &metadata); - if (atoi(argv[2])) { - assert(ret == -EINVAL && !memcmp(&metadata, &unchanged, sizeof(metadata))); - free(data); return 0; - } - assert(!ret); - assert(dcp_property_complete(data,length,length)); - size_t aligned=round_up((size_t)length,4); - unsigned char *padded=malloc(aligned+4); - memset(padded,0xa5,aligned+4); memcpy(padded,data,length); - assert(dcp_property_complete(padded,aligned,aligned+1)); - assert(!dcp_property_complete(data,length-1,length)); - assert(!dcp_property_complete(padded,aligned,aligned+2)); - assert(!dcp_property_complete(padded,aligned+4,aligned+5)); - assert(!dcp_property_complete(padded,aligned-1,aligned+1)); - free(padded); - struct dcp_parse_ctx timing_ctx; - unsigned int count = 0; - assert(!parse(timing_blob, sizeof(timing_blob), &timing_ctx)); - struct dcp_display_mode *timings = enumerate_modes(&timing_ctx, &count, - metadata.width_cm * 10, metadata.height_cm * 10, 0, false, true); - assert(!IS_ERR(timings) && count == 1 && timings[0].metadata_timing_valid); - struct drm_display_mode mode = timings[0].mode; - assert(mode.flags == (DRM_MODE_FLAG_PHSYNC | DRM_MODE_FLAG_PVSYNC)); - assert(mode.clock == 148500 && mode.hdisplay == 1920 && mode.vdisplay == 1080); - free(timings); - const struct drm_edid *edid = dcp_metadata_edid(&metadata, &mode); - assert(!IS_ERR(edid)); - f = fopen(argv[3], "wb"); assert(f); - assert(fwrite(&edid->raw, 128, 1, f) == 1); fclose(f); - drm_edid_free(edid); - mode.flags = 0; - assert(IS_ERR(dcp_metadata_edid(&metadata, &mode))); - mode.flags = DRM_MODE_FLAG_PHSYNC | DRM_MODE_FLAG_PVSYNC; - mode.clock++; - assert(IS_ERR(dcp_metadata_edid(&metadata, &mode))); - free(data); - if (argc == 5) { - f=fopen(argv[4],"rb"); assert(f && !fseek(f,0,SEEK_END)); - length=ftell(f); assert(length>0 && !fseek(f,0,SEEK_SET)); - data=malloc(length); assert(fread(data,length,1,f)==1); fclose(f); - assert(!parse(data,length,&timing_ctx)); count=0; - timings=enumerate_modes(&timing_ctx,&count,600,330,0,false,true); - assert(!IS_ERR(timings)); bool found60=false, found120=false; - for(unsigned int i=0;iH', 1138) - assert struct.unpack_from('> 4) << 8) == 1920 - assert raw[57] + ((raw[58] & 15) << 8) == 280 - assert raw[59] + ((raw[61] >> 4) << 8) == 1080 - assert raw[60] + ((raw[61] & 15) << 8) == 45 - assert raw[62] == 88 and raw[63] == 44 and raw[64] == 0x45 - assert raw[71] == 0x1e - chroma = [raw[27] * 4 + ((raw[25] >> 6) & 3), - raw[28] * 4 + ((raw[25] >> 4) & 3), - raw[29] * 4 + ((raw[25] >> 2) & 3), raw[30] * 4 + (raw[25] & 3), - raw[31] * 4 + ((raw[26] >> 6) & 3), - raw[32] * 4 + ((raw[26] >> 4) & 3), - raw[33] * 4 + ((raw[26] >> 2) & 3), raw[34] * 4 + (raw[26] & 3)] - assert chroma == [697,317,265,632,146,81,321,337] - assert raw[72:77] == bytes.fromhex('000000fc00') - assert b'QG270 S3' in raw and b'DCP metadata' in raw - result = subprocess.run(['edid-decode', '--check', str(tmp / 'edid')], capture_output=True, text=True) - if result.returncode: - print(result.stdout) - print(result.stderr) - assert result.returncode == 0 - print(f'PASS: {len(cases)} actual schema cases, epoch transitions, exact base block and independent edid-decode') diff --git a/tools/diagnostics/test-apple-display-trace.py b/tools/diagnostics/test-apple-display-trace.py deleted file mode 100644 index f0c32db9e769f..0000000000000 --- a/tools/diagnostics/test-apple-display-trace.py +++ /dev/null @@ -1,140 +0,0 @@ -#!/usr/bin/env python3 -# SPDX-License-Identifier: GPL-2.0-only OR MIT -"""Host checks for bounded trace assignments and unchanged color callback flow.""" -import argparse -from pathlib import Path -import re -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() -root = args.source_root / 'drivers/gpu/drm/apple' -trace = (root / 'trace.h').read_text() -template = (root / 'iomfb_template.c').read_text() -afk = (root / 'afk.c').read_text() - -def trace_assignment(name): - start = trace.index('TRACE_EVENT(' + name + ',') - start = trace.index('TP_fast_assign(', start) + len('TP_fast_assign(') - end, depth = start, 1 - while depth: - depth += (trace[end] == '(') - (trace[end] == ')') - end += 1 - return trace[start:end - 1] - -def function(name): - match = re.search(r'static void ' + name + r'\([^;]*?\)\n\{', template) - if not match: - raise RuntimeError('missing callback: ' + name) - end, depth = match.end(), 1 - while depth: - depth += (template[end] == '{') - (template[end] == '}') - end += 1 - return template[match.start():end] - -prefix = r''' -#include -#include -#include -#include -#include -#include -typedef uint8_t u8; typedef uint32_t u32; typedef uint64_t u64; -#define __assign_str(x) ((void)0) -#define min_t(t,a,b) ((t)(a)<(t)(b)?(t)(a):(t)(b)) -struct apple_dcp { int unused; }; -struct ring { bool ready; u32 *rptr,*wptr,bufsz; }; -struct apple_dcp_afkep { u8 endpoint; bool inline_v2,stopping; int lock; struct ring rxbfr,txbfr; }; -#define READ_ONCE(x) (x) -#define le32_to_cpu(x) (x) -#define spin_lock_irqsave(l,f) ((void)(l),(f)=0) -#define spin_unlock_irqrestore(l,f) ((void)(l),(void)(f)) -#define dma_rmb() do {} while(0) -static bool enabled; -static unsigned int snapshots; -static u32 rx_seen,rx_write_seen,tx_seen,tx_write_seen; -static bool trace_afk_ring_snapshot_enabled(void) { return enabled; } -static void trace_afk_ring_snapshot(struct apple_dcp_afkep *ep,bool rr,u32 rp,u32 wp,u32 rs,bool tr,u32 tp,u32 tw,u32 ts) -{ (void)ep;(void)rr;(void)rs;(void)tr;(void)ts;snapshots++;rx_seen=rp;rx_write_seen=wp;tx_seen=tp;tx_write_seen=tw; } -struct header_entry { u8 endpoint;u32 channel,type;size_t data_size;u8 captured,header[32]; }; -struct ctm_entry { u32 location;u8 matrix_null;u64 matrix[9]; }; -static unsigned int calls, swapped;static u32 methods[2],results[2];static bool valids[2]; -static void *swap_cookie,*reply_data;static u8 ack[5];static bool null_ack; -static u32 get_unaligned_le32(const void *p) { const u8 *b=p;return b[0]|(u32)b[1]<<8|(u32)b[2]<<16|(u32)b[3]<<24; } -static void trace_iomfb_control_result(struct apple_dcp *d,u32 m,bool valid,u32 result) -{ (void)d;assert(calls<2);methods[calls]=m;valids[calls]=valid;results[calls++]=result; } -static void do_swap(struct apple_dcp *d,void *out,void *cookie) -{ (void)d;swapped++;swap_cookie=cookie;reply_data=out; } -static void dcp_pipe_set_428(struct apple_dcp *d,bool oob,const u32 *identity, - void (*cb)(struct apple_dcp*,void*,void*),void *cookie) -{ assert(!oob && *identity==0x10000);cb(d,null_ack?NULL:ack+1,cookie); } -''' -match = re.search(r"void afk_trace_ring_snapshot\([^;]*?\)\n\{", afk) -assert match -end, depth = match.end(), 1 -while depth: - depth += (afk[end] == '{') - (afk[end] == '}') - end += 1 -code = prefix + afk[match.start():end] + '\n' -code += 'static void capture(struct header_entry *__entry,struct apple_dcp_afkep *ep,u32 channel,u32 type,size_t data_size,const void *data) {\n' + trace_assignment('afk_recv_header') + '\n}\n' -code += 'static void capture_ctm(struct ctm_entry *__entry,u32 location,const void *matrix,u8 matrix_null) {\n' + trace_assignment('iomfb_ctm') + '\n}\n' -code += function('dcp_post_swap_a428') + '\n' + function('dcp_pre_swap_a428') -code += r''' -int main(void) -{ - struct apple_dcp_afkep ep={.endpoint=0x28};struct header_entry e;size_t n,i; - for(n=0;n<=64;n++) { - u8 *p=malloc(n?n:1);for(i=0;i -#include -#include -#include -#include -#include -#include -typedef int64_t s64;typedef uint32_t u32;typedef uint64_t u64; -#define U16_MAX UINT16_MAX -#define READ_ONCE(x) (x) -#define WRITE_ONCE(x,v) ((x)=(v)) -#define IS_ERR(p) ((uintptr_t)(p)>(uintptr_t)-4096) -#define PTR_ERR(p) ((int)(intptr_t)(p)) -#define DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL 2 -#define DPAVSERV_ENDPOINT 0x28 -#define DPAVSERV_ENDPOINT_V2 0x2a -#define DRM_CONNECTOR_REGISTERED 1 -#define to_platform_device(p) (p) -#define trace_dcpavserv_init(...) ((void)0) -#define dev_info(...) ((void)0) -#define dev_err(...) ((void)0) -static unsigned int warnings; -#define dev_warn(...) (warnings++) -static void mutex_lock(bool *p) { assert(!*p);*p=true; } -static void mutex_unlock(bool *p) { assert(*p);*p=false; } -#define lockdep_assert_held(p) assert(*(p)) -static unsigned int waits,snapshots,shutdowns,opens; -static void init_completion(bool *p) { *p=false; } -static void reinit_completion(bool *p) { *p=false; } -static void complete(bool *p) { *p=true; } -static unsigned int msecs_to_jiffies(unsigned int n) { return n; } -static int wait_for_completion_timeout(bool *p,unsigned int timeout) -{ assert(timeout==1000);waits++;return *p; } -static void schedule_work(unsigned int *p) { (*p)++; } -struct apple_dcp; -struct apple_dcp_afkep { struct apple_dcp *dcp; u32 endpoint; bool wire_v2,inline_v2,match_epic_name; }; -struct apple_epic_service { struct apple_dcp_afkep *ep; bool enabled,torndown; struct { u64 generation,epoch; } admission; unsigned int users,channel; void *cookie; }; -struct apple_connector { void *dcp; bool connected,edid_invalidation_pending; struct { int registration_state; } base; unsigned int hotplug_wq; }; -struct apple_dcp { - void *dev; struct { int iomfb_method_profile; } hw; - bool quiescing,hpd_mutex,typec_cable_connected; u64 typec_generation; unsigned int nr_modes; - struct apple_connector *connector; - struct apple_dcp_afkep *dcpavservep; - struct { bool lock,enabled,enable_completion; u64 epoch; struct apple_epic_service *service; } dcpavserv; -}; -static struct apple_epic_service *afk_service_get(struct apple_epic_service *s) -{ if(!s || !s->enabled || s->torndown) return NULL; s->users++;return s; } -static void afk_service_put(struct apple_epic_service *s) { assert(s->users);s->users--; } -static void afk_service_retire(struct apple_epic_service *s) { s->enabled=false;s->torndown=true; } -static const int dpavservep_ops[1]={0}; -static struct apple_dcp_afkep ep; -static struct apple_epic_service service; -static int start_error,announce_early; -static bool announce(struct apple_dcp *d,const char *name,const char *class,s64 unit); -static struct apple_dcp_afkep *afk_init(struct apple_dcp *d,u32 endpoint,const void *ops) -{ assert(ops==dpavservep_ops);ep=(struct apple_dcp_afkep){.dcp=d,.endpoint=endpoint};return &ep; } -static int afk_start(struct apple_dcp_afkep *e) -{ assert(e==&ep && e->dcp->dcpavservep==e);if(announce_early) assert(announce(e->dcp,"dcpav-service-epic","AppleDCPDP2HDMI",e->wire_v2?1:0));return start_error; } -static void afk_shutdown(struct apple_dcp_afkep *e) { assert(e==&ep);shutdowns++; } -static void afk_trace_ring_snapshot(struct apple_dcp_afkep *e) { assert(e==&ep);snapshots++; } -''' -parts=[function('static void dcpavserv_notify_edid('),function('static bool dcpavserv_identity('), - function('static bool dcpavserv_admission_current('),function('static int dcpavserv_prepare('), - function('static void dcpavserv_init('),function('static void dcpavserv_invalidate_edid('), - function('static void dcpavserv_teardown('),function('void dpavservep_detach('), - function('int dpavservep_init(')] -suffix=r''' -static bool announce(struct apple_dcp *d,const char *name,const char *class,s64 unit) -{ - assert(!d->dcpavserv.lock);service=(struct apple_epic_service){.ep=&ep,.channel=1}; - if(dcpavserv_prepare(&service,name,class,unit)) return false; - if(ep.wire_v2) opens++; /* Standard OPEN after exact prepare, before publish. */ - service.enabled=true;dcpavserv_init(&service,name,class,unit); - return d->dcpavserv.enabled; -} -static struct apple_dcp d; -static struct apple_connector c; -static int device; -static void initial(bool v2) -{ - if(service.users) dcpavserv_teardown(&service); - assert(!service.users); - memset(&d,0,sizeof(d));memset(&c,0,sizeof(c));memset(&service,0,sizeof(service)); - d.dev=&device;d.connector=&c;d.nr_modes=1; - d.hw.iomfb_method_profile=v2?2:0;c.dcp=&device;c.connected=true;c.base.registration_state=1; - waits=snapshots=warnings=shutdowns=opens=0;start_error=announce_early=0; -} -int main(void) -{ - initial(true);announce_early=1; - assert(dpavservep_init(&d)==0 && !waits && !warnings && !snapshots); - assert(d.dcpavserv.enabled && service.users==1 && d.dcpavserv.epoch==1 && opens==1 && c.hotplug_wq==1); - assert(ep.endpoint==0x2a && ep.wire_v2 && !ep.inline_v2); - initial(true); /* Late announcement after successful nonblocking startup. */ - assert(dpavservep_init(&d)==0 && !waits && !d.dcpavserv.enabled && !opens); - assert(announce(&d,"dcpav-service-epic","AppleDCPDP2HDMI",1)); - assert(service.users==1 && d.dcpavserv.enable_completion && c.hotplug_wq==1 && opens==1); - initial(true); /* Permanently missing service: no fake admission or timeout. */ - assert(dpavservep_init(&d)==0 && !waits && !warnings && !d.dcpavserv.enabled && !d.dcpavserv.service && !c.hotplug_wq); - assert(!announce(&d,"dcpav-service-epic","wrong",1)); - assert(!announce(&d,"dcpav-service-epic","AppleDCPDP2HDMI",0)); - assert(!announce(&d,"dcpdp-service-epic","AppleDCPDP2HDMI",3)); - assert(!d.dcpavserv.enabled && !service.users && !opens); - assert(announce(&d,"dcpav-service-epic","AppleDCPDP2HDMI",1)); - assert(opens==1 && d.dcpavserv.enabled); - initial(false);announce_early=1; - assert(dpavservep_init(&d)==0 && waits==1 && !warnings && !opens && ep.endpoint==0x28); - assert(service.users==1 && d.dcpavserv.enabled && !c.hotplug_wq); - initial(false); - assert(dpavservep_init(&d)==-ETIMEDOUT && waits==1 && warnings==1 && snapshots==1 && !d.dcpavserv.enabled); - initial(true);start_error=-EIO; - assert(dpavservep_init(&d)==-EIO && shutdowns==1 && !d.dcpavservep && !waits && !service.users); - initial(true); puts("PASS: actual DPAV init/admission/notify early, late, missing, wrong identity, legacy waits and AFK startup failure"); -} -''' -with tempfile.TemporaryDirectory(prefix='dpav-announcement-') as tmp: - p=Path(tmp)/'test.c';p.write_text(prefix+'\n'.join(parts)+suffix) - cc=shlex.split(os.environ.get('HOSTCC','clang')) - subprocess.run(cc+['-std=gnu11','-Wall','-Werror','-O1','-g','-fsanitize=address,undefined','-fno-omit-frame-pointer',str(p),'-o',str(Path(tmp)/'test')],check=True) - subprocess.run([str(Path(tmp)/'test')],check=True) diff --git a/tools/diagnostics/test-apple-dpav-rebind.py b/tools/diagnostics/test-apple-dpav-rebind.py deleted file mode 100644 index fda6fdf7396a0..0000000000000 --- a/tools/diagnostics/test-apple-dpav-rebind.py +++ /dev/null @@ -1,385 +0,0 @@ -#!/usr/bin/env python3 -# SPDX-License-Identifier: GPL-2.0-only OR MIT -"""Run actual AFK announcements, ownership and DPAV admission across epochs. - -Wire frames and the property interpreter are production code; the binary -property decoder receives a synthetic dictionary. Locks, DMA and OPEN sending -are host mocks. This does not qualify firmware, concurrency or display output. -""" -from pathlib import Path -import argparse, hashlib, os, shlex, subprocess, tempfile -parser = argparse.ArgumentParser(description=__doc__) -parser.add_argument("--source-root", type=Path, default=Path(__file__).resolve().parents[2]) -root = parser.parse_args().source_root.resolve() -base = root / 'drivers/gpu/drm/apple' -def function(file,signature): - text=(base/file).read_text();start=text.index(signature);opening=text.index('{',start) - while ';' in text[start:opening]: - start=text.index(signature,start+1);opening=text.index('{',start) - end,depth=opening+1,1 - while depth: - depth+=(text[end]=='{')-(text[end]=='}');end+=1 - return text[start:end] -prefix=r''' -#undef NDEBUG -#include -#include -#include -#include -#include "afkv2-codec.h" -typedef int64_t s64; -typedef u64 dma_addr_t; -#define U16_MAX UINT16_MAX -#define EBUSY 16 -#define ENOENT 2 -#define MAX_PENDING_CMDS 16 -#define AFK_MAX_CHANNEL 16 -#define READ_ONCE(x) (x) -#define WRITE_ONCE(x,v) ((x)=(v)) -#define WARN_ON(x) (x) -#define bitmap_empty(p,n) (!*(p)) -#define spin_lock_irqsave(p,f) do { assert(!*(p));*(p)=true;(f)=0; } while(0) -#define spin_unlock_irqrestore(p,f) do { (void)(f);assert(*(p));*(p)=false; } while(0) -static int test_bit(unsigned int bit,const unsigned long *p) { return !!(*p & (1UL<(b)?(a):(b)) -#define strscpy(dst,src,n) snprintf(dst,n,"%s",(char *)(src)) -#define trace_dcpavserv_init(...) ((void)0) -#define dma_rmb() ((void)0) -#define DRM_CONNECTOR_REGISTERED 1 -#define DPAVSERV_ENDPOINT_V2 0x2a -#define DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL 2 -#define EPIC_SUBTYPE_STD_SERVICE 0xc0 -#define EPIC_SUBTYPE_ANNOUNCE_H17P 0x11 -#define to_platform_device(x) (x) -#define ERR_PTR(x) ((void *)(intptr_t)(x)) -#define IS_ERR(x) ((uintptr_t)(x)>(uintptr_t)-4096) -#define lockdep_assert_held(p) assert(*(p)) -static bool *hpd_lock,*av_lock; -static void mutex_lock(bool *p) { if(p==hpd_lock) assert(!*av_lock);assert(!*p);*p=true; } -static void mutex_unlock(bool *p) { assert(*p);*p=false; } -static void complete(int *p) { (*p)++; } -static void reinit_completion(int *p) { *p=0; } -static void schedule_work(unsigned int *p) { (*p)++; } -struct apple_dcp;struct apple_dcp_afkep;struct apple_epic_service; -struct apple_epic_service_ops { char name[32];bool reusable;void (*init)(struct apple_epic_service *,const char *,const char *,s64);int (*prepare)(struct apple_epic_service *,const char *,const char *,s64);void (*teardown)(struct apple_epic_service *); }; -struct epic_cmd_info { u16 tag;u8 type;size_t reply_length;void *rxbuf,*txbuf;dma_addr_t rxbuf_dma,txbuf_dma;size_t rxlen,txlen;u32 retcode;bool done,free_on_ack;int *completion; }; -struct apple_epic_service { - struct apple_dcp_afkep *ep;const struct apple_epic_service_ops *ops; - bool lock,enabled,torndown;unsigned int users;u32 channel;u8 cmd_tag; - unsigned long cmd_map[1];struct epic_cmd_info cmds[MAX_PENDING_CMDS]; - struct { u64 generation,epoch; } admission;void *cookie; -}; -struct apple_dcp_afkep { struct apple_dcp *dcp;const struct apple_epic_service_ops *ops;bool wire_v2,inline_v2,match_epic_name;u32 endpoint;unsigned int num_channels;struct apple_epic_service services[AFK_MAX_CHANNEL]; }; -struct apple_connector { void *dcp;bool connected,edid_invalidation_pending;struct { int registration_state; } base;unsigned int hotplug_wq; }; -struct apple_dcp { - void *dev;int fw_compat;struct apple_dcp_afkep *dcpavservep;struct apple_connector *connector; - bool hpd_mutex,quiescing,typec_cable_connected;u64 typec_generation;unsigned int nr_modes; - struct { int iomfb_method_profile; } hw; - struct { bool lock,enabled;u64 epoch;int enable_completion;struct apple_epic_service *service; } dcpavserv; -}; -struct epic_cmd { u32 retcode;u64 rxbuf,txbuf;u32 rxlen,txlen; }; -static unsigned int dma_frees,opens; -static int open_error,open_action; -void dpavservep_detach(struct apple_dcp *dcp); -static void inject_owner(struct apple_dcp_afkep *ep,u32 pending_channel); -static void dma_free_coherent(void *dev,size_t size,void *p,dma_addr_t address) -{ (void)dev;assert(size && (uintptr_t)p==address);free(p);dma_frees++; } -static void afk_remove_service_debugfs(struct apple_epic_service *s) { (void)s; } -static void afk_populate_service_debugfs(struct apple_epic_service *s) { (void)s; } -static int afk_send_epic(struct apple_dcp_afkep *e,u32 channel,u16 tag,int type,int category,u8 subtype,const void *p,size_t n) -{ - assert(channel && !tag && type==EPIC_TYPE_NOTIFY && category==EPIC_CAT_REPORT && subtype==AFKV2_OPEN && !p && !n); - opens++; - assert(!e->dcp->dcpavserv.enabled && !e->dcp->dcpavserv.service); - for(unsigned int i=0;inum_channels;i++) { - struct apple_epic_service *s=&e->services[i]; - if(s->channel==channel && !s->torndown) assert(!s->enabled && !s->users); - } - if(open_action==1) e->dcp->typec_generation++; - if(open_action==2) dpavservep_detach(e->dcp); - if(open_action==4) inject_owner(e,channel); - if(open_action==5) e->dcp->typec_cable_connected=false; - if(open_action==6) e->dcp->quiescing=true; - if(open_action==3) { - for(unsigned int i=0;inum_channels;i++) - if(e->services[i].channel==channel && !e->services[i].torndown) e->services[i].channel++; - } - return open_error; -} -static void afk_v2_callback(struct apple_dcp_afkep *e,const struct afkv2_frame *f,const struct afkv2_command *c) { (void)e;(void)f;(void)c;assert(!"unexpected callback"); } - -struct item { const char *key,*value; s64 integer; }; -struct dcp_parse_ctx { struct item *items;unsigned int count,index;bool key; }; -struct iterator { struct dcp_parse_ctx *handle;unsigned int idx,len; }; -#define kfree(p) free((void *)(p)) -#define PTR_ERR(p) ((int)(intptr_t)(p)) -static struct item announcement_items[3]; -static int parse(u8 *p,size_t n,struct dcp_parse_ctx *ctx) -{ (void)p;assert(n==37);*ctx=(struct dcp_parse_ctx){.items=announcement_items,.count=3};return 0; } -static int iterator_begin(struct dcp_parse_ctx *h,struct iterator *it,bool dict) -{ assert(dict);h->index=0;h->key=true;*it=(struct iterator){.handle=h,.len=h->count};return 0; } -static char *parse_string(struct dcp_parse_ctx *h) -{ - struct item *i=&h->items[h->index];const char *p; - if(h->key) { p=i->key;h->key=false; } else { p=i->value;h->index++;h->key=true; } - if(!p) return ERR_PTR(-EINVAL);char *copy=malloc(strlen(p)+1);assert(copy);strcpy(copy,p);return copy; -} -static int parse_int(struct dcp_parse_ctx *h,s64 *out) -{ assert(!h->key);*out=h->items[h->index++].integer;h->key=true;return 0; } -static int skip(struct dcp_parse_ctx *h) { assert(!h->key);h->index++;h->key=true;return 0; } -''' -parts=[function('parser.c','int parse_epic_service_init('),function('afk.h','static inline struct apple_epic_service *afk_service_get('), - function('afk.h','static inline void afk_service_put('),function('afk.h','static inline void afk_service_retire('),function('afk.h','static inline void afk_service_disable('), - function('afk.h','static inline bool afk_service_matches('),function('afk.h','static inline bool afk_service_reclaimable('), - function('afk.h','static inline bool\nafk_service_reinit('),function('afk.h','static inline void afk_service_discard('), - function('afk.h','static inline void afk_service_publish('), - function('epic/dpavservep.c','static void dcpavserv_notify_edid('),function('epic/dpavservep.c','static bool dcpavserv_identity('), - function('epic/dpavservep.c','static bool dcpavserv_admission_current('),function('epic/dpavservep.c','static int dcpavserv_prepare('), - function('epic/dpavservep.c','static void dcpavserv_init('),function('epic/dpavservep.c','static void dcpavserv_invalidate_edid('), - function('epic/dpavservep.c','static void dcpavserv_teardown('),function('epic/dpavservep.c','void dpavservep_detach('), - function('afk.c','static const struct apple_epic_service_ops *\nafk_match_service('), - function('afk.c','static struct apple_epic_service *afk_epic_find_service('), - function('afk.c','static void afk_recv_handle_init('), - function('epic/dpavservep.c','static int dcpdpserv_prepare('), - function('epic/dpavservep.c','static void dcpdpserv_teardown('),function('afk.c','static void afk_recv_handle_teardown('), - function('afk.c','static void afk_recv_handle_reply('),function('afk.c','static void afk_v2_receive(')] -print('Production lifecycle functions SHA256:', hashlib.sha256('\n'.join(parts).encode()).hexdigest(), flush=True) -suffix=r''' -static struct apple_dcp d; -static struct apple_dcp_afkep ep; -static struct apple_connector con; -static const struct apple_epic_service_ops ops={.name="dcpav-service-epic",.reusable=true,.prepare=dcpavserv_prepare,.init=dcpavserv_init,.teardown=dcpavserv_teardown}; -static const struct apple_epic_service_ops announcement_ops[]={ - {.name="dcpav-service-epic",.reusable=true,.prepare=dcpavserv_prepare,.init=dcpavserv_init,.teardown=dcpavserv_teardown}, - {.name="dcpdp-service-epic",.reusable=true,.prepare=dcpdpserv_prepare,.teardown=dcpdpserv_teardown}, - {0} -}; -static const char *name="dcpav-service-epic",*class="AppleDCPDP2HDMI"; -static const char *native_class="DCPDP13Service"; -static struct apple_epic_service *prepare(u32 channel,s64 unit,const char *n,const char *c) -{ - for(unsigned int i=0;iusers==1); -} -static void repeated_announcements(void) -{ - ep.ops=announcement_ops;ep.match_epic_name=true;ep.num_channels=0;d.fw_compat=DCP_FIRMWARE_H17P; - /* More than the entire slot budget; wrap channel identity as well. */ - for(unsigned int i=0;i<4*AFK_MAX_CHANNEL;i++) { - u16 channel=1+4*(i%4);unsigned int before=opens; - /* Flip, same-attachment revalidation, return flip; swap both providers. */ - bool native=((i%4==1 || i%4==2) != !!((i/4)%2)); - const char *provider=native?native_class:class; - announce(channel,name,provider,channel); - struct apple_epic_service *owner=d.dcpavserv.service; - if(native) assert(owner && "native DP EDID provider must publish"); - assert(owner && owner->channel==channel && owner->users==1 && opens==before+1); - assert(ep.num_channels<=2); /* Real allocator reuses, no slot exhaustion. */ - u64 epoch=d.dcpavserv.epoch; - announce(channel,name,provider,channel); /* Active duplicate: no extra OPEN/pin. */ - assert(opens==before+1 && owner->users==1 && d.dcpavserv.epoch==epoch); - announce(channel+2,"dcpdp-service-epic",provider,channel+2); - assert(opens==before+1 && d.dcpavserv.service==owner && d.dcpavserv.epoch==epoch); - if(i%4==1) { - u64 generation=d.typec_generation; - closed(channel,0x6a01,0); /* Revalidation retires service, not attachment. */ - assert(!d.dcpavserv.service && d.typec_generation==generation && d.dcpavserv.epoch>epoch); - } else { - disconnect();connect(); - } - } - assert(ep.num_channels==2); /* Auxiliary rejection leaves a reusable slot. */ - unsigned int before=opens; - announce(13,name,"synthetic-wrong-class",13);assert(opens==before && !d.dcpavserv.service); - char long_class[161];memset(long_class,'x',160);long_class[160]=0; - announce(13,name,long_class,13);assert(opens==before && !d.dcpavserv.service); - for(unsigned int variant=0;variant<2;variant++) { - const char *provider=variant?native_class:class; - assert(!prepare(0,0,name,provider));assert(!prepare(65536,65536,name,provider)); - assert(!prepare(13,-1,name,provider));assert(!prepare(13,0,name,provider)); - assert(!prepare(13,1,name,provider));assert(!prepare(13,13,NULL,provider)); - assert(!prepare(13,13,"dcpdp-service-epic",provider)); - announce(13,"wrong-compartment",provider,13);assert(opens==before && !d.dcpavserv.service); - ep.endpoint=0x2c;announce(13,name,provider,13);assert(opens==before && !d.dcpavserv.service);ep.endpoint=0x2a; - d.quiescing=true;announce(13,name,provider,13);assert(opens==before && !d.dcpavserv.service);d.quiescing=false; - disconnect();announce(13,name,provider,13);assert(opens==before && !d.dcpavserv.service);connect(); - } - for(unsigned int variant=0;variant<4;variant++) { - const char *forged[]={"DCPDP13ServiceExtra","dcpdp13service","AppleDCPDP2HDMIExtra","unrelated-provider"}; - announce(13,name,forged[variant],13);assert(opens==before && !d.dcpavserv.service); - } - assert(!prepare(13,13,name,NULL)); - open_error=-EIO;announce(13,name,native_class,13);assert(opens==++before && !d.dcpavserv.service);open_error=0; - for(unsigned int window=0;window<5;window++) { - const int actions[]={1,2,3,5,6};open_action=actions[window]; - announce(13,name,native_class,13); - assert(opens==++before && !d.dcpavserv.service && !d.dcpavserv.enabled); - d.typec_cable_connected=true;d.quiescing=false; - } - open_action=4;announce(13,name,native_class,13);assert(opens==++before && d.dcpavserv.service==injected_owner); - assert(injected_owner->users==1);dpavservep_detach(&d);assert(!injected_owner->users); - open_action=0;announce(13,name,native_class,13);assert(opens==++before && d.dcpavserv.service); - struct apple_epic_service *old=d.dcpavserv.service; - assert(afk_service_get(old)==old);old->cmd_map[0]=1; - disconnect();connect(); - announce(13,name,native_class,13); /* Same channel still owns a pending reply. */ - assert(opens==before && !d.dcpavserv.service && old->users==1 && old->channel==13); - announce(1,name,class,1);assert(opens==++before && d.dcpavserv.service!=old); - struct apple_epic_service *fresh=d.dcpavserv.service;u64 epoch=d.dcpavserv.epoch; - dcpavserv_teardown(old);assert(d.dcpavserv.service==fresh && d.dcpavserv.epoch==epoch); - old->cmd_map[0]=0;afk_service_put(old);disconnect();connect(); - announce(13,name,native_class,13);assert(opens==++before && d.dcpavserv.service); - struct apple_epic_service *owner=d.dcpavserv.service; - u64 owner_epoch=d.dcpavserv.epoch; - announce(5,name,class,5); /* A second admitted identity cannot displace the owner. */ - assert(opens==before && d.dcpavserv.service==owner && d.dcpavserv.epoch==owner_epoch); - dpavservep_detach(&d); - /* All slots held by retired readers: fail safely, then recover after puts. */ - for(unsigned int i=0;itorndown); - legacy=prepare(13,0,name,native_class);assert(legacy); - d.typec_generation++;afk_service_publish(legacy);dcpavserv_init(legacy,name,native_class,0); - assert(d.dcpavserv.service==legacy && legacy->users==1);dpavservep_detach(&d); - assert(dcpdpserv_prepare(legacy,"dcpdp-service-epic",native_class,0)==0); - ep.wire_v2=true;ep.endpoint=0x2a; - announce(U16_MAX,name,native_class,U16_MAX);assert(d.dcpavserv.service && d.dcpavserv.service->channel==U16_MAX); - dpavservep_detach(&d); - puts("PASS: actual AFKV2 HDMI/native DP alternating over 64 cycles: flip/revalidation/return, strict identity, stale OPEN generation/epoch/owner, auxiliary isolation, reader/command pins and legacy controls"); -} -int main(void) -{ - d.hw.iomfb_method_profile=2;d.typec_cable_connected=true;d.typec_generation=1;d.nr_modes=1;d.connector=&con;d.dev=&d; - con.dcp=&d;con.connected=true;con.base.registration_state=1; - ep.dcp=&d;ep.wire_v2=true;ep.endpoint=0x2a;ep.num_channels=AFK_MAX_CHANNEL;d.dcpavservep=&ep; - hpd_lock=&d.hpd_mutex;av_lock=&d.dcpavserv.lock; - struct item h17p[]={{"compartment","dcpav-service-epic",0},{"name","AppleDCPDP2HDMI",0},{"interface-id",NULL,13}}; - struct dcp_parse_ctx parse_ctx={.items=h17p,.count=3};const char *parsed_name,*parsed_class;s64 parsed_unit=-1; - assert(!parse_epic_service_init(&parse_ctx,&parsed_name,&parsed_class,&parsed_unit,true)); - assert(parsed_unit==13 && !strcmp(parsed_name,name) && !strcmp(parsed_class,class)); - struct apple_epic_service *proof=prepare(13,parsed_unit,parsed_name,parsed_class);assert(proof);afk_service_discard(proof); - free((void *)parsed_name);free((void *)parsed_class); - struct item legacy[]={{"EPICName","dcpav-service-epic",0},{"EPICProviderClass","legacy-class",0},{"EPICUnit",NULL,0}}; - parse_ctx=(struct dcp_parse_ctx){.items=legacy,.count=3}; - assert(!parse_epic_service_init(&parse_ctx,&parsed_name,&parsed_class,&parsed_unit,false));assert(parsed_unit==0); - free((void *)parsed_name);free((void *)parsed_class); - struct apple_epic_service *old=prepare(1,1,name,class);assert(old);publish(old,1); - assert(d.dcpavserv.service==old && old->users==1); - assert(afk_service_get(old)==old); /* Delayed EDID reader pins the retired slot. */ - old->cmds[0]=(struct epic_cmd_info){.tag=0,.type=0xc0,.rxlen=32,.txlen=32,.free_on_ack=true}; - old->cmds[0].rxbuf=calloc(1,32);old->cmds[0].txbuf=calloc(1,32);assert(old->cmds[0].rxbuf && old->cmds[0].txbuf); - old->cmds[0].rxbuf_dma=(uintptr_t)old->cmds[0].rxbuf;old->cmds[0].txbuf_dma=(uintptr_t)old->cmds[0].txbuf;old->cmd_map[0]=1; - disconnect();assert(!d.dcpavserv.service && !d.dcpavserv.enabled && old->users==1 && old->torndown && !old->enabled); - assert(con.edid_invalidation_pending && !afk_service_get(old)); - assert(!afk_service_reinit(old,&ep,&ops,5)); - assert(!prepare(5,5,name,class)); /* No admission while physically disconnected. */ - connect();struct apple_epic_service *fresh=prepare(5,5,name,class);assert(fresh && fresh!=old);publish(fresh,5); - assert(d.dcpavserv.service==fresh && fresh->users==1); - u64 epoch=d.dcpavserv.epoch; - dcpavserv_teardown(old);assert(d.dcpavserv.service==fresh && d.dcpavserv.epoch==epoch); - closed(1,0x6a01,0);assert(d.dcpavserv.service==fresh && d.dcpavserv.epoch==epoch && !dma_frees); - u8 oob[32],reply[56];size_t written; - assert(!afkv2_encode_oob_command(oob,sizeof(oob),0,old->cmds[0].rxbuf_dma,32,old->cmds[0].txbuf_dma,32));afkv2_put32(oob+4,0); - assert(!afkv2_encode_frame(reply,sizeof(reply),1,1,0,0xc0,AFKV2_RESPONSE,0,oob,sizeof(oob),&written)); - afk_v2_receive(&ep,0,0,reply,written); - assert(dma_frees==2 && !old->cmd_map[0] && old->users==1 && d.dcpavserv.service==fresh && d.dcpavserv.epoch==epoch); - assert(!afk_service_reinit(old,&ep,&ops,9));afk_service_put(old); - afk_v2_receive(&ep,0,0,reply,written);assert(dma_frees==2); /* Duplicate late ACK. */ - disconnect();connect();struct apple_epic_service *next=prepare(9,9,name,class);assert(next);publish(next,9); - assert(d.dcpavserv.service==next); - closed(1,0x6a01,0);closed(5,0x6a01,0);assert(d.dcpavserv.service==next); - disconnect();connect();next=prepare(13,13,name,class);assert(next);publish(next,13);assert(d.dcpavserv.service==next); - closed(13,0x6a01,1);assert(d.dcpavserv.service==next); /* Nonzero result rejected. */ - closed(13,0x6a00,0);assert(d.dcpavserv.service==next); /* Wrong low flags rejected. */ - d.dcpavservep=NULL;closed(13,0x6a01,0);assert(d.dcpavserv.service==next);d.dcpavservep=&ep; - closed(13,0x6a01,0);assert(!d.dcpavserv.service && !d.dcpavserv.enabled); - u8 result_byte=0;struct afkv2_frame notification={.category=AFKV2_REPORT,.type=AFKV2_CLOSED,.interface_flags=1,.payload=&result_byte,.payload_size=1}; - assert(afkv2_closed_report(¬ification));notification.token=1;assert(!afkv2_closed_report(¬ification)); - notification.token=0;notification.payload_size=0;assert(!afkv2_closed_report(¬ification)); - assert(!prepare(0,0,name,class));assert(!prepare(65536,65536,name,class)); - assert(!prepare(9,1,name,class));assert(!prepare(9,9,"wrong",class));assert(!prepare(9,9,name,"wrong")); - struct apple_epic_service *pending=prepare(9,9,name,class);assert(pending); - disconnect();connect();publish(pending,9); - assert(!d.dcpavserv.service && pending->torndown && !pending->enabled && !pending->users); - pending=prepare(9,9,name,class);assert(pending); - mutex_lock(&d.hpd_mutex);d.typec_generation++;mutex_unlock(&d.hpd_mutex); - publish(pending,9); /* Generation change alone, without registry retirement. */ - assert(!d.dcpavserv.service && pending->torndown && !pending->enabled); - pending=prepare(13,13,name,class);assert(pending); - dpavservep_detach(&d);publish(pending,13); /* Registry retirement with no physical change. */ - assert(!d.dcpavserv.service && pending->torndown && !pending->enabled); - ep.wire_v2=false;ep.endpoint=0x28;d.hw.iomfb_method_profile=0; - assert(!prepare(7,7,name,class));pending=prepare(7,0,name,NULL);assert(pending);publish(pending,0); - assert(d.dcpavserv.service==pending && pending->users==1);dpavservep_detach(&d); - assert(!pending->users && !d.hpd_mutex && !d.dcpavserv.lock); - memset(&ep.services,0,sizeof(ep.services));ep.wire_v2=true;ep.endpoint=0x2a;d.hw.iomfb_method_profile=2; - repeated_announcements(); - puts("PASS: actual dynamic 1/5/9/13 binding, forged identities, prepare/init epochs, physical retirement, reader pins, real old CLOSED/late ACK isolation and legacy unit 0"); -} -''' -with tempfile.TemporaryDirectory(prefix='dpav-rebind-') as tmp: - p=Path(tmp)/'test.c';p.write_text(prefix+'\n'.join(parts)+suffix) - cc=shlex.split(os.environ.get('HOSTCC','clang')) - subprocess.run(cc+['-std=gnu11','-Wall','-Werror','-O1','-g','-fsanitize=address,undefined','-fno-omit-frame-pointer', - '-I'+str(root/'tools/testing/selftests/drivers/gpu/apple-afkv2/host-include'), - '-I'+str(base),str(p),'-o',str(Path(tmp)/'test')],check=True) - subprocess.run([str(Path(tmp)/'test')],check=True) diff --git a/tools/diagnostics/test-apple-dptx-readiness.py b/tools/diagnostics/test-apple-dptx-readiness.py deleted file mode 100644 index d86f1505f7cf3..0000000000000 --- a/tools/diagnostics/test-apple-dptx-readiness.py +++ /dev/null @@ -1,241 +0,0 @@ -#!/usr/bin/env python3 -# SPDX-License-Identifier: GPL-2.0-only -"""Exercise production DPTX publication, retries and terminal service failures.""" -import hashlib -import os -from pathlib import Path -import re -import shlex -import subprocess -import tempfile - -root = Path(__file__).resolve().parents[2] -base = root / 'drivers/gpu/drm/apple' -dcp = (base / 'dcp.c').read_text() -ep = (base / 'dptxep.c').read_text() -service = (base / 'dptx-service.h').read_text() - - -def function(source, name): - match = re.search(r'^static (?:inline )?(?:bool|int|void) ' + name + r'\([^;{]*\)\n\{', source, re.M) - assert match, name - start, opening = match.start(), match.end() - 1 - end, depth = opening + 1, 1 - while depth: - depth += (source[end] == '{') - (source[end] == '}') - end += 1 - return source[start:end] - - -body = '\n'.join(function(service, n) for n in ( - 'dptx_service_available', 'dptx_service_bind', 'dptx_service_publish', 'dptx_service_unbind')) -body += '\n' + '\n'.join(function(dcp, n) for n in ( - 'dcp_is_external_h17p', 'dcp_dptx_connect_raw', 'dcp_external_link_locked', - 'dcp_dptx_connect', 'dcp_typec_reconnect_work')) -body += '\n' + function(ep, 'dptxport_init') -print('Production readiness SHA256:', hashlib.sha256(body.encode()).hexdigest()) -prefix = r''' -#include -#include -#include -#include -#include -#include -#include -typedef uint32_t u32; -typedef int64_t s64; -#define READ_ONCE(x) (x) -#define WRITE_ONCE(x, v) ((x) = (v)) -#define smp_load_acquire(p) (*(p)) -#define smp_store_release(p, v) (*(p) = (v)) -#define DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL 2 -#define DPTX_CONNECT_TIMEOUT 2000 -#define DPTX_TUNNEL_CONNECT_TIMEOUT 8000 -#define DPTX_RECONNECT_DELAY 1000 -#define DPTX_RECONNECT_RETRIES 5 -#define DRM_MODE_CONNECTOR_DisplayPort 10 -#define dev_warn(...) ((void)0) -#define dev_info(...) ((void)0) -#define dev_dbg(...) ((void)0) -#define dev_err(...) ((void)0) -#define jiffies_to_msecs(x) (x) -#define usleep_range(a,b) ((void)(a), (void)(b)) -#define system_freezable_wq NULL -#define container_of(p,t,m) ((t *)((char *)(p) - offsetof(t,m))) -#define to_delayed_work(p) container_of(p, struct delayed_work, work) -struct mutex { bool held; }; -static void mutex_lock(struct mutex *m) { assert(!m->held); m->held = true; } -static void mutex_unlock(struct mutex *m) { assert(m->held); m->held = false; } -static struct mutex *acquire(struct mutex *m) { mutex_lock(m); return m; } -static void release(struct mutex **m) { if (*m) mutex_unlock(*m); } -#define guard(kind) struct mutex *g __attribute__((cleanup(release))) = acquire -#define lockdep_assert_held(m) assert((m)->held) -struct completion { bool done; }; -struct work_struct { int unused; }; -struct delayed_work { struct work_struct work; unsigned int scheduled, delay; }; -struct apple_dcp; -struct apple_dcp_afkep { struct apple_dcp *dcp; }; -struct apple_epic_service { void *cookie; struct apple_dcp_afkep *ep; }; -struct dptx_port { - struct apple_epic_service *service; - bool enabled, connected, closed; - s64 unit; - void *atcphy; - struct completion enable_completion, linkcfg_completion; -}; -struct apple_dcp { - struct { int iomfb_method_profile; } hw; - bool external_prepared, external_cable, quiescing, typec_cable_connected; - int external_error; - unsigned int typec_reconnect_tries; - struct mutex external_lock, hpd_mutex; - struct delayed_work typec_reconnect_wq; - void *phy, *dev, *active_typec_route, *avep; - unsigned int dptx_die, dptx_phy, dptx_dfp_port; - int connector_type; - struct dptx_port dptxport[1]; -}; -static unsigned int raw_attempts, firmware_calls, release_calls; -static int injected_error; -static unsigned int fail_stage; -static bool dcp_is_typec_output(struct apple_dcp *p) { (void)p; return true; } -static bool dcp_is_usb4_output(struct apple_dcp *p) { (void)p; return false; } -static bool apple_dp_tunnel_t602x(void) { return false; } -static void reinit_completion(struct completion *c) { c->done = false; } -static void complete(struct completion *c) { c->done = true; } -static unsigned long wait_for_completion_timeout(struct completion *c, unsigned long timeout) -{ (void)timeout; return c->done ? 1 : 0; } -static void mod_delayed_work(void *queue, struct delayed_work *w, unsigned int delay) -{ - struct apple_dcp *p = container_of(w, struct apple_dcp, typec_reconnect_wq); - (void)queue; - assert(!p->external_lock.held && !p->hpd_mutex.held); - w->scheduled++; - w->delay = delay; -} -static int stage(unsigned int step) -{ firmware_calls++; return fail_stage == step ? injected_error : 0; } -static int dptxport_validate_connection(struct apple_epic_service *s, u32 a, u32 b, u32 c) -{ (void)s; (void)a; (void)b; (void)c; raw_attempts++; return stage(1); } -static int dptxport_connect(struct apple_epic_service *s, u32 a, u32 b, u32 c, bool typec) -{ (void)s; (void)a; (void)b; (void)c; (void)typec; return stage(2); } -static int dptxport_request_display(struct apple_epic_service *s) -{ - struct dptx_port *p = s->cookie; - int ret = stage(3); - if (!ret) complete(&p->linkcfg_completion); - return ret; -} -static int dptxport_set_hpd(struct apple_epic_service *s, bool on) -{ (void)s; (void)on; return stage(4); } -static int dptxport_set_hpd_timeout(struct apple_epic_service *s, bool on, unsigned int timeout) -{ (void)timeout; return dptxport_set_hpd(s, on); } -static int dptxport_release_display(struct apple_epic_service *s) -{ (void)s; release_calls++; return 0; } -static void av_service_connect(struct apple_dcp *p) { (void)p; } -''' -suffix = r''' -static void initial(struct apple_dcp *p, struct apple_epic_service *s, struct apple_dcp_afkep *ep) -{ - *p = (struct apple_dcp){ - .hw.iomfb_method_profile = DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL, - .external_prepared = true, .external_cable = true, .typec_cable_connected = true, - .phy = p, - }; - *ep = (struct apple_dcp_afkep){ .dcp = p }; - *s = (struct apple_epic_service){ .ep = ep }; - assert(dptx_service_bind(s, &p->dptxport[0], 0) == 0); - raw_attempts = firmware_calls = release_calls = fail_stage = 0; - injected_error = 0; -} -static int link(struct apple_dcp *p) -{ - guard(mutex)(&p->external_lock); - return dcp_external_link_locked(p); -} -static void publish(struct apple_epic_service *s) -{ dptxport_init(s, "dcpdptx-port-epic", "AppleDCPDPTXRemotePort", 0); } -int main(void) -{ - struct apple_dcp p; - struct apple_epic_service service; - struct apple_dcp_afkep ep; - initial(&p, &service, &ep); - /* Reproduce the observed initial call before the first AFK publication. */ - assert(link(&p) == -EAGAIN && !p.external_error); - assert(!firmware_calls && !release_calls && !p.dptxport[0].connected); - /* Even publication later than the bounded retry window must restart work. */ - for (unsigned int i = 0; i < DPTX_RECONNECT_RETRIES; i++) - dcp_typec_reconnect_work(&p.typec_reconnect_wq.work); - assert(p.typec_reconnect_tries == DPTX_RECONNECT_RETRIES && !p.external_error); - unsigned int queued = p.typec_reconnect_wq.scheduled; - publish(&service); - assert(p.dptxport[0].enabled && p.dptxport[0].enable_completion.done); - assert(p.typec_reconnect_wq.scheduled == queued + 1 && !p.typec_reconnect_wq.delay); - assert(!firmware_calls); /* Publication only schedules; never calls firmware inline. */ - dcp_typec_reconnect_work(&p.typec_reconnect_wq.work); - assert(raw_attempts == 1 && p.dptxport[0].connected && !p.external_error); - assert(!p.typec_reconnect_tries); - - /* A reader between owner publication and enabled=true must also retry. */ - initial(&p, &service, &ep); - p.dptxport[0].service = &service; - assert(link(&p) == -EAGAIN && !p.external_error && !firmware_calls); - publish(&service); - assert(link(&p) == 0 && p.dptxport[0].connected); - - /* A closed published session must remain terminal, even before any power request. */ - dptx_service_unbind(&service); - assert(link(&p) == -ENODEV && p.external_error == -ENODEV); - assert(p.dptxport[0].service == &service); - assert(dptx_service_bind(&service, &p.dptxport[0], 0) == -EBUSY); - unsigned int attempts = raw_attempts; - assert(link(&p) == -ENODEV && raw_attempts == attempts); - - /* Neither ENODEV nor EAGAIN from a real operation can masquerade as unpublished. */ - for (unsigned int phase = 1; phase <= 4; phase++) { - for (unsigned int transient = 0; transient < 2; transient++) { - initial(&p, &service, &ep); - publish(&service); - fail_stage = phase; - injected_error = transient ? -EAGAIN : -ENODEV; - assert(link(&p) == injected_error && p.external_error == injected_error); - unsigned int calls = firmware_calls; - fail_stage = 0; - assert(link(&p) == injected_error && firmware_calls == calls); - } - } - /* Missing configuration is terminal; only the first unpublished service is retryable. */ - initial(&p, &service, &ep); - p.phy = NULL; - assert(link(&p) == -ENODEV && p.external_error == -ENODEV && !firmware_calls); - - /* Legacy raw callers keep ENODEV and do not acquire external retry semantics. */ - initial(&p, &service, &ep); - p.hw.iomfb_method_profile = 0; - assert(dcp_dptx_connect(&p, 0) == -ENODEV && !firmware_calls); - publish(&service); - assert(!p.typec_reconnect_wq.scheduled); - - /* Publication cannot revive a removed cable, failed session, or quiescing device. */ - for (unsigned int state = 0; state < 3; state++) { - initial(&p, &service, &ep); - if (state == 0) p.typec_cable_connected = false; - if (state == 1) p.external_error = -EIO; - if (state == 2) p.quiescing = true; - publish(&service); - assert(!p.typec_reconnect_wq.scheduled && !firmware_calls); - } - puts("PASS: delayed publication, exhausted retry recovery, terminal CLOSE/operation errors, async notification"); - return 0; -} -''' -with tempfile.TemporaryDirectory(prefix='apple-dptx-readiness-') as directory: - directory = Path(directory) - unit = directory / 'test.c' - unit.write_text(prefix + body + suffix) - command = shlex.split(os.environ.get('CC', 'clang')) - command += ['-std=gnu11', '-Wall', '-Wextra', '-Werror', '-Wno-unused-parameter', '-O1', '-g', - '-fsanitize=address,undefined', '-fno-omit-frame-pointer'] - subprocess.run(command + [str(unit), '-o', str(directory / 'test')], check=True) - subprocess.run([str(directory / 'test')], check=True) diff --git a/tools/diagnostics/test-apple-edid-rx-progress.py b/tools/diagnostics/test-apple-edid-rx-progress.py deleted file mode 100644 index 9f4d99ddf5683..0000000000000 --- a/tools/diagnostics/test-apple-edid-rx-progress.py +++ /dev/null @@ -1,203 +0,0 @@ -#!/usr/bin/env python3 -# SPDX-License-Identifier: GPL-2.0-only OR MIT -"""Prove DisplayAttributes cannot block a later AFK reply on ordered RTKit RX.""" -from pathlib import Path -import argparse -import os -import shlex -import subprocess -import tempfile - -root = Path(__file__).resolve().parents[2] -parser = argparse.ArgumentParser(description=__doc__) -parser.add_argument('--ref', help='public git revision for a negative baseline test') -args = parser.parse_args() - -def source(path): - if args.ref: - return subprocess.check_output(['git', '-C', str(root), 'show', args.ref+':'+path], text=True) - return (root/path).read_text() - -def function(text, signature): - start = text.index(signature) - opening = text.index('{', start) - while ';' in text[start:opening]: - start = text.index(signature, start+1) - opening = text.index('{', start) - end, depth = opening+1, 1 - while depth: - depth += (text[end]=='{')-(text[end]=='}') - end += 1 - return text[start:end] - -dcp=source('drivers/gpu/drm/apple/dcp.c') -iomfb=source('drivers/gpu/drm/apple/iomfb.c') -af=source('drivers/gpu/drm/apple/afk.c') -rtkit=source('drivers/soc/apple/rtkit.c') -template=source('drivers/gpu/drm/apple/iomfb_template.c') -assert 'alloc_ordered_workqueue("rtkit-%s"' in rtkit -assert '.recv_message = dcp_recv_msg' in dcp -assert 'dcp_set_dimensions(dcp);' in template[template.index('"DisplayAttributes"'):] -assert 'queue_work(ep->wq, &work->work)' in af -prefix=r''' -#undef NDEBUG -#include -#include -#include -#include -#include -#include -#include -#include -#include -typedef uint8_t u8; -typedef uint64_t u64; -#define READ_ONCE(x) (x) -#define WRITE_ONCE(x,v) ((x)=(v)) -#define GFP_KERNEL 0 -#define kzalloc(n,f) calloc(1,n) -#define kfree(p) free(p) -#define container_of(p,t,m) ((t *)((char *)(p)-offsetof(t,m))) -#define INIT_WORK(p,f) ((p)->fn=(f)) -#define dev_warn(...) ((void)0) -#define dev_warn_ratelimited(...) ((void)0) -#define dev_dbg_ratelimited(...) ((void)0) -#define trace_dcp_recv_msg(...) ((void)0) -#define DCP_FIRMWARE_H17P 2 -#define IOMFB_ENDPOINT 0x37 -#define AV_ENDPOINT 0x29 -#define SYSTEM_ENDPOINT 0x20 -#define DISP0_ENDPOINT 0x23 -#define DPTX_CONTROLLER_ENDPOINT 0x24 -#define APPLE_RTKIT_EP_MGMT 0 -#define APPLE_RTKIT_EP_CRASHLOG 1 -#define APPLE_RTKIT_EP_SYSLOG 2 -#define APPLE_RTKIT_EP_IOREPORT 4 -#define APPLE_RTKIT_EP_OSLOG 8 -#define APPLE_RTKIT_APP_ENDPOINT_START 0x20 -struct mutex { pthread_mutex_t native; }; -static _Thread_local struct mutex *locks[4]; -static _Thread_local unsigned int depth; -static bool held(struct mutex *m) { for(unsigned int i=0;inative)); locks[depth++]=m; } -static void mutex_unlock(struct mutex *m) { assert(depth && locks[depth-1]==m); depth--; assert(!pthread_mutex_unlock(&m->native)); } -#define lockdep_assert_held(p) assert(held(p)) -#define spin_lock_irqsave(p,f) do { mutex_lock(p); (f)=0; } while(0) -#define spin_unlock_irqrestore(p,f) do { (void)(f); mutex_unlock(p); } while(0) -struct work_struct { void (*fn)(struct work_struct *); }; -struct workqueue_struct { struct work_struct *pending; pthread_cond_t ready; pthread_mutex_t lock; }; -struct drm_device { struct { struct mutex mutex; } mode_config; }; -struct drm_connector { struct drm_device *dev; struct { int width_mm,height_mm; } display_info; }; -struct apple_connector { struct drm_connector base; struct work_struct hotplug_wq; }; -struct apple_dcp; -struct apple_dcp_afkep { struct apple_dcp *dcp; u8 endpoint; struct mutex lock; bool stopping; struct workqueue_struct *wq; }; -struct apple_dcp { - bool quiescing; int fw_compat; void *dev; - int width_mm,height_mm; struct { int width_mm,height_mm; } panel; - unsigned int nr_modes; struct { struct { int width_mm,height_mm; } mode; } modes[1]; - struct apple_connector *connector; - struct apple_dcp_afkep *dcpavservep,*dptxep,*avep,*systemep,*ibootep,*dptx_controller_ep; - struct { struct mutex lock; bool stopped; void *owner; } iomfb; -}; -struct afk_receive_message_work { struct apple_dcp_afkep *ep; u64 message; struct work_struct work; }; -struct apple_rtkit_ops { void (*recv_message)(void *,u8,u64); }; -struct apple_rtkit { bool shutting_down; const struct apple_rtkit_ops *ops; void *cookie,*dev; }; -struct apple_rtkit_rx_work { struct apple_rtkit *rtk; u8 ep; u64 msg; struct work_struct work; }; -#define current ((void *)1) -static pthread_mutex_t reply_lock=PTHREAD_MUTEX_INITIALIZER; -static pthread_cond_t reply_ready=PTHREAD_COND_INITIALIZER; -static bool reply_done; -static unsigned int advances, dimension_updates; -static struct workqueue_struct dimensions_queue={.ready=PTHREAD_COND_INITIALIZER,.lock=PTHREAD_MUTEX_INITIALIZER}; -static struct workqueue_struct afk_queue={.ready=PTHREAD_COND_INITIALIZER,.lock=PTHREAD_MUTEX_INITIALIZER}; -static void queue_work(struct workqueue_struct *q,struct work_struct *w) -{ assert(!pthread_mutex_lock(&q->lock)); assert(!q->pending); q->pending=w; pthread_cond_signal(&q->ready); assert(!pthread_mutex_unlock(&q->lock)); } -static void schedule_work(struct work_struct *w) { queue_work(&dimensions_queue,w); } -static void afk_receive_message_worker(struct work_struct *w) -{ - struct afk_receive_message_work *a=container_of(w,struct afk_receive_message_work,work); - assert(a->ep->endpoint==0x2a && a->message==7); - assert(!pthread_mutex_lock(&reply_lock)); reply_done=true; - pthread_cond_signal(&reply_ready); assert(!pthread_mutex_unlock(&reply_lock)); free(a); -} -static bool iomfb_uses_queue(struct apple_dcp *d) { (void)d; return true; } -static void iomfb_queue_advance(struct apple_dcp *d) { assert(held(&d->iomfb.lock)); advances++; } -static void dcp_set_dimensions(struct apple_dcp *d); -static void iomfb_recv_message(struct apple_dcp *d,u64 message) -{ assert(message==1 && held(&d->iomfb.lock)); dcp_set_dimensions(d); } -#define rx_unused(name) static void name(struct apple_rtkit *r,u64 m) { (void)r;(void)m; } -rx_unused(apple_rtkit_management_rx) -rx_unused(apple_rtkit_crashlog_rx) -rx_unused(apple_rtkit_syslog_rx) -rx_unused(apple_rtkit_ioreport_rx) -rx_unused(apple_rtkit_oslog_rx) -''' -parts=[function(dcp,'void dcp_set_dimensions(')] -if 'void dcp_update_connector_dimensions(' in dcp: - parts.append(function(dcp,'void dcp_update_connector_dimensions(')) -else: - parts.append('static void dcp_update_connector_dimensions(struct apple_dcp *d, struct drm_connector *c) { (void)d; (void)c; }') -parts += [function(iomfb,'void iomfb_recv_msg('),function(af,'int afk_receive_message('), - function(dcp,'static struct apple_dcp_afkep *dcp_afkep('), - function(dcp,'static void dcp_recv_msg('),function(rtkit,'static void apple_rtkit_rx_work(')] -suffix=r''' -static struct drm_device drm={.mode_config.mutex.native=PTHREAD_MUTEX_INITIALIZER}; -static struct apple_connector connector={.base.dev=&drm}; -static struct apple_dcp d={.connector=&connector,.width_mm=600,.height_mm=340,.nr_modes=1,.fw_compat=DCP_FIRMWARE_H17P,.iomfb.lock.native=PTHREAD_MUTEX_INITIALIZER}; -static struct apple_dcp_afkep av={.dcp=&d,.endpoint=0x2a,.lock.native=PTHREAD_MUTEX_INITIALIZER,.wq=&afk_queue}; -static void dimension_work(struct work_struct *w) -{ - (void)w; - mutex_lock(&drm.mode_config.mutex); - dcp_update_connector_dimensions(&d,&connector.base); dimension_updates++; - mutex_unlock(&drm.mode_config.mutex); -} -static void *worker(void *arg) -{ - struct workqueue_struct *q=arg; - assert(!pthread_mutex_lock(&q->lock)); - while(!q->pending) assert(!pthread_cond_wait(&q->ready,&q->lock)); - struct work_struct *w=q->pending; q->pending=NULL; - assert(!pthread_mutex_unlock(&q->lock)); w->fn(w); return NULL; -} -static const struct apple_rtkit_ops ops={.recv_message=dcp_recv_msg}; -static struct apple_rtkit rtk={.ops=&ops,.cookie=&d}; -static void *ordered_rx(void *arg) -{ - (void)arg; - /* One ordered RX worker must complete DisplayAttributes before AFK. */ - struct apple_rtkit_rx_work *a=calloc(1,sizeof(*a)); assert(a); - a->rtk=&rtk; a->ep=IOMFB_ENDPOINT; a->msg=1; apple_rtkit_rx_work(&a->work); - a=calloc(1,sizeof(*a)); assert(a); - a->rtk=&rtk; a->ep=0x2a; a->msg=7; apple_rtkit_rx_work(&a->work); - return NULL; -} -int main(void) -{ - pthread_t rx,afk,dimensions; - (void)schedule_work; /* The negative baseline does not defer dimensions. */ - d.dcpavservep=&av; connector.hotplug_wq.fn=dimension_work; - mutex_lock(&drm.mode_config.mutex); /* DRM get_modes holds this while waiting. */ - assert(!pthread_create(&afk,NULL,worker,&afk_queue)); - assert(!pthread_create(&dimensions,NULL,worker,&dimensions_queue)); - assert(!pthread_mutex_lock(&reply_lock)); - assert(!pthread_create(&rx,NULL,ordered_rx,NULL)); - struct timespec deadline; assert(!clock_gettime(CLOCK_REALTIME,&deadline)); deadline.tv_sec+=2; - int rc=0; - while(!reply_done && !rc) rc=pthread_cond_timedwait(&reply_ready,&reply_lock,&deadline); - if(!reply_done) { fputs("FAIL: ordered RTKit RX blocked before AFK reply while mode_config.mutex held\n",stderr); fflush(stderr); _Exit(1); } - assert(!pthread_mutex_unlock(&reply_lock)); - assert(!dimension_updates); /* Deferred worker cannot take the probe's mutex. */ - mutex_unlock(&drm.mode_config.mutex); - assert(!pthread_join(rx,NULL) && !pthread_join(afk,NULL) && !pthread_join(dimensions,NULL)); - assert(advances==1 && dimension_updates==1 && connector.base.display_info.width_mm==600 && connector.base.display_info.height_mm==340); - assert(d.modes[0].mode.width_mm==600 && d.modes[0].mode.height_mm==340); - puts("PASS: actual ordered RTKit RX -> synchronous IOMFB dimensions callback -> AFK enqueue makes reply progress while DRM mutex held; deferred dimensions update completes after release"); -} -''' -with tempfile.TemporaryDirectory(prefix='apple-edid-rx-') as tmp: - test=Path(tmp)/'test.c'; test.write_text(prefix+'\n'.join(parts)+suffix) - cc=shlex.split(os.environ.get('HOSTCC','clang')) - subprocess.run(cc+['-std=gnu11','-Wall','-Werror','-O1','-g','-pthread', - '-fsanitize=address,undefined','-fno-omit-frame-pointer',str(test),'-o',str(Path(tmp)/'test')],check=True) - subprocess.run([str(Path(tmp)/'test')],check=True) diff --git a/tools/diagnostics/test-apple-external-lifecycle.py b/tools/diagnostics/test-apple-external-lifecycle.py deleted file mode 100644 index 60d3d61deacd3..0000000000000 --- a/tools/diagnostics/test-apple-external-lifecycle.py +++ /dev/null @@ -1,598 +0,0 @@ -#!/usr/bin/env python3 -# SPDX-License-Identifier: GPL-2.0-only -"""Host-only interleavings through production external lifecycle and queue code.""" -import hashlib -import os -from pathlib import Path -import shlex -import subprocess -import tempfile - -root = Path(__file__).resolve().parents[2] -base = root / 'drivers/gpu/drm/apple' -dcp = (base / 'dcp.c').read_text() -iomfb = (base / 'iomfb.c').read_text() -h17p = (base / 'iomfb_h17p.c').read_text() -drm = (base / 'apple_drv.c').read_text() - - -def block(source, marker): - start = source.index(marker) - begin = source.index('{', start) - end, depth = begin + 1, 1 - while depth: - depth += (source[end] == '{') - (source[end] == '}') - end += 1 - return source[start:end] - - -def function(source, name): - import re - marker = re.search(r'^(?:static )?(?:bool|void|int) ' + name + r'\([^;{]*\)\n\{', source, re.M) - assert marker, name - return block(source, marker.group()[:-1]) - - -def external_branch(name, result): - body = function(dcp, name) - branch = block(body, 'if (dcp_is_external_h17p(dcp)) {') - return body[:body.index('if (dcp_is_external_h17p(dcp)) {')] + branch + result + '\n}' - - -parts = [function(iomfb, 'dcp_handle_hotplug_actions')] -for name in ('iomfb_channels_idle', 'iomfb_pause_presentation', 'iomfb_queue_settle', 'iomfb_queue'): - parts.append(function(iomfb, name)) -parts.append(block(h17p, 'struct dcp_external_power_cookie {') + ';') -for name in ('iomfb_external_power_complete', 'iomfb_external_reply_size', - 'iomfb_external_parameter_complete', 'iomfb_external_device_complete'): - parts.append(function(h17p, name)) -parts.append(function(h17p, 'iomfb_external_power_h17p')) -for name in ('dcp_is_external_h17p', 'dcp_external_report_disconnected', - 'dcp_external_publish_generation', 'dcp_external_link_locked', - 'dcp_external_start_locked', 'dcp_external_stop_locked', - 'dcp_external_disconnect', 'dcp_external_stop_work', 'dcp_external_hotplug'): - parts.append(function(dcp, name)) -parts += [external_branch('dcp_dptx_connect_oob', '\nreturn -ENOSYS;'), - external_branch('dcp_poweron', ''), external_branch('dcp_poweroff', ''), - function(dcp, 'dcp_external_recover')] -# Include real admission call sites; only their allocation/start/release dependencies are mocked. -parts += [function(iomfb, 'iomfb_queue_atomic'), function(iomfb, 'iomfb_backlight_kick'), - function(iomfb, 'iomfb_queue_crc_h17p')] -parts += [function(iomfb, 'dcp_crtc_atomic_modeset')] -parts += [function(drm, 'apple_crtc_atomic_enable'), function(drm, 'apple_crtc_atomic_disable')] -route_admission = function(dcp, 'dcp_typec_route_deactivate') -route_admission = route_admission[:route_admission.index('\n\t/*')] + '\n\treturn 0;\n}' -route_admission = route_admission.replace('dcp_typec_route_deactivate(', 'test_route_deactivate_admission(') -parts.append(route_admission) -production = '\n'.join(parts) -print('Production lifecycle SHA256:', hashlib.sha256(production.encode()).hexdigest()) -prefix = r''' -#include -#include -#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 msecs_to_jiffies(x) (x) -#define time_after_eq(a, b) ((a) >= (b)) -#define DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL 2 -#define dev_err(...) ((void)0) -#define dev_info(...) ((void)0) -#define WARN_ONCE(...) ((void)0) -enum { DCP_FIRMWARE_V_12_3, DCP_FIRMWARE_V_13_5, DCP_FIRMWARE_H17P }; -#define IOMFBPARAM_ADAPTIVE_SYNC 1 -struct dcp_set_parameter_dcp { u32 param, value[8], count; }; -struct mutex { bool held; }; -static void mutex_lock(struct mutex *m) { assert(!m->held); m->held = true; } -static void mutex_unlock(struct mutex *m) { assert(m->held); m->held = false; } -static struct mutex *acquire(struct mutex *m) { mutex_lock(m); return m; } -static void release(struct mutex **m) { if (*m) mutex_unlock(*m); } -static struct mutex *once(struct mutex *m) { mutex_unlock(m); return NULL; } -#define JOIN2(a,b) a##b -#define JOIN(a,b) JOIN2(a,b) -#define guard(kind) struct mutex *JOIN(guard_, __COUNTER__) __attribute__((cleanup(release))) = acquire -#define scoped_guard(kind, value) for (struct mutex *g __attribute__((cleanup(release))) = acquire(value); g; g = once(g)) -#define lockdep_assert_held(m) assert((m)->held) -#define container_of(p, type, member) ((type *)((char *)(p) - offsetof(type, member))) -struct list_head { unsigned int count; }; -static bool list_empty(struct list_head *h) { return !h->count; } -static void list_add_tail(struct list_head *n, struct list_head *h) { (void)n; h->count++; } -#define list_add list_add_tail -struct work_struct { unsigned int scheduled; }; -struct delayed_work { unsigned int cancelled; }; -struct completion { bool done; }; -struct kref { unsigned int count; }; -struct dcp_wait_cookie { struct kref refcount; struct completion done; u32 status; }; -struct dcp_packet_header { char tag[4]; u32 in_len, out_len; }; -struct dcp_set_power_state_req_h17p { u64 unklong; u32 unkint; unsigned char null, padding[3]; } __attribute__((packed)); -struct dcp_set_power_state_resp { u32 unknown, ret; } __attribute__((packed)); -struct device { int unused; }; -struct apple_connector { bool connected, edid_invalidation_pending; struct work_struct hotplug_wq; }; -struct dptx_port { bool connected; void *service; }; -struct iomfb_transaction { struct list_head link; void (*start)(void *,void *), (*release)(void *); bool brightness_only, presentation; }; -struct platform_device; -struct drm_crtc_state { bool active, active_changed, mode_changed, color_mgmt_changed; void *event; struct { int hdisplay, vdisplay; } mode; }; -struct drm_device { int event_lock; }; -struct drm_crtc { struct drm_crtc_state *state; struct drm_device *dev; }; -struct drm_atomic_state { struct drm_crtc_state *next; }; -struct apple_crtc { struct drm_crtc base; struct platform_device *dcp; }; -#define to_apple_crtc(p) container_of(p, struct apple_crtc, base) -static struct drm_crtc_state *drm_atomic_get_new_crtc_state(struct drm_atomic_state *s, struct drm_crtc *c) { (void)c; return s->next; } -static bool drm_atomic_crtc_needs_modeset(struct drm_crtc_state *s) { return s->mode_changed; } -static unsigned int modesets, vblanks; -static int dcp_crtc_atomic_modeset(struct drm_crtc *, struct drm_atomic_state *); -#define spin_lock_irq(lock) ((void)(lock)) -#define spin_unlock_irq(lock) ((void)(lock)) -static void drm_crtc_send_vblank_event(struct drm_crtc *c, void *e) { (void)c; assert(e); vblanks++; } -struct apple_dcp; -struct iomfb_atomic_transaction { struct iomfb_transaction transaction; struct drm_crtc *crtc; struct drm_atomic_state *state; struct apple_dcp *dcp; }; -struct iomfb_crc_transaction { struct iomfb_transaction transaction; u32 swap_id; }; -struct apple_dcp { - struct device *dev; - struct dcp_metadata_epoch metadata_epoch; - struct { int iomfb_method_profile; } hw; - struct mutex external_lock, hpd_mutex; - u64 external_generation, external_crtc_generation, typec_generation, external_rx_generation, external_stop_generation, external_power_reply; - bool external_power_reply_valid, external_power_attempted, quiescing; - int fw_compat; - bool external_prepared, external_cable, external_running, external_stopped; - bool external_hotplug_pending, external_hotplug_connected; - int external_error; - bool crashed, pipe_enabled_h17p, typec_crtc_off, typec_cable_connected, placeholder_retried, crc_enabled; - struct delayed_work typec_reconnect_wq, placeholder_edid_wq; - struct work_struct external_stop_work, vblank_wq; - struct apple_connector *connector, *typec_connector; - struct dptx_port dptxport[1]; - void *avep; - struct { bool pending; } present_state_h17p; - struct { unsigned int depth; } ch_cmd, ch_cb, ch_oobcmd, ch_oobcb, ch_async, ch_oobasync; - struct { - struct mutex lock; - struct list_head pending; - struct iomfb_transaction *active; - unsigned int queued; - bool stopped, presentation_paused, backlight_queued; - struct work_struct work; - } iomfb; - struct { bool valid; } mode_state; - unsigned int retained_dma, retained_fb; -}; -struct platform_device { struct apple_dcp *data; }; -struct mux_control { int unused; }; -struct apple_dcp_typec_route { struct apple_dcp *dcp; struct mux_control *active_xbar; bool tunnel; }; -#define platform_get_drvdata(p) ((p)->data) -static void dcp_firmware_metadata_connection(struct apple_dcp *p, u64 generation, bool connected) -{ dcp_metadata_epoch_connection(&p->metadata_epoch,generation,connected); } -static struct apple_dcp *current_dcp; -static unsigned long jiffies; -static unsigned int sleeps, power0, power1, drops, releases, connects, allocations; -static int connect_error, hpd_error, release_error, wire_error; -static bool drain_callback, timeout_reply, absent_reply; -static int prep_error; -static bool on_zero_reply, off_one_reply; -static u32 wire_in, wire_out; -static char events[256]; -static unsigned int event_count; -static void event(char c) { assert(event_count < sizeof(events) - 1); events[event_count++] = c; } -static bool dcp_external_hotplug(struct apple_dcp *, bool); -static void usleep_range(unsigned int a, unsigned int b) -{ - (void)a; (void)b; - assert(!current_dcp->iomfb.lock.held); - jiffies += 2; sleeps++; - if (drain_callback) { - mutex_lock(¤t_dcp->iomfb.lock); - assert(dcp_external_hotplug(current_dcp, false)); - current_dcp->iomfb.active = NULL; - current_dcp->iomfb.pending.count = current_dcp->iomfb.queued = 0; - current_dcp->present_state_h17p.pending = false; - current_dcp->ch_cb.depth = 0; - mutex_unlock(¤t_dcp->iomfb.lock); - drain_callback = false; - } -} -static unsigned int dcp_mode_hotplug(void *state, bool connected, bool *result) -{ (void)state; if (result) *result = connected; return 1; } -#define DCP_HOTPLUG_NOTIFY 1 -#define DCP_HOTPLUG_VBLANK 2 -static void dcp_handle_hotplug_actions(struct apple_dcp *, unsigned int); -static void schedule_work(struct work_struct *); -static void schedule_work(struct work_struct *w) -{ - w->scheduled++; - if (current_dcp && w == ¤t_dcp->connector->hotplug_wq) event('N'); -} -static void cancel_delayed_work_sync(struct delayed_work *w) -{ assert(!current_dcp->external_lock.held); w->cancelled++; } -static void cancel_work_sync(struct work_struct *w) -{ assert(!current_dcp->external_lock.held); w->scheduled = 0; } -static bool dptx_service_available(struct dptx_port *p) { return p->service != NULL; } -static int dptxport_set_hpd(void *s, bool hpd) -{ (void)s; assert(!hpd); event('H'); drops++; return hpd_error; } -static int dptxport_release_display(void *s) -{ (void)s; event('R'); releases++; return release_error; } - -static unsigned int av_retirements; -static void dpavservep_detach(struct apple_dcp *p) { assert(!p->hpd_mutex.held);av_retirements++; } -static void av_service_disconnect(struct apple_dcp *p) { (void)p; } -static int dcp_dptx_connect_raw(struct apple_dcp *p, u32 port, bool *waiting_service) -{ - if (waiting_service) *waiting_service = false; - assert(!port && p->external_lock.held); - if (p->dptxport[0].connected) return 0; - event('C'); connects++; - if (connect_error) return connect_error; - p->dptxport[0].connected = true; - mutex_lock(&p->iomfb.lock); - assert(dcp_external_hotplug(p, true)); - mutex_unlock(&p->iomfb.lock); - return 0; -} -static void kref_init(struct kref *r) { r->count = 1; } -static void kref_get(struct kref *r) { r->count++; } -static void kref_put(struct kref *r, void (*fn)(struct kref *)) { assert(r->count); if (!--r->count) fn(r); } -static void release_wait_cookie(struct kref *r) { allocations--; free(container_of(r, struct dcp_wait_cookie, refcount)); } -static void *allocate(size_t size) { allocations++; return calloc(1, size); } -#define kzalloc_obj(obj) allocate(sizeof(obj)) -static void kfree(void *p) { if (p) { allocations--; free(p); } } -static void init_completion(struct completion *c) { c->done = false; } -static void complete(struct completion *c) { c->done = true; } -static unsigned long wait_for_completion_timeout(struct completion *c, unsigned long timeout) -{ (void)timeout; assert(!current_dcp->iomfb.lock.held); return c->done; } -static u64 get_unaligned_le64(const void *p) { u64 n; memcpy(&n, p, sizeof(n)); return n; } -static struct dcp_wait_cookie *late_cookie; -static void iomfb_external_power_complete(struct apple_dcp *, void *, void *); -static void dcp_set_power_state(struct apple_dcp *p, bool oob, - struct dcp_set_power_state_req_h17p *req, - void (*cb)(struct apple_dcp *, void *, void *), void *cookie) -{ - struct { struct dcp_packet_header header; struct dcp_set_power_state_req_h17p req; struct dcp_set_power_state_resp resp; } packet = {0}; - assert(oob && p->iomfb.presentation_paused && !p->iomfb.lock.held); - assert(req->unklong <= 1 && !req->unkint && !req->null); - event(req->unklong ? '1' : '0'); - if (req->unklong) power1++; else power0++; - packet.header.in_len = wire_in; - packet.header.out_len = wire_out; - packet.resp.unknown = ((req->unklong && !on_zero_reply) || (!req->unklong && off_one_reply)) ? 1 : 0; - packet.resp.ret = wire_error; - if (timeout_reply) { late_cookie = cookie; return; } - mutex_lock(&p->iomfb.lock); - assert(dcp_external_hotplug(p, req->unklong)); - cb(p, absent_reply ? NULL : &packet.resp, cookie); - mutex_unlock(&p->iomfb.lock); -} -static void iomfb_opaque_x_reset_h17p(struct apple_dcp *p) { (void)p; } -static void dcp_set_display_device(struct apple_dcp *p, bool oob, u32 *handle, - void (*cb)(struct apple_dcp *, void *, void *), void *cookie) -{ - struct { struct dcp_packet_header h; u32 input, output; } packet = {0}; - assert(!oob && *handle == 2); event('D'); - packet.h.in_len = 4; packet.h.out_len = prep_error == 1 ? 0 : 4; - cb(p, &packet.output, cookie); -} -static void dcp_set_parameter_dcp(struct apple_dcp *p, bool oob, struct dcp_set_parameter_dcp *param, - void (*cb)(struct apple_dcp *, void *, void *), void *cookie) -{ - struct { struct dcp_packet_header h; struct dcp_set_parameter_dcp input; u32 output; } packet = {0}; - assert(!oob && param->param == IOMFBPARAM_ADAPTIVE_SYNC && param->count == 3); event('A'); - for (unsigned int i = 0; i < 8; i++) assert(!param->value[i]); - packet.h.in_len = 40; packet.h.out_len = prep_error == 2 ? 0 : 4; - cb(p, &packet.output, cookie); -} -static void dcp_mode_begin(void *state) { (void)state; } -static unsigned int dcp_mode_finish(void *state, bool success, bool *connected) -{ (void)state; (void)success; (void)connected; return 0; } -static int iomfb_modeset_h17p(struct apple_dcp *p, struct drm_crtc_state *s) -{ (void)s; assert(p->external_lock.held && p->external_running); modesets++; return 0; } -#define iomfb_modeset_v12_3 iomfb_modeset_h17p -#define iomfb_modeset_v13_3 iomfb_modeset_h17p -static void iomfb_atomic_start(void *a, void *b) { (void)a; (void)b; } -static void iomfb_atomic_release(void *p) { kfree(p); } -static struct drm_atomic_state *drm_atomic_state_get(struct drm_atomic_state *s) { return s; } -static void iomfb_backlight_start(void *a, void *b) { (void)a; (void)b; } -static void iomfb_backlight_release(void *p) { kfree(p); } -static void iomfb_crc_start(void *a, void *b) { (void)a; (void)b; } -static void iomfb_crc_release(void *p) { kfree(p); } -''' -suffix = r''' -static struct apple_connector connector; -static void initial(struct apple_dcp *p) -{ - assert(!allocations); - memset(p, 0, sizeof(*p)); - memset(&connector, 0, sizeof(connector)); connector.connected = true; - p->connector = &connector; - p->external_prepared = p->external_cable = p->external_running = p->external_power_attempted = true; - p->fw_compat = DCP_FIRMWARE_H17P; - p->external_generation = p->external_crtc_generation = p->external_rx_generation = 1; - p->typec_generation=1; dcp_metadata_epoch_connection(&p->metadata_epoch,1,true); - p->hw.iomfb_method_profile = DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL; - p->dptxport[0] = (struct dptx_port){ .connected = true, .service = p }; - p->retained_dma = 7; p->retained_fb = 11; - current_dcp = p; - jiffies = sleeps = power0 = power1 = drops = releases = connects = event_count = 0; - connect_error = hpd_error = release_error = wire_error = 0; - drain_callback = timeout_reply = absent_reply = false; - wire_in = 16; wire_out = 8; - late_cookie = NULL; prep_error = 0; on_zero_reply = off_one_reply = false; - memset(events, 0, sizeof(events)); -} -static int stop(struct apple_dcp *p, u64 generation) -{ - guard(mutex)(&p->external_lock); - return dcp_external_stop_locked(p, generation); -} -static void owned(struct apple_dcp *p) -{ assert(p->retained_dma == 7 && p->retained_fb == 11 && !allocations); } -int main(void) -{ - struct apple_dcp p; - struct platform_device dev = { &p }; - initial(&p); - assert(stop(&p, 1) == 0); - assert(!strcmp(events, "0HR") && power0 == 1 && releases == 1); - assert(p.iomfb.presentation_paused && p.external_stopped && !p.crashed); - assert(stop(&p, 1) == 0 && power0 == 1 && releases == 1); - owned(&p); - - /* A new physical cable epoch publishes HPD before the CRTC powers on. */ - initial(&p); - assert(dcp_external_disconnect(&p, true) == 0); - assert(!connector.connected && p.external_stopped && power0 == 1); - assert(dcp_dptx_connect_oob(&dev, 0) == 0); - assert(connector.connected && p.iomfb.presentation_paused && !power1); - assert(p.external_power_attempted && p.external_stopped); - dcp_external_recover(&dev); - assert(p.external_running && !p.iomfb.presentation_paused && power1 == 1); - assert(p.external_crtc_generation == p.external_rx_generation); - owned(&p); - - /* Replug/unplug before CRTC activation releases the new link without a second OFF. */ - initial(&p); - assert(dcp_external_disconnect(&p, true) == 0); - assert(dcp_dptx_connect_oob(&dev, 0) == 0 && releases == 1); - assert(dcp_external_disconnect(&p, true) == 0); - assert(releases == 2 && power0 == 1 && !power1); - assert(!p.dptxport[0].connected && !connector.connected); - owned(&p); - - /* Same-cable DPMS OFF must not publish a synthetic wake. */ - initial(&p); assert(stop(&p, 1) == 0); connector.connected = false; - assert(dcp_dptx_connect_oob(&dev, 0) == 0); - assert(!connector.connected && p.iomfb.presentation_paused && !power1); - owned(&p); - - initial(&p); assert(stop(&p, 1) == 0); - /* Atomic, backlight and CRC are rejected; control transactions remain admissible. */ - iomfb_queue_atomic(&p, NULL, NULL); - iomfb_backlight_kick(&p); - p.crc_enabled = true; - mutex_lock(&p.iomfb.lock); iomfb_queue_crc_h17p(&p, 1); mutex_unlock(&p.iomfb.lock); - assert(!p.crashed && !p.iomfb.queued && !allocations); - struct iomfb_transaction presentation = { .presentation = true }; - struct iomfb_transaction control = {0}; - assert(iomfb_queue(&p, &presentation) == -EAGAIN); - assert(iomfb_queue(&p, &control) == 0); - p.iomfb.queued = p.iomfb.pending.count = 0; - - /* A callback must be able to acquire iomfb.lock and complete a drain. */ - initial(&p); - p.iomfb.active = &presentation; p.iomfb.pending.count = p.iomfb.queued = 1; - p.present_state_h17p.pending = true; p.ch_cb.depth = 1; drain_callback = true; - assert(stop(&p, 1) == 0 && sleeps && power0 == 1 && !p.crashed); - owned(&p); - - /* Every channel, queued-count, active, pending and present state blocks strict drain. */ - for (unsigned int field = 0; field < 10; field++) { - initial(&p); - if (field == 0) p.ch_cmd.depth = 1; - if (field == 1) p.ch_cb.depth = 1; - if (field == 2) p.ch_oobcmd.depth = 1; - if (field == 3) p.ch_oobcb.depth = 1; - if (field == 4) p.ch_async.depth = 1; - if (field == 5) p.ch_oobasync.depth = 1; - if (field == 6) p.iomfb.active = &presentation; - if (field == 7) p.iomfb.pending.count = 1; - if (field == 8) p.present_state_h17p.pending = true; - if (field == 9) p.iomfb.queued = 1; - assert(stop(&p, 1) == -ETIMEDOUT && !power0 && !drops && !releases); - assert(p.iomfb.presentation_paused && p.external_error == -ETIMEDOUT); - assert(stop(&p, 1) == -ETIMEDOUT && !power0); - dcp_external_recover(&dev); - assert(!power1 && !p.crashed); - owned(&p); - } - for (unsigned int fault = 0; fault < 7; fault++) { - initial(&p); - if (fault == 0) p.crashed = true; - if (fault == 1) p.iomfb.stopped = true; - if (fault == 2) wire_out = 12; - if (fault == 3) wire_in = 12; - if (fault == 4) { wire_error = 5; } - if (fault == 5) hpd_error = -EIO; - if (fault == 6) release_error = -EIO; - assert(stop(&p, 1) < 0 && p.external_error && p.iomfb.presentation_paused); - assert(p.dptxport[0].connected && !p.external_stopped); - assert(power0 == (fault > 1)); - assert(releases == (fault == 6)); - owned(&p); - } - initial(&p); timeout_reply = true; - assert(stop(&p, 1) == -ETIMEDOUT && late_cookie && allocations == 1); - assert(!drops && !releases && p.dptxport[0].connected); - iomfb_external_power_complete(&p, NULL, late_cookie); /* late callback owns its reference */ - owned(&p); - - /* Physical removal + new cable before old CRTC disable: stale token cannot stop new link. */ - initial(&p); - assert(dcp_external_disconnect(&p, true) == 0); - assert(!p.external_cable && !connector.connected && p.external_generation == 2); - assert(p.metadata_epoch.invalidated && !p.metadata_epoch.connected && connector.edid_invalidation_pending); - assert(dcp_dptx_connect_oob(&dev, 0) == 0); - assert(p.external_generation == 3 && !p.external_running && connector.connected); - assert(p.metadata_epoch.connected && p.metadata_epoch.invalidated && p.metadata_epoch.generation==3); - assert(dcp_dptx_connect_oob(&dev,0)==0 && p.typec_generation==3); - assert(p.iomfb.presentation_paused && power1 == 0); - dcp_poweroff(&dev); - assert(power0 == 1 && !p.external_running); /* token still belongs to generation 1 */ - /* A stale disable must still deliver host DRM event bookkeeping. */ - struct drm_crtc_state inactive = { .active_changed = true, .event = &p }; - struct drm_device drm_dev = {0}; - struct apple_crtc crtc = { .dcp = &dev, .base = { .state = &inactive, .dev = &drm_dev } }; - struct drm_atomic_state atomic = { .next = &inactive }; - apple_crtc_atomic_disable(&crtc.base, &atomic); - assert(power0 == 1 && !inactive.event && vblanks == 1); - /* Active -> active recovery invokes the real CRTC hook and refreshes the token. */ - struct drm_crtc_state active = { .active = true, .mode_changed = true, .mode = { 1920, 1080 } }; - atomic.next = &active; - apple_crtc_atomic_enable(&crtc.base, &atomic); - assert(modesets == 1); - assert(p.external_crtc_generation == 3 && power1 == 1); - dcp_poweroff(&dev); - assert(power0 == 2 && p.external_stopped); - dcp_poweroff(&dev); assert(power0 == 2); - owned(&p); - - /* Uncertain physical stop reports cable loss but keeps the resource and blocks restart. */ - initial(&p); wire_error = 5; - assert(dcp_external_disconnect(&p, true) == -EREMOTEIO); - assert(!connector.connected && p.dptxport[0].connected && !releases); - assert(dcp_dptx_connect_oob(&dev, 0) == -EREMOTEIO && !power1); - assert(p.external_crtc_generation == 1 && p.iomfb.presentation_paused); - owned(&p); - /* A deferred old RX disconnect cannot stop a later connection epoch. */ - initial(&p); - mutex_lock(&p.iomfb.lock); - assert(dcp_external_hotplug(&p, false)); - mutex_unlock(&p.iomfb.lock); - assert(p.external_stop_generation == 1); - p.external_generation = p.external_rx_generation = p.external_crtc_generation = 3; - dcp_external_stop_work(&p.external_stop_work); - assert(!power0 && !drops && p.external_running); - owned(&p); - - /* Before initialization there is no firmware/shared-memory transport to touch. */ - initial(&p); - p.external_prepared = p.external_power_attempted = p.external_running = false; - p.dptxport[0].connected = false; - assert(stop(&p, 1) == 0 && !power0 && !power1 && !releases); - assert(dcp_dptx_connect_oob(&dev, 0) == -EAGAIN && !connects); - owned(&p); - - /* Initial physical association precedes service discovery; duplicate HPD keeps its ticket. */ - initial(&p); p.external_prepared=false; p.external_cable=false; - p.external_power_attempted=p.external_running=false; - p.typec_generation=0; memset(&p.metadata_epoch,0,sizeof(p.metadata_epoch)); - assert(dcp_dptx_connect_oob(&dev,0)==-EAGAIN); - assert(p.metadata_epoch.connected && !p.metadata_epoch.invalidated && p.metadata_epoch.generation==1); - struct dcp_metadata_ticket first=dcp_metadata_begin(&p.metadata_epoch); - assert(dcp_dptx_connect_oob(&dev,0)==-EAGAIN && p.typec_generation==1); - assert(dcp_metadata_matches(&p.metadata_epoch,first)); - - /* Discovery publishes physical HPD before DRM activation, without unpausing the pipe. */ - initial(&p); - p.external_power_attempted = p.external_running = false; - p.iomfb.presentation_paused = true; - p.dptxport[0].connected = false; connector.connected=false; - assert(dcp_dptx_connect_oob(&dev, 0) == 0 && connects == 1 && !power1); - assert(connector.connected && p.iomfb.presentation_paused); - dcp_external_recover(&dev); - assert(strstr(events, "DA1") && p.external_running && power1 == 1); - owned(&p); - - /* Discovery cannot publish a missing cable, a sticky error, or an OFF/restart callback. */ - for(unsigned int reason=0;reason<3;reason++) { - initial(&p); connector.connected=false; p.iomfb.presentation_paused=true; - p.external_power_attempted=false; - if(reason==0)p.external_cable=false; - if(reason==1)p.external_error=-EIO; - if(reason==2)p.external_power_attempted=true; - mutex_lock(&p.iomfb.lock); assert(dcp_external_hotplug(&p,true)); mutex_unlock(&p.iomfb.lock); - assert(!connector.connected && p.iomfb.presentation_paused && !power1); - } - - /* Exact ON requires the 64-bit reply 1; all-zero must not activate modesets. */ - initial(&p); - p.external_running = false; p.external_stopped = true; - p.iomfb.presentation_paused = true; on_zero_reply = true; - unsigned int previous_modesets = modesets; - dcp_external_recover(&dev); - assert(p.external_error == -EREMOTEIO && !p.external_running); - assert(p.external_power_reply_valid && p.external_power_reply == 0); - assert(dcp_crtc_atomic_modeset(&crtc.base, &atomic) == -EPIPE); - apple_crtc_atomic_enable(&crtc.base, &atomic); - assert(modesets == previous_modesets && power1 == 1); - owned(&p); - - /* Production accepts only an exact echoed OFF state with zero status. */ - initial(&p); p.iomfb.presentation_paused = true; - assert(iomfb_external_power_h17p(&p, false) == 0); - assert(p.external_power_reply_valid && p.external_power_reply == 0); - owned(&p); - initial(&p); p.iomfb.presentation_paused = true; off_one_reply = true; - assert(iomfb_external_power_h17p(&p, false) == -EREMOTEIO); - assert(p.external_power_reply_valid && p.external_power_reply == 1); - owned(&p); - initial(&p); p.iomfb.presentation_paused = true; wire_error = 0x1234; - assert(iomfb_external_power_h17p(&p, false) == -EREMOTEIO); - assert(p.external_power_reply_valid && p.external_power_reply == (0x1234ULL << 32)); - assert(!drops && !releases); - owned(&p); - /* Notification delivery is deferred: a client can reenter the lifecycle lock. */ - initial(&p); - assert(dcp_external_disconnect(&p, true) == 0); - assert(p.connector->hotplug_wq.scheduled && !p.external_lock.held); - mutex_lock(&p.external_lock); mutex_unlock(&p.external_lock); - - /* Fabric cleanup cannot release a failed or still-running external route. */ - initial(&p); - struct apple_dcp_typec_route route = { .dcp = &p }; - assert(test_route_deactivate_admission(&route) == -EBUSY); - p.external_error = -EOPNOTSUPP; - assert(test_route_deactivate_admission(&route) == -EOPNOTSUPP); - p.external_error = 0; p.external_stopped = true; - assert(test_route_deactivate_admission(&route) == 0); - owned(&p); - - /* Missing replies and malformed first-start preparation fail closed. */ - initial(&p); p.iomfb.presentation_paused = true; absent_reply = true; - assert(iomfb_external_power_h17p(&p, false) == -EPROTO && !p.external_power_reply_valid); - owned(&p); - for (int stage = 1; stage <= 2; stage++) { - initial(&p); p.external_running = false; p.iomfb.presentation_paused = true; - prep_error = stage; - dcp_external_recover(&dev); - assert(p.external_error == -EPROTO && !power1 && p.iomfb.presentation_paused); - owned(&p); - } - /* AV retirement is physical/fallback disconnect, not ordinary DPMS. */ - initial(&p); unsigned int av_before=av_retirements; - assert(dcp_external_disconnect(&p, false)==0 && av_retirements==av_before); - initial(&p); p.external_error=-EIO; av_before=av_retirements; - assert(dcp_external_disconnect(&p, true)==-EIO && av_retirements==av_before+1); - initial(&p); p.external_stop_generation=p.external_generation-1; av_before=av_retirements; - dcp_external_stop_work(&p.external_stop_work); assert(av_retirements==av_before); - p.external_stop_generation=p.external_generation; - dcp_external_stop_work(&p.external_stop_work); assert(av_retirements==av_before+1); - puts("PASS: strict drain, callbacks, admission, exact RPC, stop/restart, stale epochs, modeset gate, retained ownership"); - return 0; -} -''' -with tempfile.TemporaryDirectory(prefix='apple-external-lifecycle-') as tmp: - tmp = Path(tmp) - unit = tmp / 'test.c' - epoch = (base / 'dcp-metadata-epoch.h').read_text().replace('#include ', '') - prefix = prefix.replace('typedef uint64_t u64;', 'typedef uint64_t u64;\n' + epoch) - unit.write_text(prefix + production + suffix) - command = shlex.split(os.environ.get('CC', 'clang')) - command += ['-std=gnu11', '-Wall', '-Wextra', '-Werror', - '-Wno-unused-variable', '-Wno-unused-parameter', '-O1', '-g', - '-fsanitize=address,undefined', '-fno-omit-frame-pointer'] - subprocess.run(command + [str(unit), '-o', str(tmp / 'test')], check=True) - subprocess.run([str(tmp / 'test')], check=True) diff --git a/tools/diagnostics/test-apple-external-sdr.py b/tools/diagnostics/test-apple-external-sdr.py deleted file mode 100644 index 695a6059da419..0000000000000 --- a/tools/diagnostics/test-apple-external-sdr.py +++ /dev/null @@ -1,253 +0,0 @@ -#!/usr/bin/env python3 -# SPDX-License-Identifier: GPL-2.0-only -"""Parse real-format TimingElements blobs with the production SDR selection code.""" -import hashlib -import os -from pathlib import Path -import re -import shlex -import struct -import subprocess -import tempfile - -root = Path(__file__).resolve().parents[2] -base = root / 'drivers/gpu/drm/apple' -source = (base / 'parser.c').read_text() -header = (base / 'parser.h').read_text() -body = source[source.index('#define DCP_PARSE_HEADER'):source.index('int parse_display_attributes(')] -header = header[header.index('struct apple_dcp;'):header.index('int parse(')] -# Pin the integration scope: the template passes its explicit external profile, -# independent of the existing internal-panel boolean. -template = (base / 'iomfb_template.c').read_text() -call = re.search(r'enumerate_modes\(&ctx,.*?\);', template, re.S).group() -assert re.search(r'dcp->hw\.iomfb_method_profile\s*==\s*DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL\)', call) -print('Production mode parser SHA256:', hashlib.sha256((header + body).encode()).hexdigest()) - - -class Pairs(list): - """A wire dictionary which preserves duplicate keys for rejection tests.""" - - -def wire(value, out): - out += b'\0' * (-len(out) % 4) - if isinstance(value, bool): - out += struct.pack(' -#include -#include -#include -#include -#include -#include -#include -typedef uint8_t u8; -typedef uint32_t u32; -typedef uint64_t u64; -typedef int64_t s64; -#define __packed __attribute__((packed)) -#define IS_ENABLED(x) 0 -#define U32_MAX UINT32_MAX -#define U16_MAX UINT16_MAX -#define S64_MAX INT64_MAX -#define S64_MIN INT64_MIN -#define round_up(n, a) (((n) + (a) - 1) & ~((a) - 1)) -#define ERR_PTR(n) ((void *)(intptr_t)(n)) -#define PTR_ERR(p) ((int)(intptr_t)(p)) -#define IS_ERR(p) ((uintptr_t)(p) > (uintptr_t)-4096) -#define IS_ERR_OR_NULL(p) (!(p) || IS_ERR(p)) -#define GFP_KERNEL 0 -#define kmalloc(n, f) malloc(n) -#define kcalloc(n, s, f) calloc(n,s) -#define kfree(p) free(p) -#define mul_u32_u32(a, b) ((u64)(u32)(a) * (u32)(b)) -#define DIV_ROUND_CLOSEST_ULL(n, d) (((n) + (d) / 2) / (d)) -#define trace_iomfb_color_mode(...) ((void)0) -#define trace_iomfb_parse_mode_fail(...) ((void)0) -#define trace_iomfb_parse_mode_success(...) ((void)0) -#define trace_iomfb_timing_mode(...) ((void)0) -#define DRM_MODE_FLAG_PHSYNC 1 -#define DRM_MODE_FLAG_NHSYNC 2 -#define DRM_MODE_FLAG_PVSYNC 4 -#define DRM_MODE_FLAG_NVSYNC 8 -#define DRM_MODE_TYPE_DRIVER 1 -#define DRM_MODE_TYPE_PREFERRED 2 -struct drm_display_mode { - unsigned int type, flags; - int clock, vdisplay, vsync_start, vsync_end, vtotal; - int hdisplay, hsync_start, hsync_end, htotal, width_mm, height_mm; -}; -static void drm_mode_set_name(struct drm_display_mode *mode) { (void)mode; } -''' -data = '\n'.join('static const unsigned char blob_%u[] = {%s};' % - (i, ','.join(map(str, case[1]))) for i, case in enumerate(cases)) -suffix = r''' -int main(void) -{ -''' -for i, (name, blob, external, count, identity, depth, clock) in enumerate(cases): - suffix += f''' - {{ - struct dcp_parse_ctx ctx; - unsigned int count = 99; - assert(parse(blob_{i}, sizeof(blob_{i}), &ctx) == 0); - struct dcp_display_mode *modes = enumerate_modes(&ctx, &count, 600, 340, 0, - {str(name.startswith('internal')).lower()}, {str(external).lower()}); - if (IS_ERR(modes) || count != {count}) {{ - fprintf(stderr, "case {i}: {name} count=%u\\n", count); - abort(); - }} -''' - if count: - suffix += f''' - assert(modes[0].color_mode_id == {identity}); - assert(modes[0].sdr_rgb.depth == {depth}); - assert(modes[0].mode.clock == {clock}); - assert(modes[0].mode.type & DRM_MODE_TYPE_PREFERRED); -''' - suffix += ' free(modes);\n }\n' -suffix += f' puts("PASS: {len(cases)} production binary-parser SDR, malformed-input, legacy and high-refresh cases");\n return 0;\n}}\n' -with tempfile.TemporaryDirectory(prefix='apple-external-sdr-') as directory: - directory = Path(directory) - unit = directory / 'test.c' - unit.write_text(prefix + header + body + data + suffix) - command = shlex.split(os.environ.get('CC', 'clang')) - command += ['-std=gnu11', '-Wall', '-Wextra', '-Werror', '-Wno-sign-compare', '-O1', '-g', - '-fsanitize=address,undefined', '-fno-omit-frame-pointer'] - subprocess.run(command + [str(unit), '-o', str(directory / 'test')], check=True) - subprocess.run([str(directory / 'test')], check=True) diff --git a/tools/diagnostics/test-apple-h17p-edid.py b/tools/diagnostics/test-apple-h17p-edid.py deleted file mode 100644 index 57cac9f7e1635..0000000000000 --- a/tools/diagnostics/test-apple-h17p-edid.py +++ /dev/null @@ -1,211 +0,0 @@ -#!/usr/bin/env python3 -# SPDX-License-Identifier: GPL-2.0-only OR MIT -"""Exercise production DPAV routing, admission and in-flight EDID ownership.""" -from pathlib import Path -import os -import shlex -import subprocess -import tempfile - -root = Path(__file__).resolve().parents[2] -base = root / 'drivers/gpu/drm/apple' - -def function(file, signature): - source = (base / file).read_text() - start = source.index(signature) - opening = source.index('{', start) - while ';' in source[start:opening]: - start = source.index(signature, start + 1) - opening = source.index('{', start) - end, depth = opening + 1, 1 - while depth: - depth += (source[end] == '{') - (source[end] == '}') - end += 1 - return source[start:end] - -av = (base / 'epic/dpavservep.c').read_text() -af = (base / 'afk.c').read_text() -announce = af[af.index('static void afk_recv_handle_init('):af.index('static void afk_recv_handle_init(') + 6500] -assert announce.index('ops->prepare(service,') < announce.index('EPIC_CAT_REPORT, AFKV2_OPEN') -assert '.prepare = dcpavserv_prepare,' in av and '.prepare = dcpdpserv_prepare,' in av -prefix = r''' -#undef NDEBUG -#include -#include -#include -#include -#include -#include -#include -#include -typedef uint8_t u8; -typedef uint64_t u64; -typedef int64_t s64; -#define U16_MAX UINT16_MAX -#define READ_ONCE(x) (x) -#define WRITE_ONCE(x,v) ((x)=(v)) -#define ERR_PTR(x) ((void *)(intptr_t)(x)) -#define PTR_ERR(x) ((int)(intptr_t)(x)) -#define IS_ERR(x) ((uintptr_t)(x) > (uintptr_t)-4096) -#define spin_lock_irqsave(p,f) do { assert(!*(p)); *(p)=true; (f)=0; } while(0) -#define spin_unlock_irqrestore(p,f) do { (void)(f); assert(*(p)); *(p)=false; } while(0) -#define lockdep_assert_held(p) assert(*(p)) -static bool *acquire(bool *p) { assert(!*p); *p=true; return p; } -static void release(bool **p) { if(*p) { assert(**p); **p=false; } } -static bool *once(bool *p) { assert(*p); *p=false; return NULL; } -#define scoped_guard(k,p) for(bool *_lock __attribute__((cleanup(release)))=acquire(p); _lock; _lock=once(_lock)) -static void mutex_lock(bool *p) { (void)acquire(p); } -static void mutex_unlock(bool *p) { (void)once(p); } -#define dev_info(...) ((void)0) -#define dev_err(...) ((void)0) -#define trace_dcpavserv_init(...) ((void)0) -#define DRM_CONNECTOR_REGISTERED 1 -#define to_platform_device(x) (x) -static void reinit_completion(int *p) { *p=0; } -static void complete(int *p) { *p=1; } -static void schedule_work(int *p) { (*p)++; } -#define dev_dbg_ratelimited(...) ((void)0) -#define dev_warn_ratelimited(...) ((void)0) -#define trace_dcp_recv_msg(...) ((void)0) -#define DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL 2 -#define DCP_FIRMWARE_H17P 2 -#define AV_ENDPOINT 0x29 -#define SYSTEM_ENDPOINT 0x20 -#define DISP0_ENDPOINT 0x23 -#define DPAVSERV_ENDPOINT 0x28 -#define DPAVSERV_ENDPOINT_V2 0x2a -#define DPTX_ENDPOINT 0x2a -#define DPTX_ENDPOINT_V2 0x2c -#define DPTX_CONTROLLER_ENDPOINT 0x24 -#define IOMFB_ENDPOINT 0x37 -struct apple_dcp; -struct apple_connector { void *dcp; struct { int registration_state; } base; bool connected, edid_invalidation_pending; int hotplug_wq; }; -struct apple_dcp_afkep { struct apple_dcp *dcp; u8 endpoint; bool wire_v2; }; -struct apple_epic_service { struct apple_dcp_afkep *ep; unsigned int users; bool enabled,torndown; struct { u64 generation,epoch; } admission; void *cookie; unsigned int channel; }; -struct apple_dcp { - struct apple_dcp_afkep *dcpavservep, *dptxep, *avep, *systemep, *ibootep, *dptx_controller_ep; - struct { bool lock, enabled; u64 epoch; int enable_completion; struct apple_epic_service *service; } dcpavserv; - struct apple_connector *connector; unsigned int nr_modes; - struct { int iomfb_method_profile; } hw; - bool hpd_mutex, typec_cable_connected, quiescing; - u64 typec_generation; - int fw_compat; - void *dev; -}; -struct drm_edid { int identity; }; -static unsigned int reads, allocations, legacy_reads, receives; -static unsigned int event; -static struct apple_dcp_afkep *received_ep; -static struct apple_epic_service *afk_service_get(struct apple_epic_service *s) -{ if(!s || !s->enabled || s->torndown) return NULL; s->users++; return s; } -static void afk_service_put(struct apple_epic_service *s) -{ assert(s->users); s->users--; } -static void afk_service_retire(struct apple_epic_service *s) { s->enabled=false;s->torndown=true; } -static void drm_edid_free(const struct drm_edid *e) -{ if(e) { assert(allocations); allocations--; free((void *)e); } } -static void dcpavserv_teardown(struct apple_epic_service *s); -static const struct drm_edid *dcpavserv_read_edid_inline(struct apple_epic_service *s) -{ - struct apple_dcp *d=s->ep->dcp; - assert(!d->hpd_mutex && !d->dcpavserv.lock && s->users==2); - reads++; - if(event==1 || event==2) { d->typec_generation++; d->typec_cable_connected=(event==2); } - if(event==3) { s->enabled=false; dcpavserv_teardown(s); } - struct drm_edid *e=malloc(sizeof(*e)); assert(e); e->identity=42; allocations++; return e; -} -static const struct drm_edid *dcpavserv_read_edid_legacy(struct apple_epic_service *s) -{ legacy_reads++; return dcpavserv_read_edid_inline(s); } -static void iomfb_recv_msg(struct apple_dcp *d, u64 message) { (void)d; (void)message; } -static void afk_receive_message(struct apple_dcp_afkep *ep, u64 message) -{ (void)message; received_ep=ep; receives++; } -''' -parts = [function('epic/dpavservep.c', 'static void dcpavserv_notify_edid('), - function('epic/dpavservep.c', 'static void dcpavserv_invalidate_edid('), - function('epic/dpavservep.c', 'static void dcpavserv_teardown('), - function('dcp.c', 'static struct apple_dcp_afkep *dcp_afkep('), - function('dcp.c', 'static void dcp_recv_msg('), - function('epic/dpavservep.c', 'static bool dcpavserv_identity('), - function('epic/dpavservep.c', 'static bool dcpavserv_admission_current('), - function('epic/dpavservep.c', 'static int dcpavserv_prepare('), - function('epic/dpavservep.c', 'static int dcpdpserv_prepare('), - function('epic/dpavservep.c', 'static void dcpavserv_init('), - function('epic/dpavservep.c', 'bool dcpavserv_edid_current('), - function('epic/dpavservep.c', 'bool dcpavserv_edid_service_current('), - function('epic/dpavservep.c', 'const struct drm_edid *dcpavserv_copy_edid(')] -suffix = r''' -int main(void) -{ - u64 epoch=0; - int device; - struct apple_connector connector={.dcp=&device,.base.registration_state=DRM_CONNECTOR_REGISTERED,.connected=true}; - struct apple_dcp d={.fw_compat=DCP_FIRMWARE_H17P,.dev=&device,.connector=&connector,.nr_modes=1}; - struct apple_dcp_afkep av={.dcp=&d,.endpoint=0x28}, tx={.dcp=&d,.endpoint=0x2a}; - d.dcpavservep=&av; d.dptxep=&tx; - assert(dcp_afkep(&d,0x28)==&av && dcp_afkep(&d,0x2a)==&tx && !dcp_afkep(&d,0x2c)); - dcp_recv_msg(&d,0x28,1); assert(received_ep==&av && receives==1); - dcp_recv_msg(&d,0x2a,2); assert(received_ep==&tx && receives==2); - d.hw.iomfb_method_profile=DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL; - av.endpoint=0x2a; av.wire_v2=true; tx.endpoint=0x2c; - assert(!dcp_afkep(&d,0x28) && dcp_afkep(&d,0x2a)==&av && dcp_afkep(&d,0x2c)==&tx); - dcp_recv_msg(&d,0x28,3); assert(receives==2); - dcp_recv_msg(&d,0x2a,4); assert(received_ep==&av && receives==3); - dcp_recv_msg(&d,0x2c,5); assert(received_ep==&tx && receives==4); - d.dcpavservep=NULL; dcp_recv_msg(&d,0x2a,6); assert(receives==4); d.dcpavservep=&av; - struct apple_epic_service s={.ep=&av,.users=1,.enabled=true,.channel=1}; - const char *name="dcpav-service-epic", *class="AppleDCPDP2HDMI"; - assert(!dcpavserv_prepare(&s,name,class,1)); - assert(dcpavserv_prepare(&s,name,class,0)==-ENODEV); - assert(dcpavserv_prepare(&s,name,class,3)==-ENODEV); - assert(dcpavserv_prepare(&s,name,class,-1)==-ENODEV); - assert(dcpavserv_prepare(&s,"dcpdp-service-epic",class,1)==-ENODEV); - assert(dcpavserv_prepare(&s,name,"wrong",1)==-ENODEV); - assert(dcpavserv_prepare(&s,NULL,class,1)==-ENODEV); - assert(dcpavserv_prepare(&s,name,NULL,1)==-ENODEV); - av.endpoint=0x28; assert(dcpavserv_prepare(&s,name,class,1)==-ENODEV); av.endpoint=0x2a; - assert(dcpdpserv_prepare(&s,"dcpdp-service-epic",class,3)==-ENODEV); - d.dcpavserv.enabled=true; assert(dcpavserv_prepare(&s,name,class,1)==-EBUSY); - d.dcpavserv.service=&s; d.dcpavserv.epoch=1; d.typec_generation=1; d.typec_cable_connected=true; - const struct drm_edid *e=dcpavserv_copy_edid(&d,1,&epoch); - assert(!IS_ERR(e) && s.users==1 && reads==1); drm_edid_free(e); - d.typec_cable_connected=false; - assert(PTR_ERR(dcpavserv_copy_edid(&d,1,&epoch))==-ESTALE && reads==1); - d.typec_cable_connected=true; - assert(PTR_ERR(dcpavserv_copy_edid(&d,0,&epoch))==-ESTALE && reads==1); - event=1; assert(PTR_ERR(dcpavserv_copy_edid(&d,1,&epoch))==-ESTALE && !allocations && s.users==1); - event=2; d.typec_cable_connected=true; - assert(PTR_ERR(dcpavserv_copy_edid(&d,2,&epoch))==-ESTALE && !allocations && s.users==1); - event=0; e=dcpavserv_copy_edid(&d,3,&epoch); assert(!IS_ERR(e)); drm_edid_free(e); - event=3; assert(PTR_ERR(dcpavserv_copy_edid(&d,3,&epoch))==-ENODEV && !allocations && !s.users); - assert(PTR_ERR(dcpavserv_copy_edid(&d,3,&epoch))==-ENODEV); - assert(connector.edid_invalidation_pending && connector.hotplug_wq && d.dcpavserv.epoch==2); - /* Same slot and physical generation: CLOSE/re-OPEN gets a fresh ticket. */ - s.enabled=true; assert(!dcpavserv_prepare(&s,name,class,1)); dcpavserv_init(&s,name,class,1); - assert(d.dcpavserv.service==&s && s.users==1 && d.dcpavserv.epoch==3); - mutex_lock(&d.dcpavserv.lock); - assert(!dcpavserv_edid_service_current(&d,1) && dcpavserv_edid_service_current(&d,3)); - mutex_unlock(&d.dcpavserv.lock); - event=0; e=dcpavserv_copy_edid(&d,3,&epoch); assert(!IS_ERR(e) && epoch==3); - /* Simulate retirement in the final validation-to-publication gap. */ - s.enabled=false; dcpavserv_teardown(&s); s.enabled=true; assert(!dcpavserv_prepare(&s,name,class,1)); dcpavserv_init(&s,name,class,1); - mutex_lock(&d.dcpavserv.lock); assert(!dcpavserv_edid_service_current(&d,epoch)); mutex_unlock(&d.dcpavserv.lock); - drm_edid_free(e); assert(d.typec_generation==3 && d.dcpavserv.epoch==5 && s.users==1); - s.enabled=false; dcpavserv_teardown(&s); - av.wire_v2=false; av.endpoint=0x28; d.hw.iomfb_method_profile=0; - d.dcpavserv.enabled=false; assert(!dcpavserv_prepare(&s,name,NULL,0)); - assert(dcpavserv_prepare(&s,name,class,1)==-ENODEV); - assert(!dcpdpserv_prepare(&s,NULL,NULL,3)); - d.dcpavserv.service=&s; d.dcpavserv.enabled=true; s.enabled=true; s.users=1; event=0; - d.typec_cable_connected=false; e=dcpavserv_copy_edid(&d,0,&epoch); - assert(!IS_ERR(e) && legacy_reads==1 && s.users==1); drm_edid_free(e); - assert(!allocations && !d.hpd_mutex && !d.dcpavserv.lock); - puts("PASS: production old/new lookup and receive, exact service admission before OPEN, delayed disconnect/replug, service detach pins, legacy transport"); -} -''' -with tempfile.TemporaryDirectory(prefix='apple-h17p-edid-') as tmp: - source=Path(tmp)/'test.c' - source.write_text(prefix+'\n'.join(parts)+suffix) - cc=shlex.split(os.environ.get('HOSTCC','clang')) - subprocess.run(cc+['-std=gnu11','-Wall','-Werror', - '-O1','-g','-fsanitize=address,undefined','-fno-omit-frame-pointer', - str(source),'-o',str(Path(tmp)/'test')],check=True) - subprocess.run([str(Path(tmp)/'test')],check=True) diff --git a/tools/diagnostics/test-apple-j700-crossbar.py b/tools/diagnostics/test-apple-j700-crossbar.py deleted file mode 100644 index 0bc7577127196..0000000000000 --- a/tools/diagnostics/test-apple-j700-crossbar.py +++ /dev/null @@ -1,119 +0,0 @@ -#!/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) diff --git a/tools/diagnostics/test-apple-j700-d134.py b/tools/diagnostics/test-apple-j700-d134.py deleted file mode 100644 index d29debcd45634..0000000000000 --- a/tools/diagnostics/test-apple-j700-d134.py +++ /dev/null @@ -1,111 +0,0 @@ -#!/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") diff --git a/tools/diagnostics/test-apple-j700-dp.py b/tools/diagnostics/test-apple-j700-dp.py deleted file mode 100644 index 1bcb4014b82c6..0000000000000 --- a/tools/diagnostics/test-apple-j700-dp.py +++ /dev/null @@ -1,29 +0,0 @@ -#!/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() -functions = [] -for name in ("dptxport_v2_group_call", "dptxport_call_group1", "dptxport_group_call"): - start = source.index("static int " + name + "(") - end = source.index("\n}\n", start) + 3 - functions.append(source[start:end]) -with tempfile.TemporaryDirectory(prefix="j700-dp-") as work: - work = Path(work) - (work / "group-call.h").write_text("\n\n".join(functions)) - 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) diff --git a/tools/diagnostics/test-apple-j700-opaque-scope.py b/tools/diagnostics/test-apple-j700-opaque-scope.py deleted file mode 100644 index d9827134366b2..0000000000000 --- a/tools/diagnostics/test-apple-j700-opaque-scope.py +++ /dev/null @@ -1,83 +0,0 @@ -#!/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") diff --git a/tools/diagnostics/test-apple-j700-provider.py b/tools/diagnostics/test-apple-j700-provider.py deleted file mode 100644 index 9872c6d55be3b..0000000000000 --- a/tools/diagnostics/test-apple-j700-provider.py +++ /dev/null @@ -1,123 +0,0 @@ -#!/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"}) diff --git a/tools/diagnostics/test-apple-j700-tuning.py b/tools/diagnostics/test-apple-j700-tuning.py deleted file mode 100644 index b6dc336eb71f6..0000000000000 --- a/tools/diagnostics/test-apple-j700-tuning.py +++ /dev/null @@ -1,103 +0,0 @@ -#!/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"}) diff --git a/tools/diagnostics/wayland-output-frame-test.py b/tools/diagnostics/wayland-output-frame-test.py deleted file mode 100644 index d9fa634ee9cf2..0000000000000 --- a/tools/diagnostics/wayland-output-frame-test.py +++ /dev/null @@ -1,163 +0,0 @@ -#!/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 os -import tempfile -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, 61)) -parser.add_argument("--pattern", choices=("moving", "gamma"), default="moving") -parser.add_argument("--widget-pixels", action="store_true", help="Save only this test widget, before compositor/scanout") -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, "pattern": args.pattern, "ticks": 0, "draws": 0, - "actual_connector": None, "physical_visibility": "requires user observation"} -feedback = set() -complete_frames = set() - - -def draw(_area, context, width, height): - surface = window.get_surface() - monitor = display.get_monitor_at_surface(surface) if surface else None - if monitor: - stats["actual_connector"] = monitor.get_connector() - if args.pattern == "gamma": - context.set_source_rgb(0.08, 0.08, 0.08) - context.paint() - context.set_source_rgb(1, 1, 1) - context.select_font_face("Sans") - context.set_font_size(24) - context.move_to(24, 48) - context.show_text("Match a solid grey to the checkerboard brightness; view from a distance") - y, h = int(height * 0.25), int(height * 0.5) - w = int(width * 0.15) - left = int(width * 0.04) - for row in range(0, h, 2): - for col in range(0, w, 2): - value = ((row // 2) + (col // 2)) % 2 - context.set_source_rgb(value, value, value) - context.rectangle(left + col, y + row, 2, 2) - context.fill() - for index, value in enumerate((0.50, 0.67, 0.735, 0.80)): - x = int(width * (0.24 + index * 0.19)) - context.set_source_rgb(value, value, value) - context.rectangle(x, y, w, h) - context.fill() - context.set_source_rgb(1, 1, 1) - context.move_to(x, y + h + 40) - context.show_text(f"{value * 100:g}%") - stats["draws"] += 1 - return - 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 - 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) - surface = window.get_surface() - monitor = display.get_monitor_at_surface(surface) if surface else None - if monitor: - stats["actual_connector"] = monitor.get_connector() - if args.pattern == "moving" or stats["actual_connector"] != args.connector: - widget.queue_draw() - return GLib.SOURCE_CONTINUE - - -def finish(): - window.destroy() - loop.quit() - return GLib.SOURCE_REMOVE - - -def save_widget_pixels(): - try: - if stats["actual_connector"] != args.connector or not stats["draws"]: - raise RuntimeError("test widget not ready on requested connector") - width, height = area.get_width(), area.get_height() - if width <= 0 or height <= 0 or width > 8192 or height > 8192: - raise RuntimeError("widget dimensions out of diagnostic bounds") - paintable = Gtk.WidgetPaintable.new(area) - snapshot = Gtk.Snapshot.new() - paintable.snapshot(snapshot, width, height) - node = snapshot.to_node() - if node is None: - raise RuntimeError("widget snapshot empty") - renderer = window.get_renderer() - texture = renderer.render_texture(node, None) - directory = tempfile.mkdtemp(prefix="aurora-widget-pixels-", dir="/var/tmp") - path = os.path.join(directory, "widget.png") - fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600) - with os.fdopen(fd, "wb") as output: - output.write(texture.save_to_png_bytes().get_data()) - output.flush() - os.fsync(output.fileno()) - stats["widget_png"] = path - stats["widget_dimensions"] = [width, height] - stats["renderer_type"] = renderer.__gtype__.name - stats["widget_pixels_pass"] = True - stats["widget_scope"] = "owned test widget only; before compositor and physical scanout" - except Exception as error: - stats["widget_pixels_pass"] = False - stats["widget_error_type"] = type(error).__name__ - 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() -if args.widget_pixels: - GLib.timeout_add_seconds(2, save_widget_pixels) -GLib.timeout_add_seconds(args.seconds, 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 if args.pattern == "moving" else 1) -stats["presentation_pass"] = len(feedback) >= args.seconds * 10 -stats["target_pass"] = stats["actual_connector"] == args.connector -stats["verification_scope"] = "compositor feedback and output placement; physical scanout is not measured" -print(json.dumps(stats, sort_keys=True)) -checks = ("callback_pass", "draw_pass", "presentation_pass", "target_pass") if args.pattern == "moving" else ("draw_pass", "target_pass") -if args.widget_pixels: - checks += ("widget_pixels_pass",) -raise SystemExit(0 if all(stats[k] for k in checks) else 1) diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile index ac5717ad1078c..6efdaf9c73cb1 100644 --- a/tools/testing/selftests/Makefile +++ b/tools/testing/selftests/Makefile @@ -19,6 +19,7 @@ TARGETS += devices/probe TARGETS += dmabuf-heaps TARGETS += drivers/apple TARGETS += drivers/dma-buf +TARGETS += drivers/gpu/apple-dcp TARGETS += drivers/ntsync TARGETS += drivers/s390x/uvdevice TARGETS += drivers/net diff --git a/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-ring.py b/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-ring.py index 65d6fdeca7a54..b55eaea38c8d7 100644 --- a/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-ring.py +++ b/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-ring.py @@ -24,7 +24,7 @@ static int check_ring(struct ep *ep,u32 rptr,u32 wptr,size_t payload_len,size_t *total) { struct epic_hdr *ehdr;struct epic_sub_hdr *eshdr;struct afk_qe *hdr; - bool compact=false;size_t total_epic_size,total_size;int ret=0; + size_t total_epic_size,total_size;int ret=0; ''' suffix = r''' *total=total_size; diff --git a/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-send.py b/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-send.py index 0d80a729a3342..70dbd5d3a69f4 100644 --- a/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-send.py +++ b/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-send.py @@ -30,6 +30,8 @@ #define SZ_1M 1048576U #define GFP_KERNEL 0 #define MAX_PENDING_CMDS 16 +#define AFKV2_INTERFACES (1U << 16) +#define lockdep_assert_held(l) ((void)(l)) #define cpu_to_le32(x) (x) #define le32_to_cpu(x) (x) #define cpu_to_le64(x) (x) @@ -56,7 +58,7 @@ bool done,free_on_ack; int *completion; }; struct dcp { void *dev; bool quiescing; }; -struct apple_dcp_afkep { struct dcp *dcp; bool wire_v2,inline_v2; int endpoint; }; +struct apple_dcp_afkep { struct dcp *dcp; bool wire_v2,inline_v2; int endpoint,lock;u8 *command_tags; }; struct ops { const char *name; }; struct apple_epic_service { struct apple_dcp_afkep *ep; struct ops *ops; unsigned int channel; unsigned long cmd_map[1]; @@ -106,7 +108,8 @@ suffix = r''' int main(void) { - struct dcp dcp={0};struct apple_dcp_afkep ep={.dcp=&dcp,.wire_v2=true,.inline_v2=true}; + u8 tags[AFKV2_INTERFACES]={0}; + struct dcp dcp={0};struct apple_dcp_afkep ep={.dcp=&dcp,.wire_v2=true,.inline_v2=true,.command_tags=tags}; struct ops ops={.name="test"};struct apple_epic_service s={.ep=&ep,.ops=&ops,.channel=4,.enabled=true,.users=1}; u8 request[112]={0x5a},*output=calloc(1,32888);size_t received;u32 status=99; active=&s; @@ -155,4 +158,11 @@ puts("PASS: extracted production sender, 128-byte command envelope / 32888-byte reply, short OOB, strict/legacy callers and actual late-ACK ownership"); } ''' -pathlib.Path(sys.argv[2]).write_text(prefix + source[start:end] + source[reply_match.start():reply_end] + suffix) +tag_match = re.search(r"static u16 afk_command_tag\([^;]*?\)\n\{", source) +if not tag_match: + raise RuntimeError("production interface tag allocator not found") +tag_end, tag_depth = tag_match.end(), 1 +while tag_depth: + tag_depth += (source[tag_end] == "{") - (source[tag_end] == "}") + tag_end += 1 +pathlib.Path(sys.argv[2]).write_text(prefix + source[tag_match.start():tag_end] + source[start:end] + source[reply_match.start():reply_end] + suffix) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/Makefile b/tools/testing/selftests/drivers/gpu/apple-dcp/Makefile new file mode 100644 index 0000000000000..bbb39cada55bf --- /dev/null +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/Makefile @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +TEST_PROGS := run.sh +TEST_FILES := run.py common.py edid.py recovery.py rate.py phy.py lifecycle.py pm.py idle.py boot.py dwc3 README +include ../../../lib.mk diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/README b/tools/testing/selftests/drivers/gpu/apple-dcp/README new file mode 100644 index 0000000000000..b04b6b9934193 --- /dev/null +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/README @@ -0,0 +1,77 @@ +Apple external display host regressions +====================================== + +Run from a kernel source checkout: + + make -C tools/testing/selftests TARGETS=drivers/gpu/apple-dcp run_tests + +Or keep translation units, commands, failure logs and source hashes: + + tools/testing/selftests/drivers/gpu/apple-dcp/run.sh --output /tmp/apple-dcp-results + +An installed copy needs --source /path/to/kernel. --group edid, rate, phy or +lifecycle or boot selects one group. HOSTCC selects the host compiler (default clang). +Python 3 and a compiler supporting AddressSanitizer/UndefinedBehaviorSanitizer +are required. No target access, firmware command, captured fixture or kernel +configuration is required. + +One TAP entry point uses shared production extraction and compiler handling. +The modules contain public synthetic host boundaries and retained assertions: + +* edid: actual AFK admission and OPEN/CLOSE handling; native/HDMI providers + alternating over 64 epochs; foreign owners, stale replies, reader/command + pins, ordered receive progress and legacy service behavior. +* rate: actual binary parser, EDID/capacity policy, receipt ingress, getters + and lookup; all arrival orders, conflicting/discarded siblings, rejected + replacement catalogs, HPD retirement and recovery; malformed/duplicate + metadata and independent EDID decoding; eight bounded delayed retries. +* phy: actual parking/mux/init/reset/configure callbacks, masked PMA context, + faulted ACK/BIST cleanup, dirty-route retry, OFF/new-calibration epochs, + warm reset and legacy T8103/T8122/T8140/T8142 route controls. +* lifecycle: actual physical-disconnect authority and ownership despite failed + teardown; DWC3/core/PHY PM and reset-reference recovery; exact firmware + tuple/resource admission, unsupported VRR rejection and power reply bounds; + 15,552 D134 dispatcher scope/length/context and legacy reply cases. + +Transport, MMIO, kernel locks and work scheduling are host mocks. These tests +establish source behavior; electrical readiness, firmware/FDT ownership, +concurrency and system suspend require separate kernel/hardware qualification. +Kernel KUnit tests and apple-afkv2 remain separate tests in their existing +standard layouts. No archived diagnostic script is a dependency of this suite. + +The runner preserves host artifacts on failure as well as success. Existing +physical-disconnect fixture warnings for unused mock boundaries and signed +host/kernel ABI comparisons are handled only by those fixtures' original +compiler options; production source compilation uses the normal Kbuild policy. + +The bounded-PM group shares the PHY fixture and executes the actual PM get/put +and mux wrapper, usb_hc_died and typec_mux_set. The probe group executes real +CD321x register/unregister/rollback and retry-state functions. USB n/y/m uses +the actual optional dependency and a real Kconfig producer (KCONFIG_CONF may +select an existing producer). Legacy lane/teardown groups keep executing the +same locked mux body; they do not pretend their old USB mocks own runtime PM. +The cpuidle group executes the actual Apple table/probe, generic registration +and s2idle selector/entry, and tick_freeze/unfreeze. It checks source behavior +only; RTC, elapsed suspend time and simultaneous hardware execution remain +separate qualification gates. The original human cpuidle patch is preserved. + +The service-rebind group also runs retained reviewer controls through the +actual sender, receive worker, recovery, quiesce and EDID wrapper. It covers +CLOSED cancellation before lookup, first late ACK versus already consumed +duplicates, reader-pinned cross-slot/channel reuse, and the explicit physical +absence window. Per-interface sequence history has a fixed 64-KiB bound per +AFKV2 endpoint; reply routing still ignores reader-only retired services. + +Two wrap controls intentionally retain the command-17 replay assertion +failure, for OOB and inline. The group verifies that these counterexamples +still reproduce, alongside nonvacuous mutations and passing corrected safety +controls. A passing host suite does not establish a replay-exclusion contract +or admit a hardware trial. Token/sequence semantics remain a separate protocol +fact to establish; the tests do not manufacture a firmware echo guarantee. + +The boot group executes the actual OSLOG dispatcher with synthetic fixed-width +words. Known opaque event types remain visible, unknown types retain warnings, +and only the existing buffer-request path can allocate/ACK buffers. The EDID +service-rebind group also distinguishes intentional optional EOPNOTSUPP from +required admission failures and genuine optional-service errors, and proves +refused services never OPEN or publish. No captured firmware values are used. diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/accounting.py b/tools/testing/selftests/drivers/gpu/apple-dcp/accounting.py new file mode 100644 index 0000000000000..ce2791cc3a379 --- /dev/null +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/accounting.py @@ -0,0 +1,318 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright 2026 Ryan Murray +"""Source-connected accounting regressions retaining adverse-review assertions. + +SRCU, module and hardware leaves are models; the transaction, DCP and mux core +functions are extracted from the selected public source. No device is accessed. +""" +import quiesce +import transaction +from common import block + +def srcu_index(ROOT): + unit=quiesce.make_unit(ROOT) + main=block(unit,'int main(') + unit=unit.replace(main,main.replace('int main(','int __attribute__((unused)) prior_main(',1),1) + old_lock=block(unit,'static int srcu_read_lock(') + old_unlock=block(unit,'static void srcu_read_unlock(') + old_mutex_unlock=block(unit,'static void mutex_unlock(') + leaves=r''' + static pthread_mutex_t schedule_mutex=PTHREAD_MUTEX_INITIALIZER; + static pthread_cond_t schedule_cond=PTHREAD_COND_INITIALIZER; + static int srcu_bank,bank_readers[2],released_index=-1; + static bool pause_unlock,pause_seen,resume_unlock; + static _Thread_local int expected_index=-1; + static int srcu_read_lock(int *s) { + (void)s;pthread_mutex_lock(&schedule_mutex);int i=srcu_bank;bank_readers[i]++;expected_index=i;pthread_mutex_unlock(&schedule_mutex);return i; + } + static void srcu_read_unlock(int *s,int i) { + (void)s;pthread_mutex_lock(&schedule_mutex);released_index=i; + fprintf(stderr,"SRCU unlock: acquired=%d released=%d bank0=%d bank1=%d\n",expected_index,i,bank_readers[0],bank_readers[1]); + assert(i==expected_index&&bank_readers[i]>0);bank_readers[i]--;expected_index=-1; + pthread_cond_broadcast(&schedule_cond);pthread_mutex_unlock(&schedule_mutex); + } + static void mutex_unlock(struct mutex *m) { + assert(!pthread_mutex_unlock(&m->value)); + if(m==&shared_context.lock&&pause_unlock) { + pthread_mutex_lock(&schedule_mutex);pause_seen=true;pause_unlock=false;pthread_cond_broadcast(&schedule_cond); + while(!resume_unlock)pthread_cond_wait(&schedule_cond,&schedule_mutex); + pthread_mutex_unlock(&schedule_mutex); + } + } + ''' + unit=unit.replace(old_lock,'',1).replace(old_unlock,'',1).replace(old_mutex_unlock,leaves,1) + unit+=r''' + static void *first_actor(void *p) { + struct typec_mux *m=p;struct typec_mux_state safe={.mode=TYPEC_STATE_SAFE};assert(typec_mux_set(m,&safe)==0);return NULL; + } + int main(void) { + struct cd321x c;initial(&c);mux.num_mux_devs=0;pause_unlock=true; + pthread_t thread;assert(!pthread_create(&thread,NULL,first_actor,&mux)); + pthread_mutex_lock(&schedule_mutex);while(!pause_seen)pthread_cond_wait(&schedule_cond,&schedule_mutex); + srcu_bank=1;pthread_mutex_unlock(&schedule_mutex); + struct typec_mux_state safe={.mode=TYPEC_STATE_SAFE};assert(typec_mux_begin(&mux,&safe)==0); + assert(mux.srcu_index==1&&bank_readers[0]==1&&bank_readers[1]==1); + pthread_mutex_lock(&schedule_mutex);resume_unlock=true;pthread_cond_broadcast(&schedule_cond);pthread_mutex_unlock(&schedule_mutex); + assert(!pthread_join(thread,NULL));typec_mux_abort(&mux);assert(bank_readers[0]==0&&bank_readers[1]==0); + puts("PASS same-handle SRCU read-index balance across context unlock"); + } + ''' + return unit + +def resources(ROOT): + actual=next(u for n,u,c,f in transaction.controls(ROOT) if n=='actual-dcp-normal-safe') + main=block(actual,'int main(') + actual=actual.replace(main,main.replace('int main(','int __attribute__((unused)) prior_route_main(',1),1) + actual=actual.replace('struct mux_control { int unused; };',r''' + struct mux_control; + struct mux_ops { int (*set)(struct mux_control *,int); }; + struct mux_chip { const struct mux_ops *ops; }; + struct mux_control { int lock,idle_state,cached_state; unsigned last_change;struct mux_chip *chip; }; + ''',1) + muxcore=(ROOT/'drivers/mux/core.c').read_text() + resource=r''' + #define MUX_IDLE_AS_IS -1 + #define MUX_CACHE_UNKNOWN -2 + static int hw_calls,hw_failures,fixed_restore_calls,fixed_restore_failures; + static unsigned ktime_get(void) { return 0; } + static void up(int *s) { (*s)++; } + static int hw_set(struct mux_control *m,int state) { (void)m;(void)state;hw_calls++;return hw_calls<=hw_failures?-EIO:0; } + ''' + resource+=block(muxcore,'static int mux_control_set(')+'\n'+block(muxcore,'int mux_control_deselect(')+'\n' + actual=actual.replace(block(actual,'static int mux_control_deselect('),resource,1) + actual=actual.replace(block(actual,'static int mux_control_select('),r'''static int mux_control_select(struct mux_control *m,unsigned n) { (void)m;(void)n;fixed_restore_calls++;return fixed_restore_calls<=fixed_restore_failures?-EIO:0; }''',1) + actual=actual.replace('static int gpiod_get_value_cansleep(void *p) { (void)p;return 0; }', 'static int gpio_level;\nstatic int gpiod_get_value_cansleep(void *p) { (void)p;return gpio_level; }',1) + actual+=r''' + static int peer_error; + static int peer_finish(struct typec_mux_dev *p,struct typec_mux_state *s) { (void)p;(void)s;return peer_error; } + int main(int argc,char **argv) { + (void)argc;struct cd321x c;initial(&c); + struct mux_ops ops={.set=hw_set};struct mux_chip chip={.ops=&ops}; + struct mux_control xbar={.lock=0,.idle_state=0,.cached_state=1,.chip=&chip}; + struct mux_control fixed_xbar={.lock=1,.idle_state=0,.cached_state=0,.chip=&chip}; + route.xbar=route.active_xbar=&xbar; + struct typec_mux_state safe={.mode=TYPEC_STATE_SAFE}; + if(argv[1][0]=='n') { + assert(typec_mux_set(&mux,&safe)==0&&xbar.lock==1&&!port.owner&&!port.safe_pending); + puts("PASS actual DCP finalizer + real mux deselect resource balance");return 0; + } + if(argv[1][0]=='l') { + phy_provider.finish=peer_finish;mux.mux_devs[0]=&display_provider;mux.mux_devs[1]=&phy_provider;peer_error=-EIO; + assert(typec_mux_set(&mux,&safe)==-EIO&&xbar.lock==1&&port.owner==&route&&port.safe_pending&&port.safe_finish_ready); + peer_error=0;assert(typec_mux_set(&mux,&safe)==0&&xbar.lock==1&&!port.owner&&!port.safe_pending); + puts("PASS successful DCP finalizer is not repeated after a later provider failure");return 0; + } + if(argv[1][0]=='h')hw_failures=1; + if(argv[1][0]=='f') {display.fixed_connector_type=2;display.hdmi_hpd=(void *)1;gpio_level=1;display.xbar=&fixed_xbar;fixed_restore_failures=1;} + assert(typec_mux_set(&mux,&safe)==-EIO&&xbar.lock==1&&port.owner==&route&&port.safe_pending&&!port.safe_finish_ready); + bool selected_after_error=route.selected; + int ret=typec_mux_set(&mux,&safe); + fprintf(stderr,"finalizer retry: kind=%c ret=%d permits=%d selected_after_error=%d owner=%p pending=%d hw_calls=%d fixed_calls=%d\n",argv[1][0],ret,xbar.lock,selected_after_error,(void *)port.owner,port.safe_pending,hw_calls,fixed_restore_calls); + assert(xbar.lock==1); + if(argv[1][0]=='h')assert(ret==-EIO&&hw_calls==1&&port.owner==&route&&port.safe_pending); + if(argv[1][0]=='f')assert(ret==0&&!port.owner&&!port.safe_pending&&fixed_restore_calls==2); + return 0; + } + ''' + return actual + +def getter_unit(ROOT): + getter=transaction.lifetime(ROOT) + main=block(getter,'int main(') + renamed=main.replace('int main(','int __attribute__((unused)) prior_lifetime_main(',1) + renamed=renamed[:-1]+'return 0;\n}' + getter=getter.replace(main,renamed,1) + getter=getter.replace('struct typec_mux_dev {', 'struct module { unsigned refs;bool going; };\nstruct device { unsigned refs; };\nstruct typec_mux_dev { struct module *owner;struct device dev;',1) + getter=getter.replace(block(getter,'static void typec_mux_put('),'void typec_mux_put(struct typec_mux *mux);',1) + getter=getter.replace('#include ',r''' + #include + static int allocation_count,allocation_failure; + static void *review_alloc(size_t size) { if(++allocation_count==allocation_failure)return NULL;return calloc(1,size); } + ''',1) + getter=getter.replace('#define kzalloc_obj(x) calloc(1,sizeof(x))','#define kzalloc_obj(x) review_alloc(sizeof(x))',1) + getter+=r''' + #undef IS_ERR + #define IS_ERR(p) ((uintptr_t)(p)>=(uintptr_t)-4095) + #define PTR_ERR(p) ((int)(intptr_t)(p)) + #define TYPEC_MUX_MAX_DEVS 3 + #define ARRAY_SIZE(a) (sizeof(a)/sizeof((a)[0])) + static struct typec_mux_dev graph_providers[2]; + static struct module modules[2]; + static bool graph_second_error; + static void *typec_mux_match(const struct fwnode_handle *f,const char *id,void *data) { (void)f;(void)id;(void)data;return NULL; } + static int fwnode_connection_find_matches(struct fwnode_handle *f,const char *id,void *data,void *(*fn)(const struct fwnode_handle *,const char *,void *),void **matches,unsigned max) { + (void)f;(void)id;(void)data;(void)fn;assert(max>=2); + graph_providers[0].dev.refs++;matches[0]=&graph_providers[0]; + if(graph_second_error)matches[1]=ERR_PTR(-EPROBE_DEFER); + else {graph_providers[1].dev.refs++;matches[1]=&graph_providers[1];} + return 2; + } + static bool try_module_get(struct module *m) { if(!m)return true;if(m->going)return false;m->refs++;return true; } + static void module_put(struct module *m) { if(m){assert(m->refs);m->refs--;} } + static void put_device(struct device *d) { assert(d->refs);d->refs--; } + ''' + getter='#define EPROBE_DEFER 517\n'+getter + mux=(ROOT/'drivers/usb/typec/mux.c').read_text() + getter+='\n'+block(mux,'struct typec_mux *fwnode_typec_mux_get(')+'\n'+block(mux,'void typec_mux_put(')+'\n' + getter+=r''' + static void graph_setup(void) { + graph_second_error=false;allocation_count=allocation_failure=0; + for(unsigned i=0;i<2;i++){modules[i]=(struct module){0};graph_providers[i]=(struct typec_mux_dev){.owner=&modules[i],.set=phy_set};} + } + static void graph_balanced(struct fwnode_handle *f) { + assert(!f->refs&&typec_mux_contexts.next==&typec_mux_contexts); + for(unsigned i=0;i<2;i++)assert(!modules[i].refs&&!graph_providers[i].dev.refs); + } + int main(void) { + struct fwnode_handle f={0};graph_setup(); + struct typec_mux *a=fwnode_typec_mux_get(&f),*b=fwnode_typec_mux_get(&f); + assert(!IS_ERR(a)&&!IS_ERR(b)&&a->context==b->context&&a->context->users==2&&f.refs==1); + for(unsigned i=0;i<2;i++)assert(modules[i].refs==2&&graph_providers[i].dev.refs==2); + typec_mux_put(a);typec_mux_put(b);graph_balanced(&f); + graph_setup();modules[1].going=true;a=fwnode_typec_mux_get(&f);assert(PTR_ERR(a)==-ENODEV);graph_balanced(&f); + graph_setup();graph_second_error=true;a=fwnode_typec_mux_get(&f);assert(PTR_ERR(a)==-EPROBE_DEFER);graph_balanced(&f); + graph_setup();allocation_failure=2;a=fwnode_typec_mux_get(&f);assert(PTR_ERR(a)==-ENOMEM);graph_balanced(&f); + graph_setup();a=fwnode_typec_mux_get(&f);graph_providers[0].removed=true; + struct typec_mux_state safe={.mode=TYPEC_STATE_SAFE};unsigned before=phy_calls; + assert(typec_mux_set(a,&safe)==-ENODEV&&phy_calls==before);typec_mux_put(a);graph_balanced(&f); + puts("PASS actual getter/context/module/device balance: same fwnode, failed module conversion, deferred provider, context allocation and stale-provider handle"); + return 0; + } + ''' + return getter + +def controls(root): + clock = transaction.clock_order(root).replace('if(parked)return 0;if(!backend_clock)', 'if(!backend_clock)', 1) + clock = clock.replace('assert(!port.owner&&!port.safe_pending);', 'assert(!port.owner&&!port.safe_pending&&phy_calls==1&&role_calls==2);', 1) + return [ + ('same-handle-srcu-index', srcu_index(root), ((),), ('-pthread',)), + ('mux-release-stages', resources(root), (('normal',), ('late',), ('hardware',), ('fixed',)), ('-pthread',)), + ('single-safe-before-role-exit', clock, ((),), ('-pthread',)), + ('mux-get-put-balance', getter_unit(root), ((),), ('-pthread',)), + ('lookup-unregister-module-lifetime', lookup_lifetime(root), ((),), ('-pthread',)), + ('kernel-module-conversion-balance', module_conversion(root), ((),), ('-pthread',)), + ] + +def lookup_lifetime(ROOT): + getter = getter_unit(ROOT) + mux = (ROOT / "drivers/usb/typec/mux.c").read_text() + module_unit=getter.replace(block(getter,'int main('),block(getter,'int main(').replace('int main(','int __attribute__((unused)) prior_getter_main(',1),1) + module_unit=module_unit.replace('struct module { unsigned refs;bool going; };','struct module { unsigned refs,refcnt;bool going; };',1) + module_unit=module_unit.replace('struct device { unsigned refs; };','struct device { unsigned refs;bool unregistered; };',1) + sync=r''' + static pthread_mutex_t conversion_mutex=PTHREAD_MUTEX_INITIALIZER; + static pthread_cond_t conversion_cond=PTHREAD_COND_INITIALIZER; + static bool lookup_ready,lookup_resume; + static unsigned active_readers; + static void synchronize_srcu(int *s) { (void)s;pthread_mutex_lock(&conversion_mutex);assert(active_readers==0);pthread_mutex_unlock(&conversion_mutex); } + static void device_unregister(struct device *d) { d->unregistered=true;assert(d->refs);d->refs--; } + static bool module_is_live(struct module *m) { return !m->going; } + static bool atomic_inc_not_zero(unsigned *p) { if(!*p)return false;(*p)++;return true; } + #define likely(x) (x) + #define trace_module_get(...) ((void)0) + #define _RET_IP_ 0 + ''' + pos=module_unit.index('static bool try_module_get(') + module_unit=module_unit[:pos]+sync+module_unit[pos:] + modulemain=(ROOT/'kernel/module/main.c').read_text() + module_unit=module_unit.replace(block(module_unit,'static bool try_module_get('),block(modulemain,'bool try_module_get('),1) + needle='return 2;\n }' + assert module_unit.count(needle)==1 + module_unit=module_unit.replace(needle,r''' + pthread_mutex_lock(&conversion_mutex);lookup_ready=true;pthread_cond_broadcast(&conversion_cond); + while(!lookup_resume)pthread_cond_wait(&conversion_cond,&conversion_mutex); + pthread_mutex_unlock(&conversion_mutex);return 2; + }''',1) + + declarations = r''' +static pthread_mutex_t conversion_mutex=PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t conversion_cond=PTHREAD_COND_INITIALIZER; +static bool lookup_ready,lookup_resume,draining; +static unsigned active_readers; +#define WRITE_ONCE(x,v) ((x)=(v)) +''' + start = module_unit.index('static pthread_mutex_t conversion_mutex=') + end = module_unit.index('static void synchronize_srcu(', start) + module_unit = module_unit[:start] + module_unit[end:] + pos = module_unit.index('static int srcu_read_lock(') + module_unit = module_unit[:pos] + declarations + module_unit[pos:] + module_unit = module_unit.replace(block(module_unit, 'static int srcu_read_lock('), r''' +static int srcu_read_lock(int *s) { (void)s;pthread_mutex_lock(&conversion_mutex);active_readers++;pthread_mutex_unlock(&conversion_mutex);return 0; } +''', 1) + module_unit = module_unit.replace(block(module_unit, 'static void srcu_read_unlock('), r''' +static void srcu_read_unlock(int *s,int index) { (void)s;assert(index==0);pthread_mutex_lock(&conversion_mutex);assert(active_readers);active_readers--;pthread_cond_broadcast(&conversion_cond);pthread_mutex_unlock(&conversion_mutex); } +''', 1) + module_unit = module_unit.replace(block(module_unit, 'static void synchronize_srcu('), r''' +static void synchronize_srcu(int *s) { (void)s;pthread_mutex_lock(&conversion_mutex);draining=true;pthread_cond_broadcast(&conversion_cond);while(active_readers)pthread_cond_wait(&conversion_cond,&conversion_mutex);pthread_mutex_unlock(&conversion_mutex); } +''', 1) + module_unit += '\n' + block(mux, 'void typec_mux_unregister(') + r''' +static struct typec_mux *lookup_result; +static void *getter_actor(void *node) { lookup_result=fwnode_typec_mux_get(node);return NULL; } +static void *unregister_actor(void *provider) { typec_mux_unregister(provider);return NULL; } +int main(void) { + struct fwnode_handle f={0};graph_setup(); + struct module *departing=calloc(1,sizeof(*departing));departing->going=true;graph_providers[0].owner=departing;graph_providers[0].dev.refs=1; + pthread_t getter_thread,remove_thread;assert(!pthread_create(&getter_thread,NULL,getter_actor,&f)); + pthread_mutex_lock(&conversion_mutex);while(!lookup_ready)pthread_cond_wait(&conversion_cond,&conversion_mutex); + assert(active_readers==1&&graph_providers[0].dev.refs==2);pthread_mutex_unlock(&conversion_mutex); + assert(!pthread_create(&remove_thread,NULL,unregister_actor,&graph_providers[0])); + pthread_mutex_lock(&conversion_mutex);while(!draining)pthread_cond_wait(&conversion_cond,&conversion_mutex); + assert(active_readers==1&&!graph_providers[0].dev.unregistered); + lookup_resume=true;pthread_cond_broadcast(&conversion_cond);pthread_mutex_unlock(&conversion_mutex); + assert(!pthread_join(getter_thread,NULL));assert(!pthread_join(remove_thread,NULL)); + assert(PTR_ERR(lookup_result)==-ENODEV&&graph_providers[0].dev.unregistered&&graph_providers[0].dev.refs==0&&active_readers==0); + assert(graph_providers[1].dev.refs==0&&!f.refs); + free(departing); + puts("PASS lookup-to-module conversion holds a reader until removed-provider rejection and balanced cleanup"); + return 0; +} +''' + return module_unit + + +def module_conversion(ROOT): + unit = getter_unit(ROOT) + main = block(unit, 'int main(') + unit = unit.replace(main, main.replace('int main(', 'int __attribute__((unused)) prior_getter_main(', 1), 1) + unit = unit.replace('struct module { unsigned refs;bool going; };', 'struct module { unsigned refs,refcnt;bool going; };', 1) + source = (ROOT / 'kernel/module/main.c').read_text() + pos = unit.index('static bool try_module_get(') + leaves = r""" +static bool module_is_live(struct module *m) { return !m->going; } +static bool atomic_inc_not_zero(unsigned *p) { if(!*p)return false;(*p)++;return true; } +static int atomic_dec_if_positive(unsigned *p) { if(!*p)return -1;return --(*p); } +#define likely(x) (x) +#define trace_module_get(...) ((void)0) +#define trace_module_put(...) ((void)0) +#define preempt_disable() ((void)0) +#define preempt_enable() ((void)0) +#define WARN_ON(v) assert(!(v)) +#define _RET_IP_ 0 +""" + unit = unit[:pos] + leaves + unit[pos:] + unit = unit.replace(block(unit, 'static bool try_module_get('), block(source, 'bool try_module_get('), 1) + unit = unit.replace(block(unit, 'static void module_put('), block(source, 'void module_put('), 1) + old = block(unit, 'static int srcu_read_lock(') + '\n' + block(unit, 'static void srcu_read_unlock(') + unit = unit.replace(old, r""" +static unsigned conversion_readers; +static int srcu_read_lock(int *s) { (void)s;conversion_readers++;return 0; } +static void srcu_read_unlock(int *s,int i) { (void)s;assert(i==0&&conversion_readers);conversion_readers--; } +""", 1) + unit += r""" +static void live_graph(void) { graph_setup();for(unsigned i=0;i<2;i++)modules[i].refcnt=1; } +static void module_balance(struct fwnode_handle *f) { graph_balanced(f);assert(!conversion_readers);for(unsigned i=0;i<2;i++)assert(modules[i].refcnt==1); } +int main(void) { + struct fwnode_handle f={0};live_graph(); + struct typec_mux *a=fwnode_typec_mux_get(&f),*b=fwnode_typec_mux_get(&f); + assert(!IS_ERR(a)&&!IS_ERR(b)&&a->context==b->context&&!conversion_readers); + for(unsigned i=0;i<2;i++)assert(modules[i].refcnt==3&&graph_providers[i].dev.refs==2); + typec_mux_put(a);typec_mux_put(b);module_balance(&f); + live_graph();modules[1].going=true;a=fwnode_typec_mux_get(&f);assert(PTR_ERR(a)==-ENODEV);module_balance(&f); + live_graph();allocation_failure=2;a=fwnode_typec_mux_get(&f);assert(PTR_ERR(a)==-ENOMEM);module_balance(&f); + live_graph();graph_second_error=true;a=fwnode_typec_mux_get(&f);assert(PTR_ERR(a)==-EPROBE_DEFER);module_balance(&f); + live_graph();graph_providers[0].removed=true;a=fwnode_typec_mux_get(&f);assert(PTR_ERR(a)==-ENODEV);module_balance(&f); + puts("PASS actual kernel module get/put and mux get/put: shared handles and every failed conversion unwind"); + return 0; +} +""" + return unit diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/boot.py b/tools/testing/selftests/drivers/gpu/apple-dcp/boot.py new file mode 100644 index 0000000000000..bf95df845443d --- /dev/null +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/boot.py @@ -0,0 +1,56 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +# Copyright 2026 Ryan Murray +"""Opaque RTKit event dispatch: no firmware addresses or captured payloads.""" +from common import block + +def oslog(root): + source = (root / 'drivers/soc/apple/rtkit.c').read_text() + mask = next(line for line in source.splitlines() + if line.startswith('#define APPLE_RTKIT_OSLOG_TYPE ')) + return r''' +#include +#include +#include +typedef uint64_t u64; +typedef uint8_t u8; +#define GENMASK_ULL(h,l) ((~0ULL << (l)) & (~0ULL >> (63-(h)))) +#define FIELD_GET(mask,value) (((value) & (mask)) >> __builtin_ctzll(mask)) +#define APPLE_RTKIT_OSLOG_BUFFER_REQUEST 1 +#define APPLE_RTKIT_EP_OSLOG 8 +struct buffer { unsigned sentinel; }; +struct apple_rtkit { void *dev; struct buffer oslog_buffer; }; +static unsigned warnings, information, requests; +static u64 observed; +static u8 observed_type; +#define dev_warn(dev,fmt,msg) do { (void)(dev); (void)(fmt); warnings++; observed=(msg); } while (0) +#define dev_info(dev,fmt,type,msg) do { (void)(dev); (void)(fmt); information++; observed_type=(type); observed=(msg); } while (0) +static void apple_rtkit_common_rx_get_buffer(struct apple_rtkit *rtk, struct buffer *b, u8 ep, u64 msg) +{ assert(b==&rtk->oslog_buffer && ep==8); requests++; observed=msg; } +''' + mask + '\n' + block(source, 'static void apple_rtkit_oslog_rx(') + r''' +static void reset(void) { warnings=information=requests=0; observed=0; observed_type=0; } +int main(void) +{ + struct apple_rtkit rtk={.oslog_buffer={.sentinel=0x12345678}}; + for (u8 type=3; type<=5; type++) { + for (unsigned edge=0; edge<2; edge++) { + u64 msg=((u64)type<<56) | (edge ? ((1ULL<<56)-1) : 0); + reset(); apple_rtkit_oslog_rx(&rtk,msg); + assert(!warnings && information==1 && !requests); + assert(observed==msg && observed_type==type); + assert(rtk.oslog_buffer.sentinel==0x12345678); + } + } + puts("PASS recognized opaque types preserve both payload bounds without buffer/ACK operations"); + const u8 unknown[]={0,2,6,0xff}; + for (unsigned i=0;iusb3_generic_phy[0]) == 0); + assert(phy_exit(d->usb3_generic_phy[0]) == 0); + root_refs = 0; + } +} +/* Boundary mocks acquire references through the production generic PHY API. */ +static int dwc3_core_init(struct dwc3 *d) +{ + starts++; + if (core_init_error) return core_init_error; + for (int i = 0; i < d->num_usb2_ports; i++) assert(phy_init(d->usb2_generic_phy[i]) == 0); + for (int i = 0; i < d->num_usb3_ports; i++) assert(phy_init(d->usb3_generic_phy[i]) == 0); + return dwc3_phy_power_on(d); +} +static int dwc3_apple_core_probe(struct dwc3_apple *a) +{ + int ret = dwc3_core_init(&a->dwc); + if (!ret) a->state = DWC3_APPLE_NO_CABLE; + return ret; +} +static void dwc3_apple_set_role(struct dwc3_apple *a, enum dwc3_apple_state s) +{ assert(atcphy_usb3_set_mode(a->usb3_phy, s == DWC3_APPLE_HOST ? PHY_MODE_USB_HOST : PHY_MODE_USB_DEVICE, 0) == 0); } +struct fixture { struct apple_atcphy atc; struct phy p2, p3, extra; struct dwc3_apple apple; }; +static void initial(struct fixture *f, unsigned int gen, bool fixed, bool pm) +{ + memset(f, 0, sizeof(*f)); + memset(core_regs, 0, sizeof(core_regs)); + memset(pipe_regs, 0, sizeof(pipe_regs)); + memset(usb2_regs, 0, sizeof(usb2_regs)); + hardware = (struct atcphy_hw){ + .gen = gen == 0 ? ATCPHY_GENERATION_T8103 : ATCPHY_GENERATION_T8122, + .has_t8140_dp = gen == 2, .has_usb4 = gen != 2, + .pipe_mux_clk = PIPEHANDLER_MUX_CTRL_CLK_T8103, + .pipe_mux_data = PIPEHANDLER_MUX_CTRL_DATA_T8103, + }; + f->atc = (struct apple_atcphy){ .hw = &hardware, + .regs.core = (unsigned char *)core_regs, .regs.lpdptx = (unsigned char *)core_regs, + .regs.pipehandler = (unsigned char *)pipe_regs, .regs.usb2phy = (unsigned char *)usb2_regs, + .mode = APPLE_ATCPHY_MODE_USB3, .typec_mode = APPLE_ATCPHY_MODE_USB3, + .pipe_state = ATCPHY_PIPEHANDLER_STATE_USB3, + .host_active = true, .fixed_usb2 = fixed, .ss_tunables = true, .dp_tunables = true, + }; + f->p3 = (struct phy){ .data = &f->atc, .ops = &usb3_ops, .power_count = 1, .init_count = 1, + .pm_enabled = pm, .pm_refs = pm ? 1 : 0 }; + f->p2 = (struct phy){ .data = &f->atc, .ops = &usb2_ops, .power_count = 1, .init_count = 1, + .pm_enabled = pm, .pm_refs = pm ? 1 : 0 }; + f->extra = (struct phy){ .ops = &plain_ops, .power_count = 1, .init_count = 1, + .pm_enabled = pm, .pm_refs = pm ? 1 : 0 }; + f->apple = (struct dwc3_apple){ .state = DWC3_APPLE_HOST, .reset = &f->atc.rcdev, + .usb2_phy = &f->p2, .usb3_phy = &f->p3, .core_active = true, .reset_pending = true, + .usb3_power_off_pending = 1, .usb2_power_off_pending = 1, + .dwc = { .num_usb3_ports = 1, .num_usb2_ports = 1, + .usb3_generic_phy = { &f->p3 }, .usb2_generic_phy = { &f->p2 } }, + }; + core_regs[ATCPHY_POWER_CTRL / 4] = ATCPHY_POWER_PHY_RESET_N | ATCPHY_POWER_SLEEP_BIG | + ATCPHY_POWER_SLEEP_SMALL | ATCPHY_POWER_APB_RESET_N; + core_regs[ATCPHY_POWER_STAT / 4] = core_regs[ATCPHY_POWER_CTRL / 4]; + core_regs[AUS_COMMON_DIG_RCAL1 / 4] = AUS_COMMON_DIG_RCAL1_ALL_CODES_DONE; + core_regs[ACIOPHY_TOP_PHY_STAT / 4] = ACIOPHY_TOP_PHY_STAT_LN0_UNK0; + pipe_regs[PIPEHANDLER_AON_GEN / 4] = PIPEHANDLER_AON_GEN_DWC3_RESET_N; + polls = fail_poll = reset_calls = reset_writes = core_writes = warnings = 0; + generic_warnings = 0; + host_exits = clocks_disabled = callback_calls = gadget_exits = event_cleanups = 0; + legacy_suspends = core_mode_exits = free_buffers = starts = deasserts = setup_cio = root_refs = 0; + host_init_error = gadget_init_error = core_init_error = reset_error = other_off_error = 0; + init_cleanup_fault = false; + syscore = true; + stuck_ack = false; +} +static void recover(void) { fail_poll = 0; stuck_ack = false; pipe_regs[PIPEHANDLER_LOCK_ACK / 4] = 0; } +static int exit_apple(struct fixture *f) +{ mutex_lock(&f->apple.lock); int ret = dwc3_apple_exit(&f->apple); mutex_unlock(&f->apple.lock); return ret; } +static int role(struct fixture *f, enum usb_role r) +{ struct usb_role_switch sw = { &f->apple }; return dwc3_usb_role_switch_set(&sw, r); } +static void clean(struct fixture *f) +{ + assert(!f->p3.power_count && !f->p2.power_count && !f->p3.init_count && !f->p2.init_count); + assert(!f->p3.pm_refs && !f->p2.pm_refs && !f->apple.core_active && !f->apple.reset_pending); + assert(!f->apple.usb3_power_off_pending && !f->apple.usb2_power_off_pending); + assert(!f->atc.host_active && !f->atc.mode_dirty && f->atc.mode == APPLE_ATCPHY_MODE_OFF); + assert(!syscore); + assert(!f->atc.lock && !f->p3.mutex && !f->p2.mutex && !f->apple.lock); + assert(!(pipe_regs[PIPEHANDLER_AON_GEN / 4] & PIPEHANDLER_AON_GEN_DWC3_RESET_N)); +} +static void cycles(void) +{ + unsigned int cases = 0; + for (unsigned int gen = 0; gen < 3; gen++) + for (unsigned int fixed = 0; fixed < 2; fixed++) + for (unsigned int pm = 0; pm < 2; pm++) + for (unsigned int fault = 0; fault < 4; fault++) { + struct fixture f; initial(&f, gen, fixed, pm); + if (fault == 1) { + stuck_ack = true; pipe_regs[PIPEHANDLER_LOCK_ACK / 4] = PIPEHANDLER_LOCK_EN; + } else if (fault > 1) { + f.atc.pipe_state = ATCPHY_PIPEHANDLER_STATE_DUMMY; fail_poll = fault - 1; + } + int ret = exit_apple(&f); + printf("CYCLE gen=%u fixed=%u pm=%u fault=%u exit=%d refs=%d init=%d\n", + gen, fixed, pm, fault, ret, f.p3.power_count, f.p3.init_count); fflush(stdout); + if (fault) { + assert(ret == -ETIMEDOUT && f.p3.power_count == 1 && f.p3.init_count == 1); + assert(f.apple.core_active && clocks_disabled == 0 && event_cleanups == 1 && host_exits == 1); + assert(syscore); + if (fault > 1) assert(f.atc.mode_dirty); + if (fault == 1) { + assert(role(&f, USB_ROLE_HOST) == -ETIMEDOUT && starts == 0 && deasserts == 0); + assert(host_exits == 1 && event_cleanups == 1 && f.p3.power_count == 1); + } + recover(); + assert(exit_apple(&f) == 0); + } else assert(ret == 0); + clean(&f); + assert(host_exits == 1 && clocks_disabled == 1 && event_cleanups == 1); + assert(exit_apple(&f) == 0 && host_exits == 1 && clocks_disabled == 1); + assert(role(&f, USB_ROLE_HOST) == 0 && f.p3.power_count == 1 && f.p3.init_count == 1); + assert(f.atc.mode == APPLE_ATCPHY_MODE_USB3 && !f.atc.mode_dirty); + assert(core_regs[ATCPHY_POWER_CTRL / 4] & ATCPHY_POWER_PHY_RESET_N); + assert(role(&f, USB_ROLE_NONE) == 0); + clean(&f); + assert(!generic_warnings); + if (!fault) assert(!warnings); + cases++; + } + printf("PASS: %u normal/parking/domain shutdown and restart scenarios\n", cases); +} +static void pending_reset(void) +{ + for (unsigned int gen = 0; gen < 3; gen++) { + struct fixture f; initial(&f, gen, false, true); + reset_error = -ETIMEDOUT; + assert(exit_apple(&f) == -ETIMEDOUT); + assert(!f.apple.core_active && f.apple.reset_pending && !f.p3.power_count && !f.p3.init_count); + assert(syscore); + assert(role(&f, USB_ROLE_HOST) == -ETIMEDOUT && !starts && !deasserts); + assert(host_exits == 1 && clocks_disabled == 1 && event_cleanups == 1 && callback_calls == 1); + reset_error = 0; + assert(exit_apple(&f) == 0); + clean(&f); + assert(host_exits == 1 && clocks_disabled == 1 && callback_calls == 1); + assert(reset_calls == 3); + } + puts("PASS: pending reset retry does not repeat host, PHY or core teardown"); +} +static void partial_release(void) +{ + struct fixture f; initial(&f, 1, false, true); + other_off_error = -EIO; + assert(exit_apple(&f) == -EIO); + assert(!f.p3.power_count && f.p3.init_count == 1 && f.p2.power_count == 1); + assert(!f.apple.usb3_power_off_pending && f.apple.usb2_power_off_pending == 1); + assert(callback_calls == 1 && !clocks_disabled); + other_off_error = 0; + assert(exit_apple(&f) == 0); clean(&f); + assert(callback_calls == 1 && event_cleanups == 1 && clocks_disabled == 1); + initial(&f, 1, false, false); + f.apple.dwc.num_usb3_ports = 2; f.apple.dwc.usb3_generic_phy[1] = &f.extra; + f.apple.usb3_power_off_pending = 3; + other_off_error = -EIO; + assert(exit_apple(&f) == -EIO && !f.p3.power_count && f.extra.power_count == 1); + other_off_error = 0; + assert(exit_apple(&f) == 0 && !f.extra.power_count && !f.extra.init_count); + clean(&f); assert(callback_calls == 1); + puts("PASS: successful USB3 references are not released twice after another PHY fails"); +} +static void init_failures(void) +{ + for (unsigned int device = 0; device < 2; device++) { + struct fixture f; initial(&f, 2, false, true); + assert(exit_apple(&f) == 0); + if (device) gadget_init_error = -EIO; else host_init_error = -EIO; + reset_error = -ETIMEDOUT; + assert(role(&f, device ? USB_ROLE_DEVICE : USB_ROLE_HOST) == -EIO); + assert(f.apple.reset_pending && !f.apple.core_active && !f.p3.power_count); + assert(syscore); + host_init_error = gadget_init_error = reset_error = 0; + assert(role(&f, device ? USB_ROLE_DEVICE : USB_ROLE_HOST) == 0); + assert(f.p3.power_count == 1 && f.p3.init_count == 1); + assert(role(&f, USB_ROLE_NONE) == 0); clean(&f); + } + struct fixture f; initial(&f, 1, false, false); assert(exit_apple(&f) == 0); + core_init_error = -EIO; reset_error = -ETIMEDOUT; + assert(role(&f, USB_ROLE_HOST) == -EIO && f.apple.reset_pending && !f.apple.core_active); + core_init_error = reset_error = 0; + assert(role(&f, USB_ROLE_HOST) == 0); + assert(role(&f, USB_ROLE_NONE) == 0); clean(&f); + initial(&f, 1, false, true); assert(exit_apple(&f) == 0); + host_init_error = -EIO; init_cleanup_fault = true; + assert(role(&f, USB_ROLE_HOST) == -EIO && f.apple.core_active); + assert(syscore); + assert(f.p3.power_count == 1 && f.p3.init_count == 1); + recover(); host_init_error = 0; init_cleanup_fault = false; + assert(role(&f, USB_ROLE_HOST) == 0 && f.p3.power_count == 1 && f.p3.init_count == 1); + assert(role(&f, USB_ROLE_NONE) == 0); clean(&f); + puts("PASS: host/gadget/core startup failure reset obligations drain before retry"); +} +static void pending_restart(void) +{ + unsigned int cases = 0; + for (unsigned int gen = 0; gen < 3; gen++) + for (unsigned int fixed = 0; fixed < 2; fixed++) + for (unsigned int pm = 0; pm < 2; pm++) + for (unsigned int fault = 1; fault < 4; fault++) { + struct fixture f; initial(&f, gen, fixed, pm); + if (fault == 1) { + stuck_ack = true; pipe_regs[PIPEHANDLER_LOCK_ACK / 4] = PIPEHANDLER_LOCK_EN; + } else { f.atc.pipe_state = ATCPHY_PIPEHANDLER_STATE_DUMMY; fail_poll = fault - 1; } + assert(exit_apple(&f) == -ETIMEDOUT); + recover(); + assert(role(&f, USB_ROLE_HOST) == 0 && f.p3.power_count == 1 && f.p3.init_count == 1); + assert(f.atc.mode == APPLE_ATCPHY_MODE_USB3 && !f.atc.mode_dirty); + assert(core_regs[ATCPHY_POWER_CTRL / 4] & ATCPHY_POWER_PHY_RESET_N); + assert(!generic_warnings); + assert(role(&f, USB_ROLE_NONE) == 0); clean(&f); cases++; + } + struct fixture f; initial(&f, 2, true, true); f.apple.fixed_hub = true; + assert(role(&f, USB_ROLE_NONE) == 0 && f.apple.state == DWC3_APPLE_HOST && f.p3.power_count == 1); + assert(exit_apple(&f) == 0); + mutex_lock(&f.apple.lock); + assert(dwc3_apple_init(&f.apple, DWC3_APPLE_HOST) == 0); + mutex_unlock(&f.apple.lock); + assert(f.p3.power_count == 1 && exit_apple(&f) == 0); clean(&f); + printf("PASS: %u pending-failure to HOST transitions and actual fixed-hub role no-op\n", cases); +} +static void negative_power(void) +{ + struct fixture f; initial(&f, 0, false, false); + f.atc.pipe_state = ATCPHY_PIPEHANDLER_STATE_DUMMY; fail_poll = 1; + assert(exit_apple(&f) == -ETIMEDOUT && f.p3.power_count == 1 && f.p3.init_count == 1); + recover(); assert(role(&f, USB_ROLE_HOST) == 0 && f.p3.power_count == 1); +} +static void negative_reset(void) +{ + struct fixture f; initial(&f, 0, false, false); reset_error = -ETIMEDOUT; + assert(exit_apple(&f) == -ETIMEDOUT && reset_calls == 1); + reset_error = 0; assert(exit_apple(&f) == 0 && reset_calls == 2); + assert(!(pipe_regs[PIPEHANDLER_AON_GEN / 4] & PIPEHANDLER_AON_GEN_DWC3_RESET_N)); +} +static void negative_legacy(void) +{ + struct fixture f; initial(&f, 0, false, false); + f.atc.pipe_state = ATCPHY_PIPEHANDLER_STATE_DUMMY; fail_poll = 1; + assert(phy_power_off(&f.p3) == -ETIMEDOUT && f.atc.mode_dirty); + recover(); + struct typec_mux_dev mux = { &f.atc }; + struct typec_mux_state state = { .mode = TYPEC_STATE_USB }; + assert(atcphy_mux_set(&mux, &state) == 0); + assert(core_regs[ATCPHY_POWER_CTRL / 4] & ATCPHY_POWER_PHY_RESET_N); +} +static void legacy_restore(void) +{ + for (unsigned int gen = 0; gen < 3; gen++) { + struct fixture f; initial(&f, gen, false, false); + f.atc.pipe_state = ATCPHY_PIPEHANDLER_STATE_DUMMY; fail_poll = 1; + assert(phy_power_off(&f.p3) == -ETIMEDOUT && f.atc.mode_dirty); + unsigned int before = core_writes; + assert(atcphy_usb3_set_mode(&f.p3, PHY_MODE_USB_HOST, 0) == -EBUSY && core_writes == before); + if (gen < 2) { + f.atc.dp_link_rate = ATCPHY_DP_LINK_RATE_RBR; + assert(atcphy_dp_configure(&f.atc, ATCPHY_DP_LINK_RATE_RBR) == -EBUSY && core_writes == before); + } + recover(); + struct typec_mux_dev mux = { &f.atc }; + struct typec_mux_state state = { .mode = TYPEC_STATE_USB }; + assert(atcphy_mux_set(&mux, &state) == 0 && !f.atc.mode_dirty && core_writes > before); + assert(core_regs[ATCPHY_POWER_CTRL / 4] & ATCPHY_POWER_PHY_RESET_N); + assert(phy_power_off(&f.p3) == 0 && !f.p3.power_count); + } + puts("PASS: M1/M2/J700 aborted shutdown blocks PIPE and restores saved lane mode"); +} +static void removal_and_generic(void) +{ + struct fixture f; initial(&f, 0, false, false); + root_refs = 1; assert(phy_init(&f.p3) == 0 && phy_power_on(&f.p3) == 0); + assert(exit_apple(&f) == 0); clean(&f); + unsigned int callbacks = callback_calls, clocks = clocks_disabled, events = event_cleanups; + dwc3_core_remove_no_shutdown(&f.apple.dwc); + assert(callbacks == callback_calls && clocks == clocks_disabled && events == event_cleanups); + assert(!core_mode_exits && free_buffers == 1 && !f.p3.power_count && !f.p3.init_count); + initial(&f, 1, false, false); + dwc3_core_exit(&f.apple.dwc); + assert(!f.p3.power_count && !f.p3.init_count && !f.p2.power_count && !f.p2.init_count); + assert(event_cleanups == 1 && clocks_disabled == 1 && callback_calls == 1); + puts("PASS: root-hub references, already-exited removal and ordinary generic core exit"); +} +int main(int argc, char **argv) +{ + if (argc == 2 && !strcmp(argv[1], "power")) { negative_power(); return 0; } + if (argc == 2 && !strcmp(argv[1], "reset")) { negative_reset(); return 0; } + if (argc == 2 && !strcmp(argv[1], "legacy")) { negative_legacy(); return 0; } + cycles(); pending_restart(); pending_reset(); partial_release(); init_failures(); legacy_restore(); removal_and_generic(); + puts("PASS: Apple teardown actual-function qualification"); return 0; +} diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/dwc3/mocks.c b/tools/testing/selftests/drivers/gpu/apple-dcp/dwc3/mocks.c new file mode 100644 index 0000000000000..e54e73146cbad --- /dev/null +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/dwc3/mocks.c @@ -0,0 +1,125 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +struct apple_atcphy { + const struct atcphy_hw *hw; + struct { unsigned char *core, *lpdptx, *pipehandler, *usb2phy; } regs; + struct reset_controller_dev rcdev; + void *dev; + int lock, dp_link_rate; + bool swap_lanes, tunnel_clock_on, tunnel_saved; + bool fixed_usb2, host_active, mode_dirty, ss_tunables, dp_tunables, aux_power_requested; + unsigned int dp_owned_pmas, dp_idle_pmas; + unsigned int dp_usb_context_valid, dp_usb_context_dirty; + u32 dp_usb_context[2][27]; + bool pipe_pending, usb_off_pending, usb_off_drained; + enum atcphy_mode mode, typec_mode; + enum atcphy_pipehandler_state pipe_state; +}; +struct phy; +struct phy_ops { int (*init)(struct phy *); int (*exit)(struct phy *); + int (*power_on)(struct phy *); int (*power_off)(struct phy *); }; +struct phy { struct apple_atcphy *data; const struct phy_ops *ops; + int mutex, power_count, init_count, pm_refs, pwr, dev; bool pm_enabled; }; +#define phy_get_drvdata(p) ((p)->data) +static int phy_pm_runtime_get_sync(struct phy *p) +{ if (!p->pm_enabled) return -ENOTSUPP; p->pm_refs++; return 0; } +static void phy_pm_runtime_put(struct phy *p) { if (p->pm_enabled) p->pm_refs--; } +static void phy_pm_runtime_put_sync(struct phy *p) { phy_pm_runtime_put(p); } +static int regulator_enable(int p) { assert(!p); return 0; } +static void regulator_disable(int p) { assert(!p); } +struct dwc3 { struct phy *usb2_generic_phy[3], *usb3_generic_phy[3]; + int num_usb2_ports, num_usb3_ports, usb2_phy, usb3_phy; + struct reset_controller_dev *reset; + int dev, usb_psy, vbus_draw_work; }; +struct dwc3_apple { struct dwc3 dwc; enum dwc3_apple_state state; + struct reset_controller_dev *reset; int lock; void *dev; + struct phy *usb2_phy, *usb3_phy; + bool fixed_hub, core_active, core_exit_pending, reset_pending; + unsigned long usb2_power_off_pending, usb3_power_off_pending; }; +static unsigned int polls, fail_poll, reset_calls, reset_writes, core_writes; +static unsigned int host_exits, clocks_disabled, callback_calls; +static unsigned int gadget_exits, event_cleanups, legacy_suspends, core_mode_exits, free_buffers; +static unsigned int starts, deasserts, setup_cio, root_refs; +static int host_init_error, gadget_init_error, core_init_error, reset_error, other_off_error; +static bool init_cleanup_fault; +static bool syscore; +static bool stuck_ack; +static u32 core_regs[0x30000 / 4], pipe_regs[0x1000 / 4], usb2_regs[0x1000 / 4]; +static u32 readl(void *p) { return *(u32 *)p; } +static void mask32(void *p, u32 m, u32 v) +{ + *(u32 *)p = (*(u32 *)p & ~m) | v; + if (p == &pipe_regs[PIPEHANDLER_LOCK_REQ / 4] && !stuck_ack) + pipe_regs[PIPEHANDLER_LOCK_ACK / 4] = *(u32 *)p & PIPEHANDLER_LOCK_EN; + if (p == &pipe_regs[PIPEHANDLER_AON_GEN / 4]) reset_writes++; + if ((uintptr_t)p >= (uintptr_t)core_regs && + (uintptr_t)p < (uintptr_t)core_regs + sizeof(core_regs)) core_writes++; +} +static void writel(u32 v, void *p) { mask32(p, ~0U, v); } +static void set32(void *p, u32 v) { mask32(p, 0, v); } +static void clear32(void *p, u32 v) { mask32(p, v, 0); } +static void core_mask32(struct apple_atcphy *p, u32 o, u32 m, u32 v) +{ + assert(o < sizeof(core_regs)); + mask32(p->regs.core + o, m, v); + if (o == ATCPHY_POWER_CTRL) core_regs[ATCPHY_POWER_STAT / 4] = core_regs[o / 4]; +} +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); } +#define readl_poll_timeout(a,v,c,i,t) \ + ({ (v) = readl(a); ++polls; polls == fail_poll || !(c) ? -ETIMEDOUT : 0; }) +#define readl_poll_timeout_atomic readl_poll_timeout +/* These dependencies are outside the cross-layer test; no DP ownership is seeded. */ +static int j700_dp_stop(struct apple_atcphy *p) { assert(!p->dp_owned_pmas); return 0; } +static int j700_dp_idle_pending(struct apple_atcphy *p) { (void)p; return 0; } +static void atc_tunnel_stop_t8103(struct apple_atcphy *p) { (void)p; } +static void atc_tunnel_restore(struct apple_atcphy *p) { (void)p; } +/* USB2 power-on is outside this teardown/recovery control. */ +static void atcphy_usb2_power_on(struct apple_atcphy *p) { (void)p; } +static void atcphy_apply_tunables(struct apple_atcphy *p, enum atcphy_mode m) { (void)p; (void)m; } +static int atcphy_configure_lanes(struct apple_atcphy *p, enum atcphy_mode m) { (void)p; (void)m; return 0; } +static int atcphy_enable_dp_aux(struct apple_atcphy *p) { (void)p; return 0; } +static int atcphy_auspll_apb_command(struct apple_atcphy *p, int command) +{ (void)p; (void)command; return 0; } +static int atcphy_dp_configure_lane(struct apple_atcphy *p, int lane, + const struct atcphy_dp_link_rate_configuration *config) +{ (void)p; (void)lane; (void)config; return 0; } +static void usb_phy_set_suspend(int p, int s) { (void)p; (void)s; legacy_suspends++; } +static void usb_phy_shutdown(int p) { (void)p; } +static void dwc3_event_buffers_cleanup(struct dwc3 *d) { (void)d; event_cleanups++; } +static void dwc3_clk_disable(struct dwc3 *d) { (void)d; clocks_disabled++; } +static void dwc3_host_exit(struct dwc3 *d); +static void dwc3_gadget_exit(struct dwc3 *d) { (void)d; gadget_exits++; } +static void dev_pm_syscore_device(void *d, bool s) { (void)d; syscore = s; } +static void dwc3_enable_susphy(struct dwc3 *d, bool s) { (void)d; (void)s; } +static int atcphy_dwc3_reset_assert(struct reset_controller_dev *, unsigned long); +static int reset_control_assert(struct reset_controller_dev *r) +{ if (!r) return 0; reset_calls++; if (reset_error) return reset_error; + return atcphy_dwc3_reset_assert(r, 0); } +static int atcphy_dwc3_reset_deassert(struct reset_controller_dev *, unsigned long); +static int reset_control_deassert(struct reset_controller_dev *r) +{ deasserts++; return atcphy_dwc3_reset_deassert(r, 0); } +static int phy_set_mode(struct phy *p, int m) { (void)p; (void)m; return 0; } +static int dwc3_apple_core_probe(struct dwc3_apple *a); +static int dwc3_core_init(struct dwc3 *d); +static void dwc3_apple_setup_cio(struct dwc3_apple *a) { (void)a; setup_cio++; } +static int dwc3_apple_usb2_mode_before_reset(enum dwc3_apple_state s, enum phy_mode *m) +{ if (s != DWC3_APPLE_HOST && s != DWC3_APPLE_DEVICE) return -EINVAL; + *m = s == DWC3_APPLE_HOST ? PHY_MODE_INVALID : PHY_MODE_USB_DEVICE; return 0; } +static void dwc3_apple_set_role(struct dwc3_apple *a, enum dwc3_apple_state s); +static int dwc3_host_init(struct dwc3 *d) +{ (void)d; if (init_cleanup_fault) { stuck_ack = true; + pipe_regs[PIPEHANDLER_LOCK_ACK / 4] = PIPEHANDLER_LOCK_EN; } return host_init_error; } +static int dwc3_gadget_init(struct dwc3 *d) { (void)d; return gadget_init_error; } +#define pm_runtime_get_sync(...) ((void)0) +#define pm_runtime_allow(...) ((void)0) +#define pm_runtime_disable(...) ((void)0) +#define pm_runtime_dont_use_autosuspend(...) ((void)0) +#define pm_runtime_put_noidle(...) ((void)0) +#define pm_runtime_set_suspended(...) ((void)0) +#define dev_set_drvdata(...) ((void)0) +#define cancel_work_sync(...) ((void)0) +#define power_supply_put(...) ((void)0) +static void dwc3_core_exit_mode(struct dwc3 *d) { (void)d; core_mode_exits++; } +static void dwc3_debugfs_exit(struct dwc3 *d) { (void)d; } +static void dwc3_ulpi_exit(struct dwc3 *d) { (void)d; } +static void dwc3_free_event_buffers(struct dwc3 *d) { (void)d; free_buffers++; } diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/dwc3/prefix.c b/tools/testing/selftests/drivers/gpu/apple-dcp/dwc3/prefix.c new file mode 100644 index 0000000000000..a3e62f8a4f12d --- /dev/null +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/dwc3/prefix.c @@ -0,0 +1,54 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +#include +#include +#include +#include +#include +#include +#include +typedef uint32_t u32; +typedef uint16_t u16; +typedef uint8_t u8; +#define ENOTSUPP 524 +#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) +#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 field_prep FIELD_PREP +#define __iomem +#define udelay(n) ((void)(n)) +#define usleep_range(a,b) ((void)(a), (void)(b)) +#define fallthrough __attribute__((fallthrough)) +static unsigned int warnings, generic_warnings; +static void warning(const char *message) +{ warnings++; if (strstr(message, "phy_power_on was called before phy_init")) generic_warnings++; } +#define dev_warn(d, message, ...) warning(message) +#define dev_err(...) ((void)0) +#define dev_dbg(...) ((void)0) +#define lockdep_assert_held(p) assert(*(p)) +#define WARN_ON_ONCE(v) assert(!(v)) +static void mutex_lock(int *m) { assert(!*m); *m = 1; } +static void mutex_unlock(int *m) { assert(*m); *m = 0; } +struct held { int *m; }; +static struct held hold(int *m) { mutex_lock(m); return (struct held){m}; } +static void release(struct held *h) { mutex_unlock(h->m); } +#define guard(kind) struct held held_guard __attribute__((cleanup(release))) = hold +enum phy_mode { PHY_MODE_USB_HOST, PHY_MODE_USB_DEVICE }; +enum { APPLE_ATCPHY_LANE_0, APPLE_ATCPHY_LANE_1 }; +enum usb_role { USB_ROLE_NONE, USB_ROLE_HOST, USB_ROLE_DEVICE }; +struct usb_role_switch { struct dwc3_apple *data; }; +#define usb_role_switch_get_drvdata(s) ((s)->data) +#define PHY_MODE_INVALID (-1) +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) +struct reset_controller_dev { int unused; }; +#define container_of(p,t,m) ((t *)((char *)(p) - offsetof(t,m))) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/edid.py b/tools/testing/selftests/drivers/gpu/apple-dcp/edid.py new file mode 100644 index 0000000000000..69ebe810f1c2d --- /dev/null +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/edid.py @@ -0,0 +1,934 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +# Copyright 2026 Ryan Murray +"""Actual native EDID admission, service epochs, stale replies and RX progress.""" +from pathlib import Path +from common import block, source_defines + +def rebind(root): + base = root / 'drivers/gpu/drm/apple' + def function(file,signature): + text=(base/file).read_text();start=text.index(signature);opening=text.index('{',start) + while ';' in text[start:opening]: + start=text.index(signature,start+1);opening=text.index('{',start) + end,depth=opening+1,1 + while depth: + depth+=(text[end]=='{')-(text[end]=='}');end+=1 + return text[start:end] + prefix=r''' +#undef NDEBUG +#include +#include +#include +#include +#include +#include +#define container_of(p,t,m) ((t *)((char *)(p)-offsetof(t,m))) +#include "afkv2-codec.h" +typedef int64_t s64; +typedef u64 dma_addr_t; +#define U16_MAX UINT16_MAX +#define EBUSY 16 +#define EOPNOTSUPP 95 +#define ENOENT 2 +#define MAX_PENDING_CMDS 16 +#define AFK_MAX_CHANNEL 16 +#define AFKV2_INTERFACES (1U << 16) +#define READ_ONCE(x) (x) +#define WRITE_ONCE(x,v) ((x)=(v)) +#define WARN_ON(x) (x) +#define bitmap_empty(p,n) (!*(p)) +#define spin_lock_irqsave(p,f) do { assert(!*(p));*(p)=true;(f)=0; } while(0) +#define spin_unlock_irqrestore(p,f) do { (void)(f);assert(*(p));*(p)=false; } while(0) +static int test_bit(unsigned int bit,const unsigned long *p) { return !!(*p & (1UL<(b)?(a):(b)) +#define strscpy(dst,src,n) snprintf(dst,n,"%s",(char *)(src)) +#define trace_dcpavserv_init(...) ((void)0) +#define dma_rmb() ((void)0) +#define DRM_CONNECTOR_REGISTERED 1 +#define DPAVSERV_ENDPOINT_V2 0x2a +#define DCP_IOMFB_METHODS_H17P_EXTERNAL 2 +#define EPIC_SUBTYPE_STD_SERVICE 0xc0 +#define EPIC_SUBTYPE_ANNOUNCE_H17P 0x11 +#define to_platform_device(x) (x) +#define ERR_PTR(x) ((void *)(intptr_t)(x)) +#define IS_ERR(x) ((uintptr_t)(x)>(uintptr_t)-4096) +#define lockdep_assert_held(p) assert(*(p)) +static bool *hpd_lock,*av_lock; +static void mutex_lock(bool *p) { if(p==hpd_lock) assert(!*av_lock);assert(!*p);*p=true; } +static void mutex_unlock(bool *p) { assert(*p);*p=false; } +static void complete(int *p) { (*p)++; } +static void reinit_completion(int *p) { *p=0; } +static void schedule_work(unsigned int *p) { (*p)++; } +struct work_struct { bool pending; }; +static bool queue_work(void *wq,struct work_struct *w) { assert(wq);bool fresh=!w->pending;w->pending=true;return fresh; } +struct apple_dcp;struct apple_dcp_afkep;struct apple_epic_service; +void afk_service_retry(struct apple_dcp_afkep *ep); +struct apple_epic_service_ops { char name[32];bool reusable,optional;void (*init)(struct apple_epic_service *,const char *,const char *,s64);int (*prepare)(struct apple_epic_service *,const char *,const char *,s64);int (*revalidate)(struct apple_epic_service *,const char *,const char *,s64);void (*teardown)(struct apple_epic_service *); }; +struct epic_cmd_info { u16 tag;u8 type;size_t reply_length;void *rxbuf,*txbuf;dma_addr_t rxbuf_dma,txbuf_dma;size_t rxlen,txlen;u32 retcode;bool done,free_on_ack;int *completion; }; +struct apple_epic_service { + struct apple_dcp_afkep *ep;const struct apple_epic_service_ops *ops; + bool lock,enabled,torndown;unsigned int users;u32 channel;u8 cmd_tag; + unsigned long cmd_map[1];struct epic_cmd_info cmds[MAX_PENDING_CMDS]; + struct { u64 generation,epoch; } admission;void *cookie; + bool pending_open;const char *pending_name,*pending_class;s64 pending_unit; +}; +struct apple_dcp_afkep { u8 *command_tags;bool lock,stopping;void *wq;struct work_struct service_work;struct apple_dcp *dcp;const struct apple_epic_service_ops *ops;bool wire_v2,inline_v2,match_epic_name;u32 endpoint;unsigned int num_channels;struct apple_epic_service services[AFK_MAX_CHANNEL]; }; +struct apple_connector { void *dcp;bool connected,edid_invalidation_pending;struct { int registration_state; } base;unsigned int hotplug_wq; }; +struct apple_dcp { + void *dev;int fw_compat;struct apple_dcp_afkep *dcpavservep;struct apple_connector *connector; + bool external_physically_absent,hpd_mutex,quiescing,typec_cable_connected;u64 typec_generation;unsigned int nr_modes; + struct { int iomfb_method_profile; } hw; + struct { bool lock,enabled;u64 epoch;int enable_completion;struct apple_epic_service *service; } dcpavserv; +}; +struct epic_cmd { u32 retcode;u64 rxbuf,txbuf;u32 rxlen,txlen; }; +static unsigned int dma_frees,opens; + +static int open_error,open_action; +void dpavservep_detach(struct apple_dcp *dcp); +static void inject_owner(struct apple_dcp_afkep *ep,u32 pending_channel); +static void dma_free_coherent(void *dev,size_t size,void *p,dma_addr_t address) +{ (void)dev;assert(size && (uintptr_t)p==address);free(p);dma_frees++; } +static void afk_remove_service_debugfs(struct apple_epic_service *s) { (void)s; } +static void afk_populate_service_debugfs(struct apple_epic_service *s) { (void)s; } +static int afk_send_epic(struct apple_dcp_afkep *e,u32 channel,u16 tag,int type,int category,u8 subtype,const void *p,size_t n) +{ + assert(channel && !tag && type==EPIC_TYPE_NOTIFY && category==EPIC_CAT_REPORT && subtype==AFKV2_OPEN && !p && !n); + opens++; + assert(!e->dcp->dcpavserv.enabled && !e->dcp->dcpavserv.service); + for(unsigned int i=0;inum_channels;i++) { + struct apple_epic_service *s=&e->services[i]; + if(s->channel==channel && !s->torndown) assert(!s->enabled && !s->users); + } + if(open_action==1) e->dcp->typec_generation++; + if(open_action==2) dpavservep_detach(e->dcp); + if(open_action==4) inject_owner(e,channel); + if(open_action==5) e->dcp->typec_cable_connected=false; + if(open_action==6) e->dcp->quiescing=true; + if(open_action==7) e->dcp->external_physically_absent=true; + if(open_action==3) { + for(unsigned int i=0;inum_channels;i++) + if(e->services[i].channel==channel && !e->services[i].torndown) e->services[i].channel++; + } + return open_error; +} +static void afk_v2_callback(struct apple_dcp_afkep *e,const struct afkv2_frame *f,const struct afkv2_command *c) { (void)e;(void)f;(void)c;assert(!"unexpected callback"); } + +struct item { const char *key,*value; s64 integer; }; +struct dcp_parse_ctx { struct item *items;unsigned int count,index;bool key; }; +struct iterator { struct dcp_parse_ctx *handle;unsigned int idx,len; }; +#define kfree(p) free((void *)(p)) +#define PTR_ERR(p) ((int)(intptr_t)(p)) +static struct item announcement_items[3]; +static int parse(u8 *p,size_t n,struct dcp_parse_ctx *ctx) +{ (void)p;assert(n==37);*ctx=(struct dcp_parse_ctx){.items=announcement_items,.count=3};return 0; } +static int iterator_begin(struct dcp_parse_ctx *h,struct iterator *it,bool dict) +{ assert(dict);h->index=0;h->key=true;*it=(struct iterator){.handle=h,.len=h->count};return 0; } +static char *parse_string(struct dcp_parse_ctx *h) +{ + struct item *i=&h->items[h->index];const char *p; + if(h->key) { p=i->key;h->key=false; } else { p=i->value;h->index++;h->key=true; } + if(!p) + return ERR_PTR(-EINVAL); + char *copy=malloc(strlen(p)+1);assert(copy);strcpy(copy,p);return copy; +} +static int parse_int(struct dcp_parse_ctx *h,s64 *out) +{ assert(!h->key);*out=h->items[h->index++].integer;h->key=true;return 0; } +static int skip(struct dcp_parse_ctx *h) { assert(!h->key);h->index++;h->key=true;return 0; } +''' + parts=[function('afk.c','static u16 afk_command_tag('),function('afk.c','void afk_service_retry('),function('parser.c','int parse_epic_service_init('),function('afk.h','static inline struct apple_epic_service *afk_service_get('), + function('afk.h','static inline void afk_service_put('),function('afk.h','static inline void afk_service_retire('),function('afk.h','static inline void afk_service_disable('), + function('afk.h','static inline bool afk_service_matches('),function('afk.h','static inline bool afk_service_reclaimable('), + function('afk.h','static inline bool\nafk_service_reinit('),function('afk.h','static inline void afk_service_discard('), + function('afk.h','static inline void afk_service_publish('), + function('epic/dpavservep.c','static void dcpavserv_notify_edid('),function('epic/dpavservep.c','static bool dcpavserv_identity('), + function('epic/dpavservep.c','static bool dcpavserv_admission_current('),function('epic/dpavservep.c','static int dcpavserv_prepare('), + function('epic/dpavservep.c','static int dcpavserv_revalidate('),function('epic/dpavservep.c','static void dcpavserv_init('),function('epic/dpavservep.c','static void dcpavserv_invalidate_edid('), + function('epic/dpavservep.c','static void dcpavserv_teardown('),function('epic/dpavservep.c','void dpavservep_detach('), + function('afk.c','static const struct apple_epic_service_ops *\nafk_match_service('), + function('afk.c','static struct apple_epic_service *afk_epic_find_service('), + function('afk.c','static void afk_discard_pending_open('),function('afk.c','static void afk_retry_service_init('),function('afk.c','static void afk_service_worker('),function('afk.c','static void afk_recv_handle_init('), + function('epic/dpavservep.c','static int dcpdpserv_prepare('), + function('epic/dpavservep.c','static void dcpdpserv_teardown('),function('afk.c','static void afk_recv_handle_teardown('), + function('afk.c','static void afk_recv_handle_reply('),function('afk.c','static void afk_v2_receive(')] + suffix=r''' +static struct apple_dcp d; +static struct apple_dcp_afkep ep; +static struct apple_connector con; +static void run_retries(void) { assert(!ep.lock);if(ep.service_work.pending) { ep.service_work.pending=false;afk_service_worker(&ep.service_work); } } +static const struct apple_epic_service_ops ops={.name="dcpav-service-epic",.reusable=true,.prepare=dcpavserv_prepare,.revalidate=dcpavserv_revalidate,.init=dcpavserv_init,.teardown=dcpavserv_teardown}; +static const struct apple_epic_service_ops announcement_ops[]={ + {.name="dcpav-service-epic",.reusable=true,.prepare=dcpavserv_prepare,.revalidate=dcpavserv_revalidate,.init=dcpavserv_init,.teardown=dcpavserv_teardown}, + {.name="dcpdp-service-epic",.reusable=true,.optional=true,.prepare=dcpdpserv_prepare,.teardown=dcpdpserv_teardown}, + {.name=""} +}; +static const char *name="dcpav-service-epic",*class="AppleDCPDP2HDMI"; +static const char *native_class="DCPDP13Service"; +static struct apple_epic_service *prepare(u32 channel,s64 unit,const char *n,const char *c) +{ + for(unsigned int i=0;iusers==1); +} +static void repeated_announcements(void) +{ + ep.ops=announcement_ops;ep.match_epic_name=true;ep.num_channels=0;d.fw_compat=DCP_FIRMWARE_H17P; + /* More than the entire slot budget; wrap channel identity as well. */ + for(unsigned int i=0;i<4*AFK_MAX_CHANNEL;i++) { + u16 channel=1+4*(i%4);unsigned int before=opens; + /* Flip, same-attachment revalidation, return flip; swap both providers. */ + bool native=((i%4==1 || i%4==2) != !!((i/4)%2)); + const char *provider=native?native_class:class; + announce(channel,name,provider,channel); + struct apple_epic_service *owner=d.dcpavserv.service; + if(native) assert(owner && "native DP EDID provider must publish"); + assert(owner->ops==&announcement_ops[0]); + assert(owner && owner->channel==channel && owner->users==1 && opens==before+1); + assert(ep.num_channels<=2); /* Real allocator reuses, no slot exhaustion. */ + u64 epoch=d.dcpavserv.epoch; + announce(channel,name,provider,channel); /* Active duplicate: no extra OPEN/pin. */ + assert(opens==before+1 && owner->users==1 && d.dcpavserv.epoch==epoch); + announce(channel+2,"dcpdp-service-epic",provider,channel+2); + assert(opens==before+1 && d.dcpavserv.service==owner && d.dcpavserv.epoch==epoch); + if(i%4==1) { + u64 generation=d.typec_generation; + closed(channel,0x6a01,0); /* Revalidation retires service, not attachment. */ + assert(!d.dcpavserv.service && d.typec_generation==generation && d.dcpavserv.epoch>epoch); + } else { + disconnect();connect(); + } + } + unsigned int missing_ops_before=opens; + assert(afk_match_service(&ep,"missing-service-epic")==NULL); + assert(afk_match_service(&ep,"dcpav-service-epic")==&announcement_ops[0]); + announce(13,"missing-service-epic",native_class,13); + announce(13,"dcpav-service-epic-extra",native_class,13); + assert(opens==missing_ops_before && !d.dcpavserv.service); + assert(ep.num_channels==2); /* Auxiliary rejection leaves a reusable slot. */ + unsigned int before=opens; + announce(13,name,"synthetic-wrong-class",13);assert(opens==before && !d.dcpavserv.service); + char long_class[161];memset(long_class,'x',160);long_class[160]=0; + announce(13,name,long_class,13);assert(opens==before && !d.dcpavserv.service); + for(unsigned int variant=0;variant<2;variant++) { + const char *provider=variant?native_class:class; + assert(!prepare(0,0,name,provider));assert(!prepare(65536,65536,name,provider)); + assert(!prepare(13,-1,name,provider));assert(!prepare(13,0,name,provider)); + assert(!prepare(13,1,name,provider));assert(!prepare(13,13,NULL,provider)); + assert(!prepare(13,13,"dcpdp-service-epic",provider)); + announce(13,"wrong-compartment",provider,13);assert(opens==before && !d.dcpavserv.service); + ep.endpoint=0x2c;announce(13,name,provider,13);assert(opens==before && !d.dcpavserv.service);ep.endpoint=0x2a; + d.quiescing=true;announce(13,name,provider,13);assert(opens==before && !d.dcpavserv.service);d.quiescing=false; + disconnect();announce(13,name,provider,13);assert(opens==before && !d.dcpavserv.service);connect(); + } + for(unsigned int variant=0;variant<4;variant++) { + const char *forged[]={"DCPDP13ServiceExtra","dcpdp13service","AppleDCPDP2HDMIExtra","unrelated-provider"}; + announce(13,name,forged[variant],13);assert(opens==before && !d.dcpavserv.service); + } + assert(!prepare(13,13,name,NULL)); + open_error=-EIO;announce(13,name,native_class,13);assert(opens==++before && !d.dcpavserv.service);open_error=0; + for(unsigned int window=0;window<5;window++) { + const int actions[]={1,2,3,5,6};open_action=actions[window]; + announce(13,name,native_class,13); + assert(opens==++before && !d.dcpavserv.service && !d.dcpavserv.enabled); + d.typec_cable_connected=true;d.quiescing=false; + } + open_action=4;announce(13,name,native_class,13);assert(opens==++before && d.dcpavserv.service==injected_owner); + assert(injected_owner->users==1);dpavservep_detach(&d);assert(!injected_owner->users); + open_action=0;announce(13,name,native_class,13);assert(opens==++before && d.dcpavserv.service); + struct apple_epic_service *old=d.dcpavserv.service; + assert(afk_service_get(old)==old);ep.command_tags[13]=1;old->cmd_map[0]=1; + disconnect();connect(); + announce(13,name,native_class,13); /* Same channel still owns a pending reply. */ + assert(opens==before && !d.dcpavserv.service && old->users==1 && old->channel==13); + /* The firmware need not announce again after the old reply drains. */ + old->cmds[0]=(struct epic_cmd_info){.tag=0,.type=0xc0,.rxlen=32,.txlen=32,.free_on_ack=true}; + old->cmds[0].rxbuf=calloc(1,32);old->cmds[0].txbuf=calloc(1,32); + old->cmds[0].rxbuf_dma=(uintptr_t)old->cmds[0].rxbuf; + old->cmds[0].txbuf_dma=(uintptr_t)old->cmds[0].txbuf; + u8 drain_oob[32],drain_reply[56];size_t drain_written; + assert(!afkv2_encode_oob_command(drain_oob,sizeof(drain_oob),0,old->cmds[0].rxbuf_dma,32,old->cmds[0].txbuf_dma,32)); + afkv2_put32(drain_oob+4,0); + assert(!afkv2_encode_frame(drain_reply,sizeof(drain_reply),1,13,0,0xc0,AFKV2_RESPONSE,0,drain_oob,sizeof(drain_oob),&drain_written)); + afk_v2_receive(&ep,0,0,drain_reply,drain_written); + run_retries(); + assert(d.dcpavserv.service && "a blocked reannouncement must recover after the old ACK without another announcement"); + assert(d.dcpavserv.service!=old && opens==++before && old->users==1 && !old->cmd_map[0]); + struct apple_epic_service *resumed=d.dcpavserv.service; + resumed->cmd_map[0]=1; + /* The DMA allocator can reuse both old addresses for the new command. */ + resumed->lock=true; + resumed->cmds[0]=(struct epic_cmd_info){.tag=afk_command_tag(resumed,0),.type=0xc0, + .rxbuf_dma=old->cmds[0].rxbuf_dma,.txbuf_dma=old->cmds[0].txbuf_dma,.rxlen=32,.txlen=32}; + resumed->lock=false; + afk_v2_receive(&ep,0,0,drain_reply,drain_written); + assert(!resumed->cmds[0].done && resumed->cmd_map[0] && "old ACK must not complete the next owner's command"); + resumed->cmd_map[0]=0; + dpavservep_detach(&d); + announce(1,name,class,1);assert(opens==++before && d.dcpavserv.service!=old); + struct apple_epic_service *fresh=d.dcpavserv.service;u64 epoch=d.dcpavserv.epoch; + dcpavserv_teardown(old);assert(d.dcpavserv.service==fresh && d.dcpavserv.epoch==epoch); + old->cmd_map[0]=0;afk_service_put(old);disconnect();connect(); + run_retries(); + announce(13,name,native_class,13);assert(opens==++before && d.dcpavserv.service); + struct apple_epic_service *owner=d.dcpavserv.service; + u64 owner_epoch=d.dcpavserv.epoch; + announce(5,name,class,5); /* A second admitted identity cannot displace the owner. */ + assert(opens==before && d.dcpavserv.service==owner && d.dcpavserv.epoch==owner_epoch); + dpavservep_detach(&d); + /* All slots held by retired readers: fail safely, then recover after puts. */ + for(unsigned int i=0;itorndown); + legacy=prepare(13,0,name,native_class);assert(legacy); + d.typec_generation++;afk_service_publish(legacy);dcpavserv_init(legacy,name,native_class,0); + assert(d.dcpavserv.service==legacy && legacy->users==1);dpavservep_detach(&d); + assert(dcpdpserv_prepare(legacy,"dcpdp-service-epic",native_class,0)==0); + ep.wire_v2=true;ep.endpoint=0x2a;d.external_physically_absent=false; + announce(U16_MAX,name,native_class,U16_MAX);assert(d.dcpavserv.service && d.dcpavserv.service->channel==U16_MAX); + dpavservep_detach(&d); + puts("PASS: actual AFKV2 HDMI/native DP alternating over 64 cycles: flip/revalidation/return, strict identity, stale OPEN generation/epoch/owner, auxiliary isolation, reader/command pins and legacy controls"); +} + +static void deferred_announcements(void) +{ + for(unsigned int cycle=0;cycle<64;cycle++) { + unsigned int event=cycle%10,before=opens,freed=dma_frees; + u16 channel=1+4*(cycle%4); + const char *provider=(cycle%2)?native_class:class; + announce(channel,name,provider,channel); + struct apple_epic_service *old=d.dcpavserv.service; + assert(old && opens==++before && afk_service_get(old)==old); + old->cmd_map[0]=1; + old->cmds[0]=(struct epic_cmd_info){.tag=0,.type=0xc0,.rxlen=32,.txlen=32,.free_on_ack=true}; + old->cmds[0].rxbuf=calloc(1,32);old->cmds[0].txbuf=calloc(1,32); + old->cmds[0].rxbuf_dma=(uintptr_t)old->cmds[0].rxbuf; + old->cmds[0].txbuf_dma=(uintptr_t)old->cmds[0].txbuf; + u8 oob[32],reply[56];size_t written; + assert(!afkv2_encode_oob_command(oob,sizeof(oob),0,old->cmds[0].rxbuf_dma,32,old->cmds[0].txbuf_dma,32)); + afkv2_put32(oob+4,0); + assert(!afkv2_encode_frame(reply,sizeof(reply),1,channel,0,0xc0,AFKV2_RESPONSE,0,oob,sizeof(oob),&written)); + u64 generation=d.typec_generation; + closed(channel,0x6a01,0); + assert(!d.dcpavserv.service && old->users==1 && old->torndown && d.typec_generation==generation); + u64 epoch=d.dcpavserv.epoch; + announce(channel,name,provider,channel); + run_retries(); + assert(opens==before && !d.dcpavserv.service && dma_frees==freed); + for(unsigned int i=0;ipending_open) continue; + assert(dcpavserv_revalidate(pending,pending->pending_name,pending->pending_class,pending->pending_unit)==0); + d.dcpavserv.enabled=true; + assert(dcpavserv_revalidate(pending,pending->pending_name,pending->pending_class,pending->pending_unit)==-EBUSY); + d.dcpavserv.enabled=false; + } + if(event==1) d.typec_generation++; + if(event==2) dpavservep_detach(&d); + if(event==3) d.typec_cable_connected=false; + if(event==4) d.quiescing=true; + struct apple_epic_service *other=NULL; + if(event==5) { other=prepare(channel+2,channel+2,name,class);publish(other,channel+2); } + if(event==6) assert(!ep.stopping); + if(event==7) open_error=-EIO; + if(event==8) announce(channel,name,provider,channel); + if(event==9) { d.typec_generation++;announce(channel,name,provider,channel); } + afk_v2_receive(&ep,0,0,reply,written); + if(event==6) { assert(ep.service_work.pending);ep.stopping=true; } + run_retries(); + assert(dma_frees==freed+2 && old->users==1 && !old->cmd_map[0]); + if(event==0 || event==8 || event==9) { + assert(d.dcpavserv.service && d.dcpavserv.service!=old); + assert(d.dcpavserv.service->channel==channel && opens==++before); + assert(d.dcpavserv.epoch>epoch && d.dcpavserv.service->users==1); + } else if(event==5) { + assert(d.dcpavserv.service==other && opens==before); + } else { + assert(!d.dcpavserv.service && opens==before+(event==7)); + } + afk_v2_receive(&ep,0,0,reply,written); /* Duplicate late ACK cannot touch the new owner. */ + assert(dma_frees==freed+2); + afk_service_put(old);run_retries(); + assert(!old->users); + if(event==6) { + for(unsigned int i=0;iusers==1); + assert(afk_service_get(old)==old); /* Delayed EDID reader pins the retired slot. */ + old->cmds[0]=(struct epic_cmd_info){.tag=0,.type=0xc0,.rxlen=32,.txlen=32,.free_on_ack=true}; + old->cmds[0].rxbuf=calloc(1,32);old->cmds[0].txbuf=calloc(1,32);assert(old->cmds[0].rxbuf && old->cmds[0].txbuf); + old->cmds[0].rxbuf_dma=(uintptr_t)old->cmds[0].rxbuf;old->cmds[0].txbuf_dma=(uintptr_t)old->cmds[0].txbuf;old->cmd_map[0]=1; + disconnect();assert(!d.dcpavserv.service && !d.dcpavserv.enabled && old->users==1 && old->torndown && !old->enabled); + assert(con.edid_invalidation_pending && !afk_service_get(old)); + assert(!afk_service_reinit(old,&ep,&ops,5)); + assert(!prepare(5,5,name,class)); /* No admission while physically disconnected. */ + connect();struct apple_epic_service *fresh=prepare(5,5,name,class);assert(fresh && fresh!=old);publish(fresh,5); + assert(d.dcpavserv.service==fresh && fresh->users==1); + u64 epoch=d.dcpavserv.epoch; + dcpavserv_teardown(old);assert(d.dcpavserv.service==fresh && d.dcpavserv.epoch==epoch); + closed(1,0x6a01,0);assert(d.dcpavserv.service==fresh && d.dcpavserv.epoch==epoch && !dma_frees); + u8 oob[32],reply[56];size_t written; + assert(!afkv2_encode_oob_command(oob,sizeof(oob),0,old->cmds[0].rxbuf_dma,32,old->cmds[0].txbuf_dma,32));afkv2_put32(oob+4,0); + assert(!afkv2_encode_frame(reply,sizeof(reply),1,1,0,0xc0,AFKV2_RESPONSE,0,oob,sizeof(oob),&written)); + afk_v2_receive(&ep,0,0,reply,written); + assert(dma_frees==2 && !old->cmd_map[0] && old->users==1 && d.dcpavserv.service==fresh && d.dcpavserv.epoch==epoch); + assert(!afk_service_reinit(old,&ep,&ops,9));afk_service_put(old); + afk_v2_receive(&ep,0,0,reply,written);assert(dma_frees==2); /* Duplicate late ACK. */ + disconnect();connect();struct apple_epic_service *next=prepare(9,9,name,class);assert(next);publish(next,9); + assert(d.dcpavserv.service==next); + closed(1,0x6a01,0);closed(5,0x6a01,0);assert(d.dcpavserv.service==next); + disconnect();connect();next=prepare(13,13,name,class);assert(next);publish(next,13);assert(d.dcpavserv.service==next); + closed(13,0x6a01,1);assert(d.dcpavserv.service==next); /* Nonzero result rejected. */ + closed(13,0x6a00,0);assert(d.dcpavserv.service==next); /* Wrong low flags rejected. */ + d.dcpavservep=NULL;closed(13,0x6a01,0);assert(d.dcpavserv.service==next);d.dcpavservep=&ep; + closed(13,0x6a01,0);assert(!d.dcpavserv.service && !d.dcpavserv.enabled); + u8 result_byte=0;struct afkv2_frame notification={.category=AFKV2_REPORT,.type=AFKV2_CLOSED,.interface_flags=1,.payload=&result_byte,.payload_size=1}; + assert(afkv2_closed_report(¬ification));notification.token=1;assert(!afkv2_closed_report(¬ification)); + notification.token=0;notification.payload_size=0;assert(!afkv2_closed_report(¬ification)); + assert(!prepare(0,0,name,class));assert(!prepare(65536,65536,name,class)); + assert(!prepare(9,1,name,class));assert(!prepare(9,9,"wrong",class));assert(!prepare(9,9,name,"wrong")); + struct apple_epic_service *pending=prepare(9,9,name,class);assert(pending); + disconnect();connect();publish(pending,9); + assert(!d.dcpavserv.service && pending->torndown && !pending->enabled && !pending->users); + pending=prepare(9,9,name,class);assert(pending); + mutex_lock(&d.hpd_mutex);d.typec_generation++;mutex_unlock(&d.hpd_mutex); + publish(pending,9); /* Generation change alone, without registry retirement. */ + assert(!d.dcpavserv.service && pending->torndown && !pending->enabled); + pending=prepare(13,13,name,class);assert(pending); + dpavservep_detach(&d);publish(pending,13); /* Registry retirement with no physical change. */ + assert(!d.dcpavserv.service && pending->torndown && !pending->enabled); + ep.wire_v2=false;ep.endpoint=0x28;d.hw.iomfb_method_profile=0; + assert(!prepare(7,7,name,class));pending=prepare(7,0,name,NULL);assert(pending);publish(pending,0); + assert(d.dcpavserv.service==pending && pending->users==1);dpavservep_detach(&d); + assert(!pending->users && !d.hpd_mutex && !d.dcpavserv.lock); + memset(&ep.services,0,sizeof(ep.services));ep.wire_v2=true;ep.endpoint=0x2a;d.hw.iomfb_method_profile=2; + repeated_announcements(); + deferred_announcements(); + free(ep.command_tags);ep.command_tags=NULL; + puts("PASS: actual dynamic 1/5/9/13 binding, forged identities, prepare/init epochs, physical retirement, reader pins, real old CLOSED/late ACK isolation and legacy unit 0"); +} +''' + return prefix + '\n'.join(parts) + suffix + +def admission(root): + + base = root / 'drivers/gpu/drm/apple' + + def function(file, signature): + source = (base / file).read_text() + start = source.index(signature) + opening = source.index('{', start) + while ';' in source[start:opening]: + start = source.index(signature, start + 1) + opening = source.index('{', start) + end, depth = opening + 1, 1 + while depth: + depth += (source[end] == '{') - (source[end] == '}') + end += 1 + return source[start:end] + + av = (base / 'epic/dpavservep.c').read_text() + af = (base / 'afk.c').read_text() + announce = af[af.index('static void afk_recv_handle_init('):af.index('static void afk_recv_handle_init(') + 6500] + assert announce.index('ops->prepare(service,') < announce.index('EPIC_CAT_REPORT, AFKV2_OPEN') + assert '.prepare = dcpavserv_prepare,' in av and '.prepare = dcpdpserv_prepare,' in av + prefix = r''' +#undef NDEBUG +#include +#include +#include +#include +#include +#include +#include +#include +typedef uint8_t u8; +typedef uint64_t u64; +typedef int64_t s64; +#define U16_MAX UINT16_MAX +#define READ_ONCE(x) (x) +#define WRITE_ONCE(x,v) ((x)=(v)) +#define ERR_PTR(x) ((void *)(intptr_t)(x)) +#define PTR_ERR(x) ((int)(intptr_t)(x)) +#define IS_ERR(x) ((uintptr_t)(x) > (uintptr_t)-4096) +#define spin_lock_irqsave(p,f) do { assert(!*(p)); *(p)=true; (f)=0; } while(0) +#define spin_unlock_irqrestore(p,f) do { (void)(f); assert(*(p)); *(p)=false; } while(0) +#define lockdep_assert_held(p) assert(*(p)) +static bool *acquire(bool *p) { assert(!*p); *p=true; return p; } +static void release(bool **p) { if(*p) { assert(**p); **p=false; } } +static bool *once(bool *p) { assert(*p); *p=false; return NULL; } +#define scoped_guard(k,p) for(bool *_lock __attribute__((cleanup(release)))=acquire(p); _lock; _lock=once(_lock)) +static void mutex_lock(bool *p) { (void)acquire(p); } +static void mutex_unlock(bool *p) { (void)once(p); } +#define dev_info(...) ((void)0) +#define dev_err(...) ((void)0) +#define trace_dcpavserv_init(...) ((void)0) +#define DRM_CONNECTOR_REGISTERED 1 +#define to_platform_device(x) (x) +static void reinit_completion(int *p) { *p=0; } +static void complete(int *p) { *p=1; } +static void schedule_work(int *p) { (*p)++; } +#define dev_dbg_ratelimited(...) ((void)0) +#define dev_warn_ratelimited(...) ((void)0) +#define trace_dcp_recv_msg(...) ((void)0) +#define DCP_IOMFB_METHODS_H17P_EXTERNAL 2 +#define DCP_FIRMWARE_H17P 2 +#define AV_ENDPOINT 0x29 +#define SYSTEM_ENDPOINT 0x20 +#define DISP0_ENDPOINT 0x23 +#define DPAVSERV_ENDPOINT 0x28 +#define DPAVSERV_ENDPOINT_V2 0x2a +#define DPTX_ENDPOINT 0x2a +#define DPTX_ENDPOINT_V2 0x2c +#define DPTX_CONTROLLER_ENDPOINT 0x24 +#define IOMFB_ENDPOINT 0x37 +struct apple_dcp; +struct apple_connector { void *dcp; struct { int registration_state; } base; bool connected, edid_invalidation_pending; int hotplug_wq; }; +struct apple_dcp_afkep { struct apple_dcp *dcp; u8 endpoint; bool wire_v2; }; +struct apple_epic_service { struct apple_dcp_afkep *ep; unsigned int users; bool enabled,torndown; struct { u64 generation,epoch; } admission; void *cookie; unsigned int channel; }; +struct apple_dcp { + struct apple_dcp_afkep *dcpavservep, *dptxep, *avep, *systemep, *ibootep, *dptx_controller_ep; + struct { bool lock, enabled; u64 epoch; int enable_completion; struct apple_epic_service *service; } dcpavserv; + struct apple_connector *connector; unsigned int nr_modes; + struct { int iomfb_method_profile; } hw; + bool external_physically_absent,hpd_mutex, typec_cable_connected, quiescing; + u64 typec_generation; + int fw_compat; + void *dev; +}; +struct drm_edid { int identity; }; +static unsigned int reads, allocations, legacy_reads, receives; +static unsigned int event; +static struct apple_dcp_afkep *received_ep; +static struct apple_epic_service *afk_service_get(struct apple_epic_service *s) +{ if(!s || !s->enabled || s->torndown) return NULL; s->users++; return s; } +static void afk_service_put(struct apple_epic_service *s) +{ assert(s->users); s->users--; } +static void afk_service_retire(struct apple_epic_service *s) { s->enabled=false;s->torndown=true; } +static void drm_edid_free(const struct drm_edid *e) +{ if(e) { assert(allocations); allocations--; free((void *)e); } } +static void dcpavserv_teardown(struct apple_epic_service *s); +static const struct drm_edid *dcpavserv_read_edid_inline(struct apple_epic_service *s) +{ + struct apple_dcp *d=s->ep->dcp; + assert(!d->hpd_mutex && !d->dcpavserv.lock && s->users==2); + reads++; + if(event==1 || event==2) { d->typec_generation++; d->typec_cable_connected=(event==2); } + if(event==3) { s->enabled=false; dcpavserv_teardown(s); } + struct drm_edid *e=malloc(sizeof(*e)); assert(e); e->identity=42; allocations++; return e; +} +static const struct drm_edid *dcpavserv_read_edid_legacy(struct apple_epic_service *s) +{ legacy_reads++; return dcpavserv_read_edid_inline(s); } +static void iomfb_recv_msg(struct apple_dcp *d, u64 message) { (void)d; (void)message; } +static void afk_receive_message(struct apple_dcp_afkep *ep, u64 message) +{ (void)message; received_ep=ep; receives++; } +''' + parts = [function('epic/dpavservep.c', 'static void dcpavserv_notify_edid('), + function('epic/dpavservep.c', 'static void dcpavserv_invalidate_edid('), + function('epic/dpavservep.c', 'static void dcpavserv_teardown('), + function('dcp.c', 'static struct apple_dcp_afkep *dcp_afkep('), + function('dcp.c', 'static void dcp_recv_msg('), + function('epic/dpavservep.c', 'static bool dcpavserv_identity('), + function('epic/dpavservep.c', 'static bool dcpavserv_admission_current('), + function('epic/dpavservep.c', 'static int dcpavserv_prepare('), + function('epic/dpavservep.c', 'static int dcpdpserv_prepare('), + function('epic/dpavservep.c', 'static void dcpavserv_init('), + function('epic/dpavservep.c', 'bool dcpavserv_edid_current('), + function('epic/dpavservep.c', 'bool dcpavserv_edid_service_current('), + function('epic/dpavservep.c', 'const struct drm_edid *dcpavserv_copy_edid(')] + suffix = r''' +int main(void) +{ + u64 epoch=0; + int device; + struct apple_connector connector={.dcp=&device,.base.registration_state=DRM_CONNECTOR_REGISTERED,.connected=true}; + struct apple_dcp d={.fw_compat=DCP_FIRMWARE_H17P,.dev=&device,.connector=&connector,.nr_modes=1}; + struct apple_dcp_afkep av={.dcp=&d,.endpoint=0x28}, tx={.dcp=&d,.endpoint=0x2a}; + d.dcpavservep=&av; d.dptxep=&tx; + assert(dcp_afkep(&d,0x28)==&av && dcp_afkep(&d,0x2a)==&tx && !dcp_afkep(&d,0x2c)); + dcp_recv_msg(&d,0x28,1); assert(received_ep==&av && receives==1); + dcp_recv_msg(&d,0x2a,2); assert(received_ep==&tx && receives==2); + d.hw.iomfb_method_profile=DCP_IOMFB_METHODS_H17P_EXTERNAL; + av.endpoint=0x2a; av.wire_v2=true; tx.endpoint=0x2c; + assert(!dcp_afkep(&d,0x28) && dcp_afkep(&d,0x2a)==&av && dcp_afkep(&d,0x2c)==&tx); + dcp_recv_msg(&d,0x28,3); assert(receives==2); + dcp_recv_msg(&d,0x2a,4); assert(received_ep==&av && receives==3); + dcp_recv_msg(&d,0x2c,5); assert(received_ep==&tx && receives==4); + d.dcpavservep=NULL; dcp_recv_msg(&d,0x2a,6); assert(receives==4); d.dcpavservep=&av; + struct apple_epic_service s={.ep=&av,.users=1,.enabled=true,.channel=1}; + const char *name="dcpav-service-epic", *class="AppleDCPDP2HDMI"; + assert(!dcpavserv_prepare(&s,name,class,1)); + assert(dcpavserv_prepare(&s,name,class,0)==-ENODEV); + assert(dcpavserv_prepare(&s,name,class,3)==-ENODEV); + assert(dcpavserv_prepare(&s,name,class,-1)==-ENODEV); + assert(dcpavserv_prepare(&s,"dcpdp-service-epic",class,1)==-ENODEV); + assert(dcpavserv_prepare(&s,name,"wrong",1)==-ENODEV); + assert(dcpavserv_prepare(&s,NULL,class,1)==-ENODEV); + assert(dcpavserv_prepare(&s,name,NULL,1)==-ENODEV); + av.endpoint=0x28; assert(dcpavserv_prepare(&s,name,class,1)==-ENODEV); av.endpoint=0x2a; + assert(dcpdpserv_prepare(&s,"dcpdp-service-epic",class,3)==-EOPNOTSUPP); + d.dcpavserv.enabled=true; assert(dcpavserv_prepare(&s,name,class,1)==-EBUSY); + d.dcpavserv.service=&s; d.dcpavserv.epoch=1; d.typec_generation=1; d.typec_cable_connected=true; + const struct drm_edid *e=dcpavserv_copy_edid(&d,1,&epoch); + assert(!IS_ERR(e) && s.users==1 && reads==1); drm_edid_free(e); + d.typec_cable_connected=false; + assert(PTR_ERR(dcpavserv_copy_edid(&d,1,&epoch))==-ESTALE && reads==1); + d.typec_cable_connected=true; + assert(PTR_ERR(dcpavserv_copy_edid(&d,0,&epoch))==-ESTALE && reads==1); + event=1; assert(PTR_ERR(dcpavserv_copy_edid(&d,1,&epoch))==-ESTALE && !allocations && s.users==1); + event=2; d.typec_cable_connected=true; + assert(PTR_ERR(dcpavserv_copy_edid(&d,2,&epoch))==-ESTALE && !allocations && s.users==1); + event=0; e=dcpavserv_copy_edid(&d,3,&epoch); assert(!IS_ERR(e)); drm_edid_free(e); + event=3; assert(PTR_ERR(dcpavserv_copy_edid(&d,3,&epoch))==-ENODEV && !allocations && !s.users); + assert(PTR_ERR(dcpavserv_copy_edid(&d,3,&epoch))==-ENODEV); + assert(connector.edid_invalidation_pending && connector.hotplug_wq && d.dcpavserv.epoch==2); + /* Same slot and physical generation: CLOSE/re-OPEN gets a fresh ticket. */ + s.enabled=true; assert(!dcpavserv_prepare(&s,name,class,1)); dcpavserv_init(&s,name,class,1); + assert(d.dcpavserv.service==&s && s.users==1 && d.dcpavserv.epoch==3); + mutex_lock(&d.dcpavserv.lock); + assert(!dcpavserv_edid_service_current(&d,1) && dcpavserv_edid_service_current(&d,3)); + mutex_unlock(&d.dcpavserv.lock); + event=0; e=dcpavserv_copy_edid(&d,3,&epoch); assert(!IS_ERR(e) && epoch==3); + /* Simulate retirement in the final validation-to-publication gap. */ + s.enabled=false; dcpavserv_teardown(&s); s.enabled=true; assert(!dcpavserv_prepare(&s,name,class,1)); dcpavserv_init(&s,name,class,1); + mutex_lock(&d.dcpavserv.lock); assert(!dcpavserv_edid_service_current(&d,epoch)); mutex_unlock(&d.dcpavserv.lock); + drm_edid_free(e); assert(d.typec_generation==3 && d.dcpavserv.epoch==5 && s.users==1); + s.enabled=false; dcpavserv_teardown(&s); + av.wire_v2=false; av.endpoint=0x28; d.hw.iomfb_method_profile=0; + d.dcpavserv.enabled=false; assert(!dcpavserv_prepare(&s,name,NULL,0)); + assert(dcpavserv_prepare(&s,name,class,1)==-ENODEV); + assert(!dcpdpserv_prepare(&s,NULL,NULL,3)); + d.dcpavserv.service=&s; d.dcpavserv.enabled=true; s.enabled=true; s.users=1; event=0; + d.typec_cable_connected=false; e=dcpavserv_copy_edid(&d,0,&epoch); + assert(!IS_ERR(e) && legacy_reads==1 && s.users==1); drm_edid_free(e); + d.hw.iomfb_method_profile=0;av.wire_v2=false;d.external_physically_absent=true; + mutex_lock(&d.hpd_mutex);assert(dcpavserv_edid_current(&d,0));mutex_unlock(&d.hpd_mutex); + assert(!allocations && !d.hpd_mutex && !d.dcpavserv.lock); + puts("PASS: production old/new lookup and receive, exact service admission before OPEN, delayed disconnect/replug, service detach pins, legacy transport"); +} +''' + return prefix + '\n'.join(parts) + suffix + +def rx_progress(root): + + def source(path): + return (root / path).read_text() + + def function(text, signature): + start = text.index(signature) + opening = text.index('{', start) + while ';' in text[start:opening]: + start = text.index(signature, start+1) + opening = text.index('{', start) + end, depth = opening+1, 1 + while depth: + depth += (text[end]=='{')-(text[end]=='}') + end += 1 + return text[start:end] + + dcp=source('drivers/gpu/drm/apple/dcp.c') + iomfb=source('drivers/gpu/drm/apple/iomfb.c') + af=source('drivers/gpu/drm/apple/afk.c') + rtkit=source('drivers/soc/apple/rtkit.c') + template=source('drivers/gpu/drm/apple/iomfb_template.c') + assert 'alloc_ordered_workqueue("rtkit-%s"' in rtkit + assert '.recv_message = dcp_recv_msg' in dcp + assert 'dcp_set_dimensions(dcp);' in template[template.index('"DisplayAttributes"'):] + assert 'queue_work(ep->wq, &work->work)' in af + prefix=r''' +#undef NDEBUG +#include +#include +#include +#include +#include +#include +#include +#include +#include +typedef uint8_t u8; +typedef uint32_t u32; +typedef uint64_t u64; +#define READ_ONCE(x) (x) +#define WRITE_ONCE(x,v) ((x)=(v)) +#define GFP_KERNEL 0 +#define kzalloc(n,f) calloc(1,n) +#define kfree(p) free(p) +#define container_of(p,t,m) ((t *)((char *)(p)-offsetof(t,m))) +#define INIT_WORK(p,f) ((p)->fn=(f)) +#define dev_warn(...) ((void)0) +#define DCP_FW_VER 26 +#define DCP_FW_VERSION(a,b,c) (a) +#define SZ_1M (1024*1024) +#define DCP_IOMFB_METHODS_H17P_EXTERNAL 2 +#define dev_warn_ratelimited(...) ((void)0) +#define dev_dbg_ratelimited(...) ((void)0) +#define trace_dcp_recv_msg(...) ((void)0) +#define DCP_FIRMWARE_H17P 2 +#define IOMFB_ENDPOINT 0x37 +#define AV_ENDPOINT 0x29 +#define SYSTEM_ENDPOINT 0x20 +#define DISP0_ENDPOINT 0x23 +#define DPTX_CONTROLLER_ENDPOINT 0x24 +#define APPLE_RTKIT_EP_MGMT 0 +#define APPLE_RTKIT_EP_CRASHLOG 1 +#define APPLE_RTKIT_EP_SYSLOG 2 +#define APPLE_RTKIT_EP_IOREPORT 4 +#define APPLE_RTKIT_EP_OSLOG 8 +#define APPLE_RTKIT_APP_ENDPOINT_START 0x20 +struct mutex { pthread_mutex_t native; }; +static _Thread_local struct mutex *locks[4]; +static _Thread_local unsigned int depth; +static bool held(struct mutex *m) { for(unsigned int i=0;inative)); locks[depth++]=m; } +static void mutex_unlock(struct mutex *m) { assert(depth && locks[depth-1]==m); depth--; assert(!pthread_mutex_unlock(&m->native)); } +#define lockdep_assert_held(p) assert(held(p)) +#define spin_lock_irqsave(p,f) do { mutex_lock(p); (f)=0; } while(0) +#define spin_unlock_irqrestore(p,f) do { (void)(f); mutex_unlock(p); } while(0) +struct work_struct { void (*fn)(struct work_struct *); }; +struct workqueue_struct { struct work_struct *pending; pthread_cond_t ready; pthread_mutex_t lock; }; +struct drm_device { struct { struct mutex mutex; } mode_config; }; +struct drm_connector { struct drm_device *dev; struct { int width_mm,height_mm; } display_info; }; +struct apple_connector { struct drm_connector base; struct work_struct hotplug_wq; }; +struct apple_dcp; +struct apple_dcp_afkep { struct apple_dcp *dcp; u8 endpoint; struct mutex lock; bool stopping; struct workqueue_struct *wq; }; +struct test_mode_ticket { u64 generation; bool associated; }; +struct apple_dcp { + bool quiescing; int fw_compat; void *dev; + struct { int iomfb_method_profile; } hw; + struct { struct mutex lock; u64 epoch; bool enabled; void *service; } dcpavserv; + struct test_mode_ticket dummy_ticket; + struct { u64 connection_sequence; } rate_proof; + struct { struct mutex lock; struct { u64 generation; bool connected; } epoch; } metadata; + u64 typec_generation; bool typec_cable_connected; + struct { size_t length; void *data; u64 av_service_epoch, automatic_connection_sequence; bool automatic_service_pending; struct test_mode_ticket mode_ticket; } chunks; + int width_mm,height_mm; struct { int width_mm,height_mm; } panel; + unsigned int nr_modes; struct { struct { int width_mm,height_mm; } mode; } modes[1]; + struct apple_connector *connector; + struct apple_dcp_afkep *dcpavservep,*dptxep,*avep,*systemep,*ibootep,*dptx_controller_ep; + struct { struct mutex lock; bool stopped; void *owner; } iomfb; +}; +struct afk_receive_message_work { struct apple_dcp_afkep *ep; u64 message; struct work_struct work; }; +struct apple_rtkit_ops { void (*recv_message)(void *,u8,u64); }; +struct apple_rtkit { bool shutting_down; const struct apple_rtkit_ops *ops; void *cookie,*dev; }; +struct apple_rtkit_rx_work { struct apple_rtkit *rtk; u8 ep; u64 msg; struct work_struct work; }; +#define current ((void *)1) +static pthread_mutex_t reply_lock=PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t reply_ready=PTHREAD_COND_INITIALIZER; +static bool reply_done; +static unsigned int advances, dimension_updates; +static struct workqueue_struct dimensions_queue={.ready=PTHREAD_COND_INITIALIZER,.lock=PTHREAD_MUTEX_INITIALIZER}; +static struct workqueue_struct afk_queue={.ready=PTHREAD_COND_INITIALIZER,.lock=PTHREAD_MUTEX_INITIALIZER}; +static void queue_work(struct workqueue_struct *q,struct work_struct *w) +{ assert(!pthread_mutex_lock(&q->lock)); assert(!q->pending); q->pending=w; pthread_cond_signal(&q->ready); assert(!pthread_mutex_unlock(&q->lock)); } +static void schedule_work(struct work_struct *w) { queue_work(&dimensions_queue,w); } +static void afk_receive_message_worker(struct work_struct *w) +{ + struct afk_receive_message_work *a=container_of(w,struct afk_receive_message_work,work); + assert(a->ep->endpoint==0x2a && a->message==7); + assert(!pthread_mutex_lock(&reply_lock)); reply_done=true; + pthread_cond_signal(&reply_ready); assert(!pthread_mutex_unlock(&reply_lock)); free(a); +} +static bool iomfb_uses_queue(struct apple_dcp *d) { (void)d; return true; } +static void iomfb_queue_advance(struct apple_dcp *d) { assert(held(&d->iomfb.lock)); advances++; } +static void dcp_set_dimensions(struct apple_dcp *d); +static u8 dcpep_cb_prop_start(struct apple_dcp *d,u32 *length); +static void dcp_firmware_metadata_begin(struct apple_dcp *d) { (void)d; } +#define dcp_metadata_begin(e) ((struct test_mode_ticket){.generation=(e)->generation,.associated=(e)->connected}) +static void iomfb_recv_message(struct apple_dcp *d,u64 message) +{ + assert(message==1 && held(&d->iomfb.lock)); dcp_set_dimensions(d); + u32 length=64; + assert(dcpep_cb_prop_start(d,&length)); +} +#define rx_unused(name) static void name(struct apple_rtkit *r,u64 m) { (void)r;(void)m; } +rx_unused(apple_rtkit_management_rx) +rx_unused(apple_rtkit_crashlog_rx) +rx_unused(apple_rtkit_syslog_rx) +rx_unused(apple_rtkit_ioreport_rx) +rx_unused(apple_rtkit_oslog_rx) +''' + parts=[function(dcp,'void dcp_set_dimensions('), function(template,'static u8 dcpep_cb_prop_start(')] + if 'void dcp_update_connector_dimensions(' in dcp: + parts.append(function(dcp,'void dcp_update_connector_dimensions(')) + else: + parts.append('static void dcp_update_connector_dimensions(struct apple_dcp *d, struct drm_connector *c) { (void)d; (void)c; }') + parts += [function(iomfb,'void iomfb_recv_msg('),function(af,'int afk_receive_message('), + function(dcp,'static struct apple_dcp_afkep *dcp_afkep('), + function(dcp,'static void dcp_recv_msg('),function(rtkit,'static void apple_rtkit_rx_work(')] + suffix=r''' +static struct drm_device drm={.mode_config.mutex.native=PTHREAD_MUTEX_INITIALIZER}; +static struct apple_connector connector={.base.dev=&drm}; +static struct apple_dcp d={.connector=&connector,.width_mm=600,.height_mm=340,.nr_modes=1,.fw_compat=DCP_FIRMWARE_H17P,.iomfb.lock.native=PTHREAD_MUTEX_INITIALIZER}; +static struct apple_dcp_afkep av={.dcp=&d,.endpoint=0x2a,.lock.native=PTHREAD_MUTEX_INITIALIZER,.wq=&afk_queue}; +static void dimension_work(struct work_struct *w) +{ + (void)w; + mutex_lock(&drm.mode_config.mutex); + dcp_update_connector_dimensions(&d,&connector.base); dimension_updates++; + mutex_unlock(&drm.mode_config.mutex); +} +static void *worker(void *arg) +{ + struct workqueue_struct *q=arg; + assert(!pthread_mutex_lock(&q->lock)); + while(!q->pending) assert(!pthread_cond_wait(&q->ready,&q->lock)); + struct work_struct *w=q->pending; q->pending=NULL; + assert(!pthread_mutex_unlock(&q->lock)); w->fn(w); return NULL; +} +static const struct apple_rtkit_ops ops={.recv_message=dcp_recv_msg}; +static struct apple_rtkit rtk={.ops=&ops,.cookie=&d}; +static void *ordered_rx(void *arg) +{ + (void)arg; + /* One ordered RX worker must complete DisplayAttributes before AFK. */ + struct apple_rtkit_rx_work *a=calloc(1,sizeof(*a)); assert(a); + a->rtk=&rtk; a->ep=IOMFB_ENDPOINT; a->msg=1; apple_rtkit_rx_work(&a->work); + a=calloc(1,sizeof(*a)); assert(a); + a->rtk=&rtk; a->ep=0x2a; a->msg=7; apple_rtkit_rx_work(&a->work); + return NULL; +} +int main(void) +{ + pthread_t rx,afk,dimensions; + (void)schedule_work; /* The negative baseline does not defer dimensions. */ + d.dcpavservep=&av; connector.hotplug_wq.fn=dimension_work; + d.hw.iomfb_method_profile=DCP_IOMFB_METHODS_H17P_EXTERNAL; + assert(!pthread_mutex_init(&d.dcpavserv.lock.native,NULL)); d.dcpavserv.epoch=1; + assert(!pthread_mutex_init(&d.metadata.lock.native,NULL)); d.metadata.epoch.generation=1; + d.metadata.epoch.connected=true; d.typec_generation=1; d.typec_cable_connected=true; + mutex_lock(&drm.mode_config.mutex); /* DRM get_modes holds this while waiting. */ + assert(!pthread_create(&afk,NULL,worker,&afk_queue)); + assert(!pthread_create(&dimensions,NULL,worker,&dimensions_queue)); + assert(!pthread_mutex_lock(&reply_lock)); + assert(!pthread_create(&rx,NULL,ordered_rx,NULL)); + struct timespec deadline; assert(!clock_gettime(CLOCK_REALTIME,&deadline)); deadline.tv_sec+=2; + int rc=0; + while(!reply_done && !rc) rc=pthread_cond_timedwait(&reply_ready,&reply_lock,&deadline); + if(!reply_done) { fputs("FAIL: ordered RTKit RX blocked before AFK reply while mode_config.mutex held\n",stderr); fflush(stderr); _Exit(1); } + assert(!pthread_mutex_unlock(&reply_lock)); + assert(!dimension_updates); /* Deferred worker cannot take the probe's mutex. */ + mutex_unlock(&drm.mode_config.mutex); + assert(!pthread_join(rx,NULL) && !pthread_join(afk,NULL) && !pthread_join(dimensions,NULL)); + assert(advances==1 && dimension_updates==1 && connector.base.display_info.width_mm==600 && connector.base.display_info.height_mm==340); + assert(d.modes[0].mode.width_mm==600 && d.modes[0].mode.height_mm==340); + assert(d.chunks.length==64 && d.chunks.data); free(d.chunks.data); + assert(!pthread_mutex_destroy(&d.dcpavserv.lock.native)); + assert(!pthread_mutex_destroy(&d.metadata.lock.native)); + puts("PASS: actual ordered RTKit RX -> synchronous IOMFB dimensions/property-start callbacks -> AFK enqueue makes reply progress while DRM mutex held; deferred dimensions update completes after release"); +} +''' + return prefix + '\n'.join(parts) + suffix diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/idle.py b/tools/testing/selftests/drivers/gpu/apple-dcp/idle.py new file mode 100644 index 0000000000000..7a0666fbe92ef --- /dev/null +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/idle.py @@ -0,0 +1,185 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright 2026 Ryan Murray +"""Actual cpuidle driver/registration/selector and tick suspension source replay.""" +import re +def function(text, name): + match=re.search(r'^((?:static\s+)?(?:inline\s+)?(?:__cpuidle\s+|noinstr\s+)?(?:int|void)\s+'+re.escape(name)+r'\([^;{}]*?\))\s*\{',text,re.M) + if not match: raise ValueError(name) + start=match.start(); pos=match.end(); depth=1 + while depth: + if text[pos]=='{': depth+=1 + elif text[pos]=='}': depth-=1 + pos+=1 + return text[start:pos]+'\n' + +MOCKS = r''' +#include +#include +#include +#include +#include +#include +typedef uint64_t u64; typedef int64_t s64; typedef int64_t ktime_t; +#define CONFIG_SUSPEND 1 +#define CPUIDLE_NAME_LEN 16 +#define CPUIDLE_DESC_LEN 32 +#define CPUIDLE_STATE_MAX 10 +#define BIT(n) (1U<<(n)) +#define NSEC_PER_USEC 1000 +#define __cpuidle +#define noinstr +#define THIS_MODULE NULL +#define SYSTEM_SUSPEND 1 +#define SYSTEM_RUNNING 0 +#define TPS(x) x +#define instrumentation_begin() ((void)0) +#define instrumentation_end() ((void)0) +#define raw_spin_lock(x) ((void)0) +#define raw_spin_unlock(x) ((void)0) +#define lock_map_acquire_try(x) ((void)0) +#define lock_map_release(x) ((void)0) +#define trace_suspend_resume(...) ((void)0) +#define WARN_ON(x) ((x)?(warnings++,1):0) +#define WARN_ON_ONCE(x) WARN_ON(x) +#define pr_err(...) ((void)0) +#define for_each_cpu(cpu,mask) for(cpu=0;cpu<(mask)->count;cpu++) +#define per_cpu(v,cpu) v[cpu] +struct platform_device {int dummy;}; +struct module; struct cpumask {int count;}; +struct cpuidle_device; struct cpuidle_driver; +struct cpuidle_device {int cpu; struct cpuidle_state_usage states_usage[10];}; +static struct cpumask possible={6}; static const struct cpumask *cpu_possible_mask=&possible; +static struct cpuidle_device cpuidle_dev[6]; +static int current, irq[6], warnings, wfi, deep, pm, pm_fail, reg_fail=-1, drv_fail; +static int registered[6], unregistered, rcu[6], frozen, stopped[6], online=6; +static int tick_freeze_depth, tick_freeze_lock, tick_freeze_map, system_state; +static int tk_suspend, tk_resume, local_suspend, local_resume, sched_suspend, sched_resume; +static int fanout, faulty_irq, checks; +static uint64_t clock_ns; +static void check(bool b) {checks++; assert(b);} +static int num_online_cpus(void){return online;} +static void sched_clock_suspend(void){sched_suspend++;} +static void sched_clock_resume(void){sched_resume++;} +static void timekeeping_suspend(void){check(tick_freeze_depth==online);tk_suspend++;frozen=1;} +static void timekeeping_resume(void){check(frozen);tk_resume++;frozen=0;} +static void tick_suspend_local(void){local_suspend++;} +static void tick_resume_local(void){local_resume++;} +static void touch_softlockup_watchdog(void){} +static void ct_cpuidle_enter(void){check(irq[current]);rcu[current]++;} +static void ct_cpuidle_exit(void){check(irq[current]);rcu[current]--;} +static bool irqs_disabled(void){return irq[current];} +static void raw_local_irq_disable(void){irq[current]=1;} +static void local_irq_enable(void){check(!frozen);irq[current]=0;} +static void stop_critical_timings(void){stopped[current]++;} +static void start_critical_timings(void){stopped[current]--;} +static ktime_t local_clock_noinstr(void){return clock_ns+=1000;} +static ktime_t ns_to_ktime(ktime_t t){return t;} +static ktime_t ktime_us_delta(ktime_t a,ktime_t b){return (a-b)/1000;} +static u64 div_u64(u64 a,u64 b){return a/b;} +static u64 mul_u32_u32(unsigned a,unsigned b){return (u64)a*b;} +static bool t8140; +static bool of_machine_is_compatible(const char *s){return t8140&&!strcmp(s,"apple,t8140");} +static int cpu_pm_enter(void){pm++;return pm_fail;} +static void cpu_pm_exit(void){pm--;} +static void apple_cpu_deep_wfi(void){deep++;} +static void cpu_do_idle(void); +static int cpuidle_register_driver(struct cpuidle_driver *drv); +static int cpuidle_register_device(struct cpuidle_device *dev){if(dev->cpu==reg_fail)return -5; registered[dev->cpu]=1;return 0;} +static void cpuidle_unregister(struct cpuidle_driver *drv){(void)drv;memset(registered,0,sizeof(registered));unregistered++;} +''' +MAIN = r''' +static int cpuidle_register_driver(struct cpuidle_driver *drv){if(drv_fail)return -5;__cpuidle_driver_init(drv);return 0;} +static void cpu_do_idle(void){ + check(irq[current]);wfi++; + if(fanout && current<5){int previous=current;current++;irq[current]=1; + cpuidle_enter_s2idle(&apple_idle_driver,&cpuidle_dev[current],100000);current=previous;} + if(faulty_irq)irq[current]=0; +} +static struct cpuidle_driver original; +static void reset(bool is_t8140){ + apple_idle_driver=original;t8140=is_t8140; current=0; + memset(cpuidle_dev,0,sizeof(cpuidle_dev)); memset(registered,0,sizeof(registered)); + for(int i=0;i<6;i++){irq[i]=1;rcu[i]=stopped[i]=0;} + wfi=deep=pm=pm_fail=warnings=unregistered=0;reg_fail=-1;drv_fail=0; + tick_freeze_depth=frozen=tk_suspend=tk_resume=local_suspend=local_resume=sched_suspend=sched_resume=0; + fanout=faulty_irq=0; +} +int main(void){ + original=apple_idle_driver; + reset(true);check(!apple_cpuidle_probe(NULL)); + check(apple_idle_driver.cpumask==cpu_possible_mask);check(!apple_idle_driver.bctimer); + for(int i=0;i<6;i++)check(registered[i]&&cpuidle_dev[i].cpu==i); + int expected=EXPECT_FIXED?1:0; + check(apple_idle_driver.state_count==(EXPECT_FIXED?2:1)); + check(find_deepest_state(&apple_idle_driver,&cpuidle_dev[0],100000,0,true)==expected); + check(cpuidle_enter_s2idle(&apple_idle_driver,&cpuidle_dev[0],100000)==expected); + check(tick_freeze_depth==0&&!frozen);check(deep==0&&pm==0); + check(local_suspend==expected&&local_resume==expected); + if(EXPECT_FIXED){ + check(apple_idle_driver.states[1].enter==apple_enter_wfi); + check(apple_idle_driver.states[1].enter_s2idle==apple_enter_wfi); + check(apple_idle_driver.states[1].flags==0); + check(apple_idle_driver.states[1].exit_latency_ns==1000); + check(apple_idle_driver.states[1].target_residency_ns==1000); + check(apple_idle_driver.states[1].power_usage==apple_idle_driver.states[0].power_usage); + reset(true);apple_cpuidle_probe(NULL);fanout=1; + check(cpuidle_enter_s2idle(&apple_idle_driver,&cpuidle_dev[0],100000)==1); + check(wfi==6&&deep==0&&pm==0);check(tk_suspend==1&&tk_resume==1); + check(sched_suspend==1&&sched_resume==1&&local_suspend==5&&local_resume==5); + check(tick_freeze_depth==0&&!frozen); + for(int i=0;i<6;i++)check(cpuidle_dev[i].states_usage[1].s2idle_usage==1&&rcu[i]==0&&stopped[i]==0); + reset(true);apple_cpuidle_probe(NULL);fanout=1;cpuidle_dev[5].states_usage[1].disable=1; + cpuidle_enter_s2idle(&apple_idle_driver,&cpuidle_dev[0],100000); + check(wfi==5&&tk_suspend==0&&tk_resume==0&&tick_freeze_depth==0); + reset(true);apple_cpuidle_probe(NULL);cpuidle_dev[0].states_usage[1].disable=1; + check(cpuidle_enter_s2idle(&apple_idle_driver,&cpuidle_dev[0],100000)==0&&wfi==0); + cpuidle_dev[0].states_usage[1].disable=0;apple_idle_driver.states[1].enter_s2idle=NULL; + check(cpuidle_enter_s2idle(&apple_idle_driver,&cpuidle_dev[0],100000)==0&&wfi==0); + reset(true);apple_cpuidle_probe(NULL); + check(cpuidle_enter_s2idle(&apple_idle_driver,&cpuidle_dev[0],999)==0); + check(cpuidle_enter_s2idle(&apple_idle_driver,&cpuidle_dev[0],1000)==1); + reset(true);apple_cpuidle_probe(NULL);apple_idle_driver.states[1].exit_latency_ns=0; + check(find_deepest_state(&apple_idle_driver,&cpuidle_dev[0],100000,0,true)==0); + reset(true);apple_cpuidle_probe(NULL);apple_idle_driver.states[1].flags=CPUIDLE_FLAG_TIMER_STOP; + check(find_deepest_state(&apple_idle_driver,&cpuidle_dev[0],100000,CPUIDLE_FLAG_TIMER_STOP,false)==0); + reset(true);apple_cpuidle_probe(NULL);faulty_irq=1; + check(cpuidle_enter_s2idle(&apple_idle_driver,&cpuidle_dev[0],100000)==1); + check(warnings==1&&tick_freeze_depth==0&&rcu[0]==0&&stopped[0]==0); + } + reset(true);apple_cpuidle_probe(NULL); + for(int i=0;i', '') + core = 'drivers/usb/typec/tipd/core.c' + dcp = 'drivers/gpu/drm/apple/dcp.c' + funcs = '\n'.join(function(f, n) for f, n in ( + ('drivers/gpu/drm/apple/iomfb.c', 'iomfb_pause_presentation'), + (dcp, 'dcp_external_report_disconnected'), + (dcp, 'dcp_external_publish_generation'), + (dcp, 'dcp_external_stop_locked'), + (dcp, 'dcp_external_disconnect'), + (dcp, 'dcp_external_stop_work'), + (dcp, 'dcp_external_hotplug'), + (dcp, 'dcp_external_link_locked'), + (dcp, 'dcp_dptx_connect_oob'), + (core, 'cd321x_retry_revalidation'), + (core, 'cd321x_update_work'))) + prefix = r''' +#include +#include +#include +#include +#include +#include +#include +typedef uint32_t u32; typedef uint64_t u64; +#define guard(x) fake_guard +#define fake_guard(x) ((void)(x)) +#define scoped_guard(x,y) for (int once = ((void)(y), 1); once; once = 0) +#define lockdep_assert_held(x) ((void)(x)) +#define READ_ONCE(x) (x) +#define WRITE_ONCE(x,v) ((x) = (v)) +#define container_of(p,t,m) ((t *)((char *)(p) - offsetof(t,m))) +#define to_delayed_work(w) container_of(w,struct delayed_work,work) +#define msecs_to_jiffies(x) (x) +#define IS_REACHABLE(x) 1 +#define CONFIG_DRM 1 +#define IS_ERR(x) false +#define PTR_ERR(x) (-EINVAL) +#define dev_warn(...) ((void)0) +#define dev_err(...) ((void)0) +#define CD321X_DEBOUNCE_DELAY_MS 500 +#define TPS_STATUS_PLUG_PRESENT (1U << 0) +#define TPS_STATUS_PLUG_UPSIDE_DOWN (1U << 1) +#define TPS_DATA_STATUS_USB2_CONNECTION (1U << 0) +#define TPS_DATA_STATUS_USB3_CONNECTION (1U << 1) +#define TPS_DATA_STATUS_TBT_CONNECTION (1U << 2) +#define TPS_DATA_STATUS_DP_CONNECTION (1U << 3) +#define TPS_DATA_STATUS_DP_PIN_ASSIGNMENT_MASK (3U << 4) +#define TPS_DATA_STATUS_USB_DATA_ROLE (1U << 6) +#define CD321X_DATA_STATUS_USB4_CONNECTION (1U << 7) +#define CD321X_DATA_STATUS_HPD_LEVEL (1U << 8) +#define TPS_POWER_STATUS_PWROPMODE(x) (x) +#define TPS_STATUS_TO_UPSIDE_DOWN(x) ((x)&TPS_STATUS_PLUG_UPSIDE_DOWN) +#define TPS_STATUS_TO_TYPEC_PORTROLE(x) 0 +#define TPS_STATUS_TO_TYPEC_VCONN(x) 0 +#define TPS_STATUS_TO_TYPEC_DATAROLE(x) 0 +#define TYPEC_STATE_SAFE 0 +#define TYPEC_STATE_USB 1 +#define TYPEC_THUNDERBOLT_SWITCH_OFF 0 +#define TYPEC_ACCESSORY_NONE 0 +#define DCP_HOTPLUG_NOTIFY 1 +struct work_struct { bool pending; }; +struct delayed_work { struct work_struct work; }; +static int system_freezable_wq; +static int schedule_work(struct work_struct *w) { w->pending = true; return 1; } +static void cancel_work_sync(struct work_struct *w) { w->pending = false; } +static void cancel_delayed_work_sync(struct delayed_work *w) { w->work.pending = false; } +#define cancel_delayed_work cancel_delayed_work_sync +static void mod_delayed_work(int q, struct delayed_work *w, int t) +{ (void)q; (void)t; w->work.pending = true; } +struct apple_connector { bool connected, edid_invalidation_pending; }; +struct apple_dcp { + int external_lock, hpd_mutex; + struct { int lock; bool presentation_paused; } iomfb; + struct { bool connected; void *service; } dptxport[1]; + struct apple_connector *connector; + struct delayed_work typec_reconnect_wq, placeholder_edid_wq; + struct work_struct external_stop_work; + bool external_running, external_stopped, external_power_attempted; + bool external_cable, typec_cable_connected, typec_crtc_off, pipe_enabled_h17p; + bool external_physically_absent, external_prepared, placeholder_retried; + int typec_reconnect_tries; + bool external_hotplug_pending, external_hotplug_connected, quiescing; + u64 external_generation, external_rx_generation, external_crtc_generation; + u64 external_stop_generation, typec_generation; + int external_error, mode_state; void *avep; +}; +struct platform_device { struct apple_dcp *data; }; +static struct apple_dcp *platform_get_drvdata(struct platform_device *p) { return p->data; } +static int raw_error, raw_links; +static int dcp_dptx_connect_raw(struct apple_dcp *d, u32 port, bool *waiting) +{ assert(port == 0); *waiting = false; raw_links++; + if (!raw_error) d->dptxport[0].connected = true; + return raw_error; } +static bool dcp_is_typec_output(struct apple_dcp *d) { (void)d; return true; } +static int dcp_dptx_connect(struct apple_dcp *d, u32 port) { (void)d; (void)port; return 0; } +#define DPTX_RECONNECT_DELAY 500 +static int settle_error, power_error, hpd_error, release_error; +static int settles, powers, releases, detaches, notices; +static bool dcp_is_external_h17p(struct apple_dcp *d) { (void)d; return true; } +static unsigned dcp_mode_hotplug(int *mode, bool connected, bool *sink) +{ (void)mode; if (sink) *sink = connected; return 1; } +static void dcp_handle_hotplug_actions(struct apple_dcp *d, unsigned act) +{ (void)d; notices += !!act; } +static int iomfb_queue_settle(struct apple_dcp *d, int t) +{ (void)d; (void)t; settles++; return settle_error; } +static int iomfb_external_power_h17p(struct apple_dcp *d, bool on) +{ (void)d; assert(!on); powers++; return power_error; } +static bool dptx_service_available(void *p) { (void)p; return true; } +static int dptxport_set_hpd(void *p, bool on) +{ (void)p; assert(!on); return hpd_error; } +static int dptxport_release_display(void *p) +{ (void)p; releases++; return release_error; } +static void av_service_disconnect(struct apple_dcp *d) { (void)d; } +static void dpavservep_detach(struct apple_dcp *d) { (void)d; detaches++; } +static void dcp_firmware_metadata_hotplug(struct apple_dcp *d, bool c) { (void)d; (void)c; } +static void dcp_firmware_metadata_connection(struct apple_dcp *d, u64 g, bool c) +{ (void)d; (void)g; (void)c; } +static int dcp_external_disconnect(struct apple_dcp *, bool); +static struct apple_dcp display; +static void drm_connector_oob_hotplug_event(void *node, int status) +{ assert(node); if (!status) dcp_external_disconnect(&display, true); } +#define connector_status_disconnected 0 +#define connector_status_connected 1 +''' + shim = r''' +enum usb_role { USB_ROLE_NONE, USB_ROLE_HOST, USB_ROLE_DEVICE }; +enum typec_pwr_opmode { TYPEC_PWR_MODE_USB, TYPEC_PWR_MODE_PD }; +enum typec_orientation { TYPEC_ORIENTATION_NONE, TYPEC_ORIENTATION_NORMAL, TYPEC_ORIENTATION_REVERSE }; +struct usb_pd_identity { int id; }; +struct typec_mux_state { void *alt, *data; int mode; }; +struct typec_thunderbolt_switch_data { int state; }; +struct typec_partner_desc { bool usb_pd; int accessory; struct usb_pd_identity *identity; }; +struct cd321x_status { u32 status, data_status, status_changed, data_status_changed, pwr_status; + struct usb_pd_identity partner_identity; }; +struct tps6598x { void *partner, *dev, *port, *psy; int lock; u32 data_status; enum usb_role *role_sw; }; +struct cd321x { struct tps6598x tps; struct cd321x_status update_status; + struct cd321x_pm_state pm; struct delayed_work update_work, resume_work; + struct typec_mux_state state; bool state_valid, display_route_active, cur_partner_is_pd; + struct usb_pd_identity cur_partner_identity; void *connector_fwnode, *tbt_switch; }; +static int tbt_error, role_error, safe_error, mode_error; +static int tbts, roles, safes; +static enum usb_role usb_role_switch_get_role(enum usb_role *r) { return *r; } +static int usb_role_switch_set_role(enum usb_role *r, enum usb_role n) +{ roles++; if (role_error) return role_error; *r = n; return 0; } +static int typec_thunderbolt_switch_set(void *p, void *s) +{ (void)p; (void)s; tbts++; return tbt_error; } +static int typec_set_mode(void *p, int s) +{ (void)p; assert(s == TYPEC_STATE_SAFE); safes++; return safe_error; } +static int cd321x_typec_update_mode(struct tps6598x *t, struct cd321x_status *s, bool *busy) +{ (void)t; (void)s; *busy = mode_error == -EBUSY; return mode_error; } +static void typec_unregister_partner(void *p) { (void)p; } +static void *typec_register_partner(void *p, struct typec_partner_desc *d) +{ (void)p; (void)d; return (void *)1; } +static void typec_partner_set_identity(void *p) { (void)p; } +#define typec_set_pwr_opmode(...) ((void)0) +#define typec_set_pwr_role(...) ((void)0) +#define typec_set_vconn_role(...) ((void)0) +#define typec_set_orientation(...) ((void)0) +#define typec_set_data_role(...) ((void)0) +#define power_supply_changed(...) ((void)0) +''' + tests = r''' +static int failures, checks; +#define CHECK(x) do { checks++; if (!(x)) { printf("FAIL line %d: %s\n", __LINE__, #x); failures++; } } while (0) +static struct apple_connector sink; +static enum usb_role role; +static struct cd321x port; +static void init(void) +{ + memset(&display,0,sizeof(display)); memset(&sink,0,sizeof(sink)); memset(&port,0,sizeof(port)); + sink.connected = true; display.connector = &sink; display.external_prepared = true; + raw_error = raw_links = 0; + display.external_running = display.external_power_attempted = true; + display.external_cable = display.typec_cable_connected = true; + display.dptxport[0].connected = display.pipe_enabled_h17p = true; + display.external_generation = display.external_rx_generation = display.external_crtc_generation = 7; + display.typec_generation = 4; + port.tps.partner = (void *)1; port.tps.role_sw = &role; role = USB_ROLE_HOST; + port.pm.phase = CD321X_PM_RUNNING; port.state_valid = true; port.state.mode = 2; + port.display_route_active = true; port.connector_fwnode = (void *)1; + settle_error = power_error = hpd_error = release_error = 0; + tbt_error = role_error = safe_error = mode_error = 0; + settles = powers = releases = detaches = notices = tbts = roles = safes = 0; +} +static void callback(bool connected) +{ + if (!dcp_external_hotplug(&display,connected)) + dcp_mode_hotplug(&display.mode_state,connected,&sink.connected); +} +int main(void) +{ + /* Unplug at each fallible transition with failing DCP power teardown. */ + for (int which = 0; which < 3; which++) { + init(); power_error = -ETIMEDOUT; + if (which == 0) tbt_error = -ETIMEDOUT; + if (which == 1) role_error = -EIO; + if (which == 2) safe_error = -ETIMEDOUT; + cd321x_update_work(&port.update_work.work); + CHECK(!sink.connected); CHECK(!display.external_cable); + CHECK(display.external_error == -ETIMEDOUT); + CHECK(display.dptxport[0].connected && display.pipe_enabled_h17p); + CHECK(!display.external_stopped && !display.typec_crtc_off); + CHECK(port.display_route_active && !port.state_valid); + CHECK(port.pm.phase == CD321X_PM_REVALIDATE && port.pm.attempts_left == CD321X_RESUME_ATTEMPTS); + CHECK(notices > 0 && releases == 0); + callback(true); CHECK(!sink.connected); + printf("case transition-%d complete\n",which); + } + /* A rejected/nonowner port cannot disconnect the shared owner's sink. */ + for (int which = 0; which < 3; which++) { + init(); port.display_route_active = false; + if (which == 0) tbt_error = -EIO; + if (which == 1) role_error = -EIO; + if (which == 2) safe_error = -EIO; + cd321x_update_work(&port.update_work.work); + CHECK(sink.connected && display.external_cable && !notices); + } + init(); port.display_route_active = false; role = USB_ROLE_NONE; + port.update_status.status = TPS_STATUS_PLUG_PRESENT; + port.update_status.data_status = TPS_DATA_STATUS_DP_CONNECTION | CD321X_DATA_STATUS_HPD_LEVEL; + mode_error = -EBUSY; cd321x_update_work(&port.update_work.work); + CHECK(sink.connected && !notices && !port.display_route_active); + /* Two ports share the connector: busy-port unplug leaves its owner live. */ + init(); + struct cd321x second = port; + enum usb_role second_role = USB_ROLE_NONE; + second.tps.role_sw = &second_role; second.display_route_active = false; + second.update_status.status = TPS_STATUS_PLUG_PRESENT; + second.update_status.data_status = TPS_DATA_STATUS_DP_CONNECTION | CD321X_DATA_STATUS_HPD_LEVEL; + mode_error = -EBUSY; cd321x_update_work(&second.update_work.work); + CHECK(!second.display_route_active && sink.connected && !notices); + second.update_status.status = second.update_status.data_status = 0; + tbt_error = -EIO; cd321x_update_work(&second.update_work.work); + CHECK(port.display_route_active && sink.connected && !notices); + power_error = -EIO; cd321x_update_work(&port.update_work.work); + CHECK(!sink.connected && port.display_route_active); + CHECK(display.dptxport[0].connected && display.pipe_enabled_h17p); + /* Revalidation is bounded even though the failed hardware remains owned. */ + init(); tbt_error = -EIO; + cd321x_update_work(&port.update_work.work); + for (int i = 0; i < CD321X_RESUME_ATTEMPTS; i++) { + CHECK(cd321x_pm_begin_read(&port.pm)); cd321x_pm_snapshot_ready(&port.pm); + cd321x_update_work(&port.update_work.work); + } + CHECK(port.pm.phase == CD321X_PM_STALE && !port.pm.attempts_left); + CHECK(!sink.connected && port.display_route_active); + /* Stop failure stages preserve all state whose shutdown was not acked. */ + for (int which = 0; which < 4; which++) { + init(); + if (which == 0) settle_error = -EIO; + if (which == 1) power_error = -EIO; + if (which == 2) hpd_error = -EIO; + if (which == 3) release_error = -EIO; + CHECK(dcp_external_disconnect(&display,true) == -EIO); + CHECK(!sink.connected && !display.typec_cable_connected); + CHECK(display.external_generation == 8 && display.external_rx_generation == 8); + CHECK(display.external_error == -EIO && display.dptxport[0].connected); + CHECK(!display.external_stopped && !display.typec_crtc_off); + CHECK(display.pipe_enabled_h17p == (which < 2)); + callback(true); CHECK(!sink.connected); + } + /* An existing error returns before pause: late RX still cannot resurrect. */ + init(); display.external_error = -EIO; + CHECK(dcp_external_disconnect(&display,true) == -EIO); + CHECK(!display.iomfb.presentation_paused); + callback(true); CHECK(!sink.connected); + CHECK(display.dptxport[0].connected && display.pipe_enabled_h17p); + /* A queued callback from the old sink cannot be replayed after unplug. */ + init(); display.external_error = -EIO; + display.iomfb.presentation_paused = true; callback(true); + CHECK(display.external_hotplug_pending); + CHECK(dcp_external_disconnect(&display,true) == -EIO); + iomfb_pause_presentation(&display,false); CHECK(!sink.connected); + /* An old stop generation cannot touch a later session. */ + init(); display.external_stop_generation = 6; + dcp_external_stop_work(&display.external_stop_work); + CHECK(sink.connected && settles == 0 && powers == 0 && releases == 0); + CHECK(dcp_external_stop_locked(&display,6) == 0 && sink.connected); + /* Same-cable CRTC off remains distinct from physical absence. */ + init(); CHECK(dcp_external_disconnect(&display,false) == 0); + CHECK(display.external_cable && display.typec_cable_connected); + CHECK(display.external_generation == 7); + /* Genuine discovery and the paused DPMS generation gate still work. */ + init(); sink.connected = false; callback(true); CHECK(sink.connected); + init(); sink.connected = false; display.iomfb.presentation_paused = true; + display.external_power_attempted = false; callback(true); CHECK(sink.connected); + init(); sink.connected = false; display.iomfb.presentation_paused = true; + display.external_stopped = true; display.external_rx_generation = 8; + callback(true); CHECK(sink.connected); + init(); sink.connected = false; display.iomfb.presentation_paused = true; + display.external_stopped = true; callback(true); CHECK(!sink.connected); + init(); sink.connected = false; display.iomfb.presentation_paused = true; + display.external_error = -EIO; callback(true); CHECK(!sink.connected); + /* Fresh firmware discovery precedes OOB: no second RX is required. */ + init(); sink.connected = false; + display.external_cable = display.typec_cable_connected = false; + display.external_generation = display.external_rx_generation = 37; + display.external_power_attempted = false; display.external_stopped = true; + display.dptxport[0].connected = false; + callback(true); CHECK(sink.connected); CHECK(!display.external_physically_absent); + struct platform_device device = { .data = &display }; + CHECK(dcp_dptx_connect_oob(&device,0) == 0); + CHECK(raw_links == 1 && sink.connected); + CHECK(display.external_generation == 38); + /* Initially paused discovery keeps the original pending-replay behavior. */ + init(); sink.connected = false; display.external_cable = false; + display.external_power_attempted = false; display.iomfb.presentation_paused = true; + callback(true); CHECK(display.external_hotplug_pending); CHECK(!sink.connected); + CHECK(dcp_dptx_connect_oob(&device,0) == 0 && raw_links == 1); + iomfb_pause_presentation(&display,false); CHECK(sink.connected); + /* Physical absence consumes late RX and clears an already queued replay. */ + init(); display.iomfb.presentation_paused = true; callback(true); + CHECK(display.external_hotplug_pending); + CHECK(dcp_external_disconnect(&display,true) == 0); + CHECK(display.external_physically_absent && !display.external_hotplug_pending); + callback(true); CHECK(!sink.connected && !display.external_hotplug_pending); + /* A real next OOB connect removes the tombstone and permits discovery. */ + CHECK(dcp_dptx_connect_oob(&device,0) == 0); + CHECK(!display.external_physically_absent && display.external_cable); + CHECK(display.external_generation == 9 && raw_links == 1); + callback(true); CHECK(sink.connected); + /* Same-cable DPMS does not create a physical-absence tombstone. */ + init(); CHECK(dcp_external_disconnect(&display,false) == 0); + CHECK(!display.external_physically_absent && display.external_cable); + sink.connected = false; callback(true); CHECK(!sink.connected); + CHECK(display.external_hotplug_pending); + iomfb_pause_presentation(&display,false); CHECK(sink.connected); + /* A failed hardware stop remains owned, even after a real OOB reconnect. */ + init(); display.external_error = -EIO; + CHECK(dcp_external_disconnect(&display,true) == -EIO); + CHECK(display.external_physically_absent); callback(true); CHECK(!sink.connected); + CHECK(dcp_dptx_connect_oob(&device,0) == -EIO); + CHECK(!display.external_physically_absent && display.external_error == -EIO); + CHECK(raw_links == 0 && display.dptxport[0].connected && display.pipe_enabled_h17p); + /* Successful physical teardown may release ownership normally. */ + init(); CHECK(dcp_external_disconnect(&display,true) == 0); + CHECK(!sink.connected && !display.dptxport[0].connected); + CHECK(display.external_stopped && display.typec_crtc_off && !display.pipe_enabled_h17p); + printf("%d assertions, %d failures\n", checks, failures); return !!failures; +} +''' + source = prefix + pm + shim + funcs + tests + return source + +def teardown(root): + + files = {n: (root / f).read_text() for n, f in { + 'atc': 'drivers/phy/apple/atc.c', + 'dp': 'drivers/phy/apple/atc-j700-dp.h', + 'phy': 'drivers/phy/phy-core.c', + 'core': 'drivers/usb/dwc3/core.c', + 'apple': 'drivers/usb/dwc3/dwc3-apple.c', + 'state': 'drivers/usb/dwc3/dwc3-apple-state.h', + }.items()} + + + def fn(n, name): + s = files[n] + normalized = n == 'atc' and name == 'atcphy_mux_set' and 'static int atcphy_mux_set_locked(' in s + if normalized: + name = 'atcphy_mux_set_locked' + m = re.search(r'^(?:static [^\n]*\b|(?:int|void) )' + name + r'\(', s, re.M) + assert m, (n, name) + result = block(s, m.start()) + return result.replace('atcphy_mux_set_locked(', 'atcphy_mux_set(', 1) if normalized else result + + decls = [] + for name in ('atcphy_generation', 'atcphy_lane_mode', 'atcphy_pipehandler_state', 'atcphy_mode', 'atcphy_dp_link_rate'): + decls.append(block(files['atc'], files['atc'].index('enum ' + name + ' {')) + ';') + for name in ('atcphy_mode_configuration', 'atcphy_hw', 'atcphy_dp_link_rate_configuration'): + decls.append(block(files['atc'], files['atc'].index('struct ' + name + ' {')) + ';') + decls.append(block(files['state'], files['state'].index('enum dwc3_apple_state {')) + ';') + start = files['atc'].index('static const struct {\n\tconst struct atcphy_mode_configuration normal;') + end = files['atc'].index('\n};', start) + 3 + decls.append(files['atc'][start:end]) + start = files['atc'].index('static const struct atcphy_dp_link_rate_configuration dp_lr_config[]') + end = files['atc'].index('\n};', start) + 3 + decls.append(files['atc'][start:end]) + + names = { + 'atc': ('atcphy_pipehandler_lock', 'atcphy_pipehandler_unlock', + 'atcphy_pipehandler_check', 'atcphy_pipehandler_set_mux', + 'atcphy_clear_nonselected_phy_reset', + 'atcphy_configure_pipehandler_usb3_host_t8103', + 'atcphy_configure_pipehandler_usb3_host_t8122', + 'atcphy_configure_pipehandler_usb3', 'atcphy_configure_pipehandler_usb4', + 'atcphy_enable_dummy_phy', 'atcphy_configure_pipehandler_dummy', + 'atcphy_configure_pipehandler', 'atcphy_park_pipehandler', + 'atcphy_get_mode_config', 'atcphy_dp_configure', + 'j700_field_update', 'j700_aux_disable', 'atcphy_disable_dp_aux', + 'atcphy_power_off', 'atcphy_power_on', 'atcphy_configure', 'atcphy_usb2_init', 'atcphy_usb3_power_off', + 'atcphy_usb3_set_mode', 'atcphy_usb2_power_off', + '_atcphy_dwc3_reset_assert', 'atcphy_dwc3_reset_assert', 'atcphy_mux_set'), + 'phy': ('phy_init', 'phy_exit', 'phy_power_on', 'phy_power_off'), + 'core': ('dwc3_phy_power_on', 'dwc3_phy_power_off', 'dwc3_phy_exit', 'dwc3_core_exit'), + 'apple': ('dwc3_apple_core_stop', 'dwc3_apple_core_init', 'dwc3_apple_core_start', + 'dwc3_apple_init', 'dwc3_apple_exit', 'dwc3_usb_role_switch_set'), + } + names['core'] = ('dwc3_phy_power_on', 'dwc3_phy_power_off', 'dwc3_phy_exit', + 'dwc3_core_exit_after_phy_power_off', 'dwc3_core_exit', + 'dwc3_core_remove_common', 'dwc3_core_remove', 'dwc3_core_remove_no_shutdown') + names['atc'] += ('atcphy_dwc3_reset_deassert',) + names['atc'] = tuple(name for name in names['atc'] if name not in ('atcphy_get_mode_config', 'j700_field_update')) + names = {n: tuple(name for name in group if re.search(r'^(?:static [^\n]*\b|(?:int|void) )' + name + r'\(', files[n], re.M)) + for n, group in names.items()} + context = (fn('atc', 'core_read32') + '\n' + fn('atc', 'atcphy_get_mode_config') + '\n' + fn('atc', 'j700_field_update') + '\n' + files['dp'][files['dp'].index('/* Masks are the union'):files['dp'].index('/* Retry only the idle operations')]) + functions = '\n\n'.join(fn(n, name) for n, group in names.items() for name in group) + definitions = [] + lines = iter(files['atc'].splitlines()) + for line in lines: + if not line.startswith('#define '): + continue + definition = [line] + while definition[-1].endswith('\\'): + definition.append(next(lines)) + definitions.append('\n'.join(definition)) + definitions = '\n'.join(definitions) + here = Path(__file__).resolve().parent + fallback = '' if 'dwc3_core_remove_no_shutdown' in names['core'] else '#define dwc3_core_remove_no_shutdown dwc3_core_remove\n' + unit = ((here / 'dwc3/prefix.c').read_text() + '\n' + definitions + '\n' + + '\n'.join(decls) + '\n' + (here / 'dwc3/mocks.c').read_text() + '\n' + + context + '\n' + functions + '\n' + fallback + (here / 'dwc3/cases.c').read_text()) + return unit + +def profile(root): + + base = root / "drivers/gpu/drm/apple" + source = (base / "dcp.c").read_text() + + + def block(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] + + + clock = source[source.index("static const struct resource t8140_firmware_clock"): + source.index("static const struct apple_dcp_hw_data apple_dcp_hw_t8140 =")] + profiles = clock + "\n" + "\n".join(block(marker) + ";" for marker in ( + "static const struct apple_dcp_hw_data apple_dcp_hw_t8140 =", + "static const struct apple_dcp_hw_data apple_dcp_hw_t8140_external =", + )) + admission = block("static int dcp_get_disp_regs(") + headers = "\n".join(line for name in ("dcp-external-bw.h", "dcp-link.h") + for line in (base / name).read_text().splitlines() + if not line.startswith("#include")) + prefix = r''' +#include +#include +#include +#include +#include +#include +typedef uint32_t u32; typedef uint64_t u64; +typedef uint32_t __le32; typedef uint64_t __le64; +#define __packed __attribute__((packed)) +#undef static_assert +#define static_assert(condition, ...) _Static_assert(condition, "wire layout") +#define le32_to_cpu(v) (v) +#define cpu_to_le32(v) (v) +#define cpu_to_le64(v) (v) +#define IORESOURCE_MEM 1 +#define MAX_DISP_REGISTERS 7 +#define DCP_FIRMWARE_H17P 3 +#define DCP_IOMFB_METHODS_H17P_EXTERNAL 2 +#define DEFINE_RES_MEM(s,n) { .start=(s), .end=(s)+(n)-1 } +#define resource_size(r) ((r)->end-(r)->start+1) +struct resource { u64 start,end; }; +struct device_node { bool named; unsigned count; }; +struct device { struct device_node *of_node; }; +struct platform_device { struct device dev; int num_resources; struct resource resources[10]; }; +#define to_platform_device(d) ((struct platform_device *)(d)) +struct apple_dcp_hw_data { + unsigned num_dptx_ports; bool adopt_live_session; + int iomfb_method_profile,firmware_compat; + const struct resource *firmware_clock; + u32 firmware_display_windows,firmware_scratch,firmware_request; +}; +struct apple_dcp { + struct device *dev; struct apple_dcp_hw_data hw; + struct resource *disp_registers[10],disp_bw_scratch_res,disp_bw_doorbell_res; + u32 disp_bw_scratch_index,disp_bw_scratch_offset,disp_bw_doorbell_index,nr_disp_registers; +}; +static bool of_property_present(struct device_node *n,const char *name) +{ return !strcmp(name,"reg-names") && n->named; } +static int of_property_count_strings(struct device_node *n,const char *name) +{ assert(!strcmp(name,"reg-names"));return n->count+1; } +static int of_property_read_string_index(struct device_node *n,const char *name,int i,const char **v) +{ + static const char *names[]={"coproc","disp-0","disp-1","disp-2","disp-3","disp-4","disp-5"}; + assert(!strcmp(name,"reg-names"));if(i<0 || (unsigned)i>n->count)return -EINVAL; + *v=names[i];return 0; +} +static struct resource *platform_get_resource(struct platform_device *p,int type,int i) +{ assert(type==IORESOURCE_MEM);return i>=1 && inum_resources?&p->resources[i-1]:NULL; } +static struct resource *platform_get_resource_byname(struct platform_device *p,int type,const char *name) +{ int i=-1;assert(sscanf(name,"disp-%d",&i)==1);return platform_get_resource(p,type,i+1); } +static int dcp_get_bw_scratch_reg(struct apple_dcp *p,u32 count) { (void)p;(void)count;return -ENOENT; } +static int dcp_get_bw_doorbell_reg(struct apple_dcp *p,u32 count) { (void)p;(void)count;return -ENOENT; } +''' + tests = r''' +static struct platform_device provider; +static struct device_node node; +static struct apple_dcp d; +static void reset(bool external,bool named) +{ + node=(struct device_node){.named=named,.count=5}; + provider=(struct platform_device){.dev.of_node=&node,.num_resources=6}; + for(unsigned i=0;i<5;i++)provider.resources[i]=(struct resource)DEFINE_RES_MEM(0x1000ULL*i,0x1000); + provider.resources[4]=t8140_firmware_clock; + d=(struct apple_dcp){.dev=&provider.dev,.hw=external?apple_dcp_hw_t8140_external:apple_dcp_hw_t8140}; +} +int main(void) +{ + const u32 fw[]={26,6,2},unknown[]={0,0,0}; + assert(apple_dcp_external_bw_valid(0xbc000,0x28000,0x70000)); + assert(!apple_dcp_external_bw_valid(0xbc000,0x28002,0x70000)); + struct apple_dcp_h17p_clock_request clock={.clock_id=APPLE_DCP_H17P_CLOCK_ID_154};u64 rate; + assert(!apple_dcp_h17p_clock_rate(&clock,123,456,&rate) && rate==123); + assert(apple_dcp_external_firmware_supported(fw,3,fw,3)); + assert(!apple_dcp_external_firmware_supported(NULL,3,fw,3)); + assert(!apple_dcp_external_firmware_supported(fw,3,NULL,3)); + assert(!apple_dcp_external_firmware_supported(unknown,3,fw,3)); + assert(apple_dcp_hw_t8140.num_dptx_ports==0 && apple_dcp_hw_t8140.adopt_live_session); + assert(apple_dcp_hw_t8140.firmware_display_windows==5); + assert(apple_dcp_hw_t8140.firmware_scratch==0x20000 && apple_dcp_hw_t8140.firmware_request==0x68000); + assert(apple_dcp_hw_t8140_external.num_dptx_ports==1 && !apple_dcp_hw_t8140_external.adopt_live_session); + assert(apple_dcp_hw_t8140_external.firmware_display_windows==4); + assert(apple_dcp_hw_t8140_external.firmware_scratch==0x28000 && apple_dcp_hw_t8140_external.firmware_request==0x70000); + for(unsigned named=0;named<2;named++) { + reset(true,named);assert(dcp_get_disp_regs(&d)==0); + assert(d.nr_disp_registers==5 && d.disp_bw_scratch_index==4); + assert(d.disp_bw_scratch_res.start+d.disp_bw_scratch_offset==0x302828000ULL); + assert(d.disp_bw_doorbell_res.start==0x302870000ULL && resource_size(&d.disp_bw_doorbell_res)==4); + struct apple_dcp_h17p_rt_bw_request request={0};struct apple_dcp_h17p_rt_bw_reply reply; + assert(!apple_dcp_h17p_rt_bw_encode_reply(&request,&reply,d.disp_bw_scratch_res.start+d.disp_bw_scratch_offset,d.disp_bw_doorbell_res.start)); + assert(reply.config.scratch==0x302828000ULL && reply.config.clock_request==0x302870000ULL && !reply.status); + request.config_null=1;assert(apple_dcp_h17p_rt_bw_encode_reply(&request,&reply,0,0)==-EINVAL); + reset(false,named);assert(dcp_get_disp_regs(&d)==0 && d.nr_disp_registers==6); + assert(d.disp_bw_scratch_offset==0x20000 && d.disp_bw_doorbell_res.start==0x302868000ULL); + reset(true,named);provider.resources[4].start+=4;assert(dcp_get_disp_regs(&d)==-EINVAL); + reset(true,named);provider.resources[4].end-=4;assert(dcp_get_disp_regs(&d)==-EINVAL); + reset(true,named);node.count=4;provider.num_resources=5;assert(dcp_get_disp_regs(&d)==-EINVAL); + reset(true,named);d.hw.firmware_scratch=0xbc000;assert(dcp_get_disp_regs(&d)==-EINVAL); + reset(true,named);d.hw.firmware_request=0xbc000;assert(dcp_get_disp_regs(&d)==-EINVAL); + reset(true,named);d.hw.firmware_display_windows=0;assert(dcp_get_disp_regs(&d)==-EINVAL); + } + puts("PASS: actual internal/external profiles, resource admission and D003 bandwidth reply"); +} +''' + return prefix + headers + "\n" + profiles + admission + tests + +def firmware(root): + + 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 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; +} +''' + return prefix + body + suffix + +def readiness(root): + + dcp = (root / 'drivers/gpu/drm/apple/dcp.c').read_text() + drm = (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) {'):] + production = '\n'.join(( + function(dcp, 'bool dcp_h17p_external_supported('), + 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, + )) + 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 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; u32 firmware[3], compat[3]; int firmware_cells, compat_cells; }; +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 machine_supported, 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 of_machine_is_compatible(const char *s) +{ assert(!strcmp(s,"apple,j700")); return machine_supported; } +static int of_property_read_variable_u32_array(struct device_node *n,const char *s,u32 *v,int min,int max) +{ + assert(min==3 && max==3); + bool fw=!strcmp(s,"apple,firmware-version"); + assert(fw || !strcmp(s,"apple,firmware-compat")); + int count=fw?n->firmware_cells:n->compat_cells; + if(count!=3) return -EINVAL; + memcpy(v,fw?n->firmware:n->compat,3*sizeof(*v)); return count; +} +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 supported_firmware) +{ + 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; + devices[0].registered = devices[0].bound = true; + machine_supported = true; + nodes[1].firmware_cells=nodes[1].compat_cells=3; + if(supported_firmware) { + u32 version[]={26,6,2}; + memcpy(nodes[1].firmware,version,sizeof(version)); + memcpy(nodes[1].compat,version,sizeof(version)); + } + 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) +{ + assert(apple_dcp_external_bw_valid(0xbc000,0x28000,0x70000)); + assert(apple_dcp_external_clock_window(0x302800000ULL,0xbc000,0x302800000ULL,0xbc000)); + /* No command line state: exact J700 firmware selects the external profile. */ + initial(true); assert(dcp_h17p_external_supported(&nodes[1])); + machine_supported=false; + assert(!dcp_h17p_external_supported(&nodes[1]) && match_providers()==1); + for(unsigned i=0;i<3;i++) { + initial(true); nodes[1].firmware[i]++; + assert(!dcp_h17p_external_supported(&nodes[1]) && match_providers()==1); + initial(true); nodes[1].compat[i]++; + assert(!dcp_h17p_external_supported(&nodes[1]) && match_providers()==1); + } + for(int count=-1;count<=5;count++) if(count!=3) { + initial(true); nodes[1].firmware_cells=count; + assert(!dcp_h17p_external_supported(&nodes[1])); + initial(true); nodes[1].compat_cells=count; + assert(!dcp_h17p_external_supported(&nodes[1])); + } + + /* Unknown external firmware preserves the normal internal provider. */ + 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: automatic hardware/firmware matching, delayed/preflight-failed providers, synchronous publication, readiness rejection"); + return 0; +} +''' + + header = (root / 'drivers/gpu/drm/apple/dcp-external-bw.h').read_text().replace('#include ', '') + return prefix + header + production + suffix + +def vrr(root): + + source = (root / 'drivers/gpu/drm/apple/dcp.c').read_text() + start = source.index('int dcp_crtc_atomic_check(') + opening = source.index('{', start) + end, depth = opening + 1, 1 + while depth: + depth += (source[end] == '{') - (source[end] == '}') + end += 1 + check = source[start:end] + prefix = r""" +#undef NDEBUG +#include +#include +#include +#include +#define DCP_IOMFB_METHODS_H17P_EXTERNAL 2 +#define READ_ONCE(x) (x) +#define dev_dbg(...) ((void)0) +struct apple_connector { bool connected; }; +struct apple_dcp { + bool crashed; + struct { int iomfb_method_profile; } hw; + struct { bool valid; } mode_state; + struct apple_connector *connector; + void *dev; +}; +struct platform_device { struct apple_dcp *data; }; +struct drm_crtc { struct platform_device *dcp; }; +struct drm_crtc_state { bool vrr_enabled, modeset; }; +struct drm_atomic_state { struct drm_crtc_state crtc; }; +#define to_apple_crtc(x) (x) +#define platform_get_drvdata(x) ((x)->data) +static struct drm_crtc_state *drm_atomic_get_new_crtc_state( + struct drm_atomic_state *s, struct drm_crtc *c) +{ (void)c; return &s->crtc; } +static bool drm_atomic_crtc_needs_modeset(struct drm_crtc_state *s) +{ return s->modeset; } +""" + suffix = r""" +int main(void) +{ + struct apple_connector connector = { .connected = true }; + struct apple_dcp dcp = { .connector = &connector }; + struct platform_device pdev = { .data = &dcp }; + struct drm_crtc crtc = { .dcp = &pdev }; + struct drm_atomic_state state = {0}; + unsigned int cases = 0; + /* Check initial modesets, plane-only commits and link recovery states. */ + for (unsigned int profile = 0; profile < 3; profile++) + for (unsigned int valid = 0; valid < 2; valid++) + for (unsigned int modeset = 0; modeset < 2; modeset++) + for (unsigned int link = 0; link < 3; link++) + for (unsigned int vrr = 0; vrr < 2; vrr++) { + dcp.hw.iomfb_method_profile = profile; + dcp.mode_state.valid = valid; + state.crtc.modeset = modeset; + state.crtc.vrr_enabled = vrr; + dcp.connector = link == 2 ? NULL : &connector; + connector.connected = link == 1; + int expected = profile == DCP_IOMFB_METHODS_H17P_EXTERNAL && vrr ? -EINVAL : 0; + int actual = dcp_crtc_atomic_check(&crtc, &state); + if (actual != expected) { + fprintf(stderr, "profile=%u valid=%u modeset=%u link=%u vrr=%u: got %d expected %d\n", + profile, valid, modeset, link, vrr, actual, expected); + return 1; + } + cases++; + } + dcp.crashed = true; + state.crtc.vrr_enabled = false; + assert(dcp_crtc_atomic_check(&crtc, &state) == -EINVAL); + printf("PASS: %u production atomic VRR admission cases and crashed-device rejection\n", cases); + return 0; +} +""" + return prefix + check + suffix + + +def power_reply(root): + """Actual external power callback validation with host completion/refcount mocks.""" + source = (root / 'drivers/gpu/drm/apple/iomfb_h17p.c').read_text() + production = block(source, 'struct dcp_external_power_cookie {') + ';\n' + production += block(source, 'static void iomfb_external_power_complete(') + production += block(source, 'static bool iomfb_external_reply_size(') + prefix = r""" +#include +#include +#include +#include +#include +#include +#include +typedef uint32_t u32; typedef uint64_t u64; +#define WRITE_ONCE(p,v) ((p)=(v)) +#define container_of(p,t,m) ((t *)((char *)(p)-offsetof(t,m))) +struct completion { bool done; }; +struct kref { unsigned count; }; +struct dcp_wait_cookie { struct kref refcount; struct completion done; u32 status; }; +struct dcp_packet_header { char tag[4]; u32 in_len,out_len; }; +struct dcp_set_power_state_req_h17p { u64 unklong; u32 unkint; unsigned char null,padding[3]; } __attribute__((packed)); +struct dcp_set_power_state_resp { u32 unknown,ret; } __attribute__((packed)); +struct apple_dcp { int unused; }; +static unsigned released; +static u64 get_unaligned_le64(const void *p) { u64 v; memcpy(&v,p,sizeof(v)); return v; } +static void complete(struct completion *c) { assert(!c->done); c->done=true; } +static void release_wait_cookie(struct kref *r) { assert(!r->count); released++; } +static void kref_put(struct kref *r,void (*release)(struct kref *)) +{ assert(r->count); if (!--r->count) release(r); } +""" + controls = r""" +int main(void) +{ + struct apple_dcp d={}; + struct { struct dcp_packet_header h; struct dcp_set_power_state_req_h17p q; unsigned char reply[8]; } packet={}; + for(unsigned on=0;on<2;on++) { + const u64 replies[]={0,1,0x123400000000ULL}; + for(unsigned i=0;i<3;i++) { + packet.h.in_len=sizeof(packet.q);packet.h.out_len=sizeof(packet.reply);memcpy(packet.reply,&replies[i],sizeof(packet.reply)); + struct dcp_external_power_cookie c={.on=on,.wait.refcount.count=2}; + assert(iomfb_external_reply_size(&packet.reply,sizeof(packet.q),sizeof(packet.reply))); + iomfb_external_power_complete(&d,&packet.reply,&c); + assert(c.wait.done.done && c.wait.refcount.count==1); + assert((int)c.wait.status==(replies[i]==on?0:-EREMOTEIO)); + kref_put(&c.wait.refcount,release_wait_cookie); + } + } + for(unsigned fault=0;fault<3;fault++) { + packet.h.in_len=sizeof(packet.q);packet.h.out_len=sizeof(packet.reply); + void *out=&packet.reply; + if(fault==0) out=NULL; + if(fault==1) packet.h.in_len--; + if(fault==2) packet.h.out_len--; + struct dcp_external_power_cookie c={.wait.refcount.count=2}; + assert(!iomfb_external_reply_size(out,sizeof(packet.q),sizeof(packet.reply))); + iomfb_external_power_complete(&d,out,&c); + assert(c.wait.done.done && c.wait.refcount.count==1 && (int)c.wait.status==-EPROTO); + kref_put(&c.wait.refcount,release_wait_cookie); + } + assert(released==9); + puts("PASS: actual exact ON/OFF echoes, nonzero status and null/truncated reply rejection preserve callback completion and references"); +} +""" + return prefix + production + controls + + +def dispatcher(root): + 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 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; + 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, + .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; +} +""" + + return prefix + source[start:end] + suffix diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/phy.py b/tools/testing/selftests/drivers/gpu/apple-dcp/phy.py new file mode 100644 index 0000000000000..fbb1951c0f0b1 --- /dev/null +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/phy.py @@ -0,0 +1,2399 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright 2026 Ryan Murray +"""Actual PHY parking, failure/retry, masked context and OFF controls.""" +from pathlib import Path +import re +from common import block, source_defines + +def parking(root): + + atc = (root / 'drivers/phy/apple/atc.c').read_text() + dp = (root / 'drivers/phy/apple/atc-j700-dp.h').read_text() + + + def function(source, name): + if name == 'atcphy_mux_set' and 'static int atcphy_mux_set_locked(' in source: + marker = re.search(r'^static [^\n]+\batcphy_mux_set_locked\(', source, re.M).group() + return block(source, marker).replace('atcphy_mux_set_locked(', 'atcphy_mux_set(', 1) + 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 = source_defines(atc) + 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, *pipehandler, *usb2phy; } regs; + struct reset_controller_dev rcdev; + 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; + unsigned int dp_usb_context_valid, dp_usb_context_dirty; + u32 dp_usb_context[2][27]; + bool pipe_pending, usb_off_pending, usb_off_drained; + 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 u32 pipe_regs[0x1000 / 4], usb2_regs[0x1000 / 4]; +static unsigned int polls, fail_poll, writes, usb2_on, usb2_writes, reset_writes, mux_writes; +static bool stuck_ack; +static bool hold_unlock_ack; +static const char *poll_caller[256], *fail_caller; +static bool bist_status_stuck; +static unsigned int bist_faults; +static bool require_parked, expected_j700_swap; +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); + if (require_parked) { + assert(p->pipe_state == ATCPHY_PIPEHANDLER_STATE_DUMMY); + assert(FIELD_GET(p->hw->pipe_mux_data, pipe_regs[PIPEHANDLER_MUX_CTRL / 4]) == + PIPEHANDLER_MUX_CTRL_DATA_DUMMY); + assert(FIELD_GET(p->hw->pipe_mux_clk, pipe_regs[PIPEHANDLER_MUX_CTRL / 4]) == + PIPEHANDLER_MUX_CTRL_CLK_DUMMY); + } + 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); + registers[0x7034 / 4] |= BIT(3); + } 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 mask32(void *p, u32 m, u32 v) +{ + *(u32 *)p = (*(u32 *)p & ~m) | v; + if (p == (void *)&pipe_regs[PIPEHANDLER_LOCK_REQ / 4] && !stuck_ack) + pipe_regs[PIPEHANDLER_LOCK_ACK / 4] = hold_unlock_ack && + !(*(u32 *)p & PIPEHANDLER_LOCK_EN) ? PIPEHANDLER_LOCK_EN : + *(u32 *)p & PIPEHANDLER_LOCK_EN; + if (p == (void *)&pipe_regs[PIPEHANDLER_AON_GEN / 4]) reset_writes++; + if (p == (void *)&pipe_regs[PIPEHANDLER_MUX_CTRL / 4]) mux_writes++; + if ((uintptr_t)p >= (uintptr_t)usb2_regs && + (uintptr_t)p < (uintptr_t)usb2_regs + sizeof(usb2_regs)) usb2_writes++; +} +static void writel(u32 value, void *address) { mask32(address, ~0U, value); } +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); assert(polls < ARRAY_SIZE(poll_caller)); \ + poll_caller[polls++] = __func__; \ + bool bist_fault = bist_status_stuck && (void *)(address) == \ + (void *)®isters[ACIOPHY_BIST_MAC_USBPHY_STATUS2 / 4]; \ + bist_faults += bist_fault; \ + polls == fail_poll || bist_fault || \ + (fail_caller && !strcmp(fail_caller, __func__)) || !(condition) ? -ETIMEDOUT : 0; }) +#define readl_poll_timeout readl_poll_timeout_atomic +/* Observation is read-only and separately tested; retain production caller control flow. */ +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; } +''') + for name in ('atcphy_pipehandler_lock', 'atcphy_pipehandler_unlock', 'atcphy_pipehandler_check', + 'atcphy_pipehandler_set_mux', 'atcphy_clear_nonselected_phy_reset', + 'atcphy_configure_pipehandler_usb3_host_t8122', + 'atcphy_configure_pipehandler_usb3_host_t8103', + 'atcphy_configure_pipehandler_usb3', 'atcphy_configure_pipehandler_usb4', + 'atcphy_enable_dummy_phy', 'atcphy_configure_pipehandler_dummy', + 'atcphy_configure_pipehandler', 'atcphy_park_pipehandler', + 'atcphy_usb2_power_off', '_atcphy_dwc3_reset_assert', 'atcphy_dwc3_reset_assert'): + parts.append(function(atc, name)) + for name in ('core_read32', '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_sw_set', 'atcphy_mux_set', + 'atcphy_usb3_set_mode', 'atcphy_usb3_power_off'): + parts.append(function(atc, name)) + production = '\n'.join(parts) + if 'static int j700_usb_capture(' in dp: + production = '#define HAS_USB_CONTEXT\n' + production + prefix = r''' +#include +#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 field_prep FIELD_PREP +#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 }; +enum typec_orientation { TYPEC_ORIENTATION_NONE, TYPEC_ORIENTATION_NORMAL, + TYPEC_ORIENTATION_REVERSE }; +enum phy_mode { PHY_MODE_USB_HOST, PHY_MODE_USB_DEVICE }; +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; }; +struct typec_switch_dev { struct apple_atcphy *data; }; +#define typec_mux_get_drvdata(mux) ((mux)->data) +#define typec_switch_get_drvdata(sw) ((sw)->data) +struct phy { struct apple_atcphy *data; }; +#define phy_get_drvdata(phy) ((phy)->data) +struct reset_controller_dev { int unused; }; +#define container_of(p, t, m) ((t *)((char *)(p) - offsetof(t, m))) +''' + suffix = r''' +static struct atcphy_hw hw = { + .has_t8140_dp = true, .gen = ATCPHY_GENERATION_T8122, + .aciophy_lane_mode = ACIOPHY_LANE_MODE_T8122, + .aciophy_crossbar = ACIOPHY_CROSSBAR_T8122, + .pipe_mux_clk = PIPEHANDLER_MUX_CTRL_CLK_T8103, + .pipe_mux_data = PIPEHANDLER_MUX_CTRL_DATA_T8103, +}; +static void cold_initial(struct apple_atcphy *p, bool fixed, bool swapped) +{ + memset(registers, 0, sizeof(registers)); + memset(pipe_regs, 0, sizeof(pipe_regs)); + memset(usb2_regs, 0, sizeof(usb2_regs)); + registers[0x200 / 4] = 0xee; /* public PMA powered state, synthetic cold fixture */ + registers[AUS_COMMON_DIG_RCAL1 / 4] = AUS_COMMON_DIG_RCAL1_ALL_CODES_DONE; + pipe_regs[PIPEHANDLER_AON_GEN / 4] = PIPEHANDLER_AON_GEN_DWC3_RESET_N; + usb2_regs[USB2PHY_USBCTL / 4] = USB2PHY_USBCTL_RUN; + *p = (struct apple_atcphy) { + .hw = &hw, .regs.core = (unsigned char *)registers, + .regs.lpdptx = (unsigned char *)registers, + .regs.pipehandler = (unsigned char *)pipe_regs, + .regs.usb2phy = (unsigned char *)usb2_regs, + .fixed_usb2 = fixed, .host_active = true, .swap_lanes = swapped, + .ss_tunables = true, .dp_tunables = true, .dp_presets_valid = { true }, + .mode = APPLE_ATCPHY_MODE_USB3_DP, .typec_mode = APPLE_ATCPHY_MODE_USB3_DP, + .pipe_state = ATCPHY_PIPEHANDLER_STATE_DUMMY, .mode_dirty = false, + .dp_owned_pmas = 0, .dp_idle_pmas = 0, + .aux_power_requested = false, + }; + polls = fail_poll = writes = usb2_on = usb2_writes = reset_writes = mux_writes = 0; + stuck_ack = bist_status_stuck = false; + hold_unlock_ack = false; + fail_caller = NULL; + bist_faults = 0; + require_parked = false; +} +static void initial(struct apple_atcphy *p, bool fixed, bool dirty, bool swapped) +{ + cold_initial(p, fixed, swapped); +#ifdef HAS_USB_CONTEXT + if (fixed && hw.has_t8140_dp) { + /* Two-PMA cleanup stress setup: capture before ownership and actual DP writes. */ + assert(j700_usb_capture(p, APPLE_ATCPHY_MODE_DP) == 0); + assert(atcphy_configure_pipehandler(p, true) == 0); + assert(j700_dp_start(p, 3, 3, false) == 0); + } else +#endif + { + p->pipe_state = ATCPHY_PIPEHANDLER_STATE_USB3; + p->dp_owned_pmas = 3; /* legacy synthetic cleanup ownership, no J700 context path */ + } + p->mode_dirty = dirty; + p->dp_idle_pmas = dirty ? 1 : 0; + p->aux_power_requested = true; + polls = fail_poll = writes = usb2_on = usb2_writes = reset_writes = mux_writes = 0; +} + +static void inject_parking(unsigned int fault) +{ + pipe_regs[PIPEHANDLER_LOCK_ACK / 4] = PIPEHANDLER_LOCK_EN; + if (fault) + fail_poll = 1; + else + stuck_ack = true; +} +static void recover(void) +{ + stuck_ack = bist_status_stuck = false; + hold_unlock_ack = false; + fail_caller = NULL; + fail_poll = polls = 0; +} +static int request(struct apple_atcphy *p, bool safe) +{ + struct typec_mux_dev mux = { p }; + struct typec_altmode dp = { .svid = USB_TYPEC_DP_SID }; + struct typec_mux_state state = { + .mode = safe ? TYPEC_STATE_SAFE : TYPEC_DP_STATE_C, + .alt = safe ? NULL : &dp, + }; + return atcphy_mux_set(&mux, &state); +} +static void unchanged(struct apple_atcphy *p, bool dirty, bool host) +{ + bool expected_dirty = dirty || (p->hw->has_t8140_dp && p->fixed_usb2 && host); + assert(p->mode == APPLE_ATCPHY_MODE_USB3_DP && p->mode_dirty == expected_dirty); + assert(p->pipe_state == ATCPHY_PIPEHANDLER_STATE_USB3 && p->host_active == host); + assert(p->dp_owned_pmas == 3 && p->dp_idle_pmas == (dirty ? 1U : 0U)); + assert(p->aux_power_requested); + assert(writes == 0 && usb2_on == 0 && usb2_writes == 0 && reset_writes == 0); + assert(mux_writes == 0); + assert(pipe_regs[PIPEHANDLER_AON_GEN / 4] == PIPEHANDLER_AON_GEN_DWC3_RESET_N); + assert(usb2_regs[USB2PHY_USBCTL / 4] == USB2PHY_USBCTL_RUN); + if (expected_dirty) { + struct phy_configure_opts_dp training = { + .set_rate = true, .set_lanes = true, .link_rate = 8100, .lanes = 2, + }; + assert(j700_dp_configure(p, &training) == -EBUSY && writes == 0); + } +} +static void observed(const char *caller, int result) +{ + printf("%s: result=%d core_writes=%u usb2_writes=%u reset_writes=%u\n", + caller, result, writes, usb2_writes, reset_writes); + fflush(stdout); +} +static void mux_parking(void) +{ + struct apple_atcphy p; + unsigned int cases = 0; + for (unsigned int dirty = 0; dirty < 2; dirty++) + for (unsigned int swapped = 0; swapped < 2; swapped++) + for (unsigned int host = 0; host < 2; host++) + for (unsigned int safe = 0; safe < 2; safe++) + for (unsigned int fault = 0; fault < 2; fault++) { + initial(&p, true, dirty, swapped); + p.host_active = host; + inject_parking(fault); + int ret = request(&p, safe); + observed("mux parking failure", ret); + assert(ret == -ETIMEDOUT); + unchanged(&p, dirty, host); + if (!fault) { + assert(request(&p, safe) == -ETIMEDOUT); + unchanged(&p, dirty, host); + } + recover(); + assert(request(&p, safe) == 0); + assert(p.mode == (safe ? APPLE_ATCPHY_MODE_USB2 : APPLE_ATCPHY_MODE_DP)); + assert(!p.mode_dirty && p.host_active == host); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_DUMMY && !p.dp_owned_pmas); + assert(!p.dp_idle_pmas && p.aux_power_requested == !safe); + assert(usb2_on == 1 && usb2_writes == 0 && reset_writes == 0); + assert(usb2_regs[USB2PHY_USBCTL / 4] == USB2PHY_USBCTL_RUN); + unsigned int before = writes; + assert(request(&p, safe) == 0 && writes == before && usb2_on == 1); + cases++; + } + printf("PASS: %u actual mux parking/retry scenarios preserve fixed USB2 and ownership\n", cases); +} +static void power_parking(void) +{ + struct apple_atcphy p; + unsigned int cases = 0; + for (unsigned int fixed = 0; fixed < 2; fixed++) + for (unsigned int dirty = 0; dirty < 2; dirty++) + for (unsigned int swapped = 0; swapped < 2; swapped++) + for (unsigned int fault = 0; fault < 2; fault++) { + initial(&p, fixed, dirty, swapped); + struct phy phy = { &p }; + inject_parking(fault); + int ret = atcphy_usb3_power_off(&phy); + observed("power-off parking failure", ret); + assert(ret == -ETIMEDOUT); + unchanged(&p, dirty, true); + if (!fault) { + assert(atcphy_usb3_power_off(&phy) == -ETIMEDOUT); + unchanged(&p, dirty, true); + } + recover(); + assert(atcphy_usb3_power_off(&phy) == 0); + assert(p.mode == APPLE_ATCPHY_MODE_OFF && !p.mode_dirty && !p.host_active); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_DUMMY); + assert(!p.dp_owned_pmas && !p.dp_idle_pmas && !p.aux_power_requested); + assert(usb2_writes == 0 && reset_writes == 0); + unsigned int before = writes; + assert(atcphy_usb3_power_off(&phy) == 0 && writes == before); + cases++; + } + printf("PASS: %u actual power-off parking/retry scenarios\n", cases); +} +static void reset_parking(void) +{ + struct apple_atcphy p; + unsigned int cases = 0; + for (unsigned int fixed = 0; fixed < 2; fixed++) + for (unsigned int dirty = 0; dirty < 2; dirty++) + for (unsigned int fault = 0; fault < 2; fault++) { + initial(&p, fixed, dirty, false); + inject_parking(fault); + int ret = atcphy_dwc3_reset_assert(&p.rcdev, 0); + observed("reset parking failure", ret); + assert(ret == -ETIMEDOUT); + unchanged(&p, dirty, true); + if (!fault) { + assert(atcphy_dwc3_reset_assert(&p.rcdev, 0) == -ETIMEDOUT); + unchanged(&p, dirty, true); + } + recover(); + assert(atcphy_dwc3_reset_assert(&p.rcdev, 0) == 0); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_DUMMY && !p.host_active); + assert(p.mode == APPLE_ATCPHY_MODE_USB3_DP && p.mode_dirty == (dirty || fixed)); + assert(p.dp_owned_pmas == 3 && p.dp_idle_pmas == (dirty ? 1U : 0U)); + assert(p.aux_power_requested && writes == 0); + assert(reset_writes == 2 && usb2_writes > 0); + assert(!(pipe_regs[PIPEHANDLER_AON_GEN / 4] & PIPEHANDLER_AON_GEN_DWC3_RESET_N)); + assert(pipe_regs[PIPEHANDLER_AON_GEN / 4] & PIPEHANDLER_AON_GEN_DWC3_FORCE_CLAMP_EN); + assert(!(usb2_regs[USB2PHY_USBCTL / 4] & USB2PHY_USBCTL_RUN)); + cases++; + } + printf("PASS: %u actual reset parking/retry scenarios retain DP ownership\n", cases); +} +static void power_cleanup(void) +{ + struct apple_atcphy p; + unsigned int phases = 0; + for (unsigned int fixed = 0; fixed < 2; fixed++) { + initial(&p, fixed, false, false); + p.pipe_state = ATCPHY_PIPEHANDLER_STATE_DUMMY; + struct phy phy = { &p }; + fail_poll = 1; /* The first DP stop ACK cannot complete. */ + int ret = atcphy_usb3_power_off(&phy); + observed("power-off DP cleanup failure", ret); + assert(ret == -ETIMEDOUT && p.mode_dirty && p.host_active); + assert(p.mode == APPLE_ATCPHY_MODE_USB3_DP && p.dp_owned_pmas == 3); + assert(p.aux_power_requested && usb2_writes == 0 && reset_writes == 0); + struct phy_configure_opts_dp training = { + .set_rate = true, .set_lanes = true, .link_rate = 8100, .lanes = 2, + }; + unsigned int before = writes; + assert(j700_dp_configure(&p, &training) == -EBUSY && writes == before); + recover(); + assert(atcphy_usb3_power_off(&phy) == 0); + assert(!p.mode_dirty && !p.host_active && !p.dp_owned_pmas && !p.aux_power_requested); + /* A failed initial transition can leave OFF recorded with dirty resources. */ + initial(&p, fixed, true, false); + p.mode = APPLE_ATCPHY_MODE_OFF; + p.pipe_state = ATCPHY_PIPEHANDLER_STATE_DUMMY; + fail_poll = 1; + assert(atcphy_usb3_power_off(&phy) == -ETIMEDOUT); + assert(p.mode_dirty && p.host_active && p.dp_owned_pmas == 3); + recover(); + assert(atcphy_usb3_power_off(&phy) == 0); + assert(p.mode == APPLE_ATCPHY_MODE_OFF && !p.mode_dirty && !p.host_active); + assert(!p.dp_owned_pmas && !p.dp_idle_pmas && !p.aux_power_requested); + for (unsigned int fault = 1; fault <= 2; fault++) { + initial(&p, fixed, false, false); + p.mode = APPLE_ATCPHY_MODE_USB2; + p.pipe_state = ATCPHY_PIPEHANDLER_STATE_DUMMY; + p.dp_owned_pmas = p.dp_idle_pmas = 0; + p.aux_power_requested = false; + fail_poll = fault; /* BIG/SMALL sleep ACK. */ + assert(atcphy_usb3_power_off(&phy) == -ETIMEDOUT); + assert(p.mode == APPLE_ATCPHY_MODE_USB2 && p.mode_dirty && p.host_active); + recover(); + assert(atcphy_usb3_power_off(&phy) == 0 && !p.mode_dirty && !p.host_active); + } + /* Inject every poll in DP stop, pending PMA idle, AUX and domain sleep. */ + initial(&p, fixed, true, false); + p.pipe_state = ATCPHY_PIPEHANDLER_STATE_DUMMY; + assert(atcphy_usb3_power_off(&phy) == 0); + unsigned int total = polls; + for (unsigned int fault = 1; fault <= total; fault++) { + initial(&p, fixed, true, false); + p.pipe_state = ATCPHY_PIPEHANDLER_STATE_DUMMY; + fail_poll = fault; + assert(atcphy_usb3_power_off(&phy) == -ETIMEDOUT); + assert(p.mode == APPLE_ATCPHY_MODE_USB3_DP && p.mode_dirty && p.host_active); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_DUMMY); + assert(usb2_writes == 0 && reset_writes == 0); + unsigned int owned = p.dp_owned_pmas, idle = p.dp_idle_pmas; + bool aux = p.aux_power_requested; + unsigned int before = writes; + assert(j700_dp_configure(&p, &training) == -EBUSY && writes == before); + assert(p.dp_owned_pmas == owned && p.dp_idle_pmas == idle); + assert(p.aux_power_requested == aux); + recover(); + assert(atcphy_usb3_power_off(&phy) == 0); + assert(p.mode == APPLE_ATCPHY_MODE_OFF && !p.mode_dirty && !p.host_active); + assert(!p.dp_owned_pmas && !p.dp_idle_pmas && !p.aux_power_requested); + phases++; + } + } + printf("PASS: %u cleanup poll faults retain retry/ownership; dirty OFF retries cleanup\n", phases); +} +static int select_mode(struct apple_atcphy *p, enum atcphy_mode mode) +{ + struct typec_mux_dev mux = { p }; + struct typec_altmode dp = { .svid = USB_TYPEC_DP_SID }; + struct typec_mux_state state = { .mode = TYPEC_STATE_SAFE }; + + if (mode == APPLE_ATCPHY_MODE_USB3) + state.mode = TYPEC_STATE_USB; + else if (mode == APPLE_ATCPHY_MODE_USB3_DP || mode == APPLE_ATCPHY_MODE_DP) { + state.alt = &dp; + state.mode = mode == APPLE_ATCPHY_MODE_DP ? TYPEC_DP_STATE_C : TYPEC_DP_STATE_D; + } + return atcphy_mux_set(&mux, &state); +} +static void orient(struct apple_atcphy *p, bool reverse) +{ + struct typec_switch_dev sw = { p }; + assert(atcphy_sw_set(&sw, reverse ? TYPEC_ORIENTATION_REVERSE : + TYPEC_ORIENTATION_NORMAL) == 0); +} +static void orientation_initial(struct apple_atcphy *p, bool fixed) +{ + cold_initial(p, fixed, false); + p->mode = p->typec_mode = fixed ? APPLE_ATCPHY_MODE_USB2 : APPLE_ATCPHY_MODE_OFF; + p->pipe_state = ATCPHY_PIPEHANDLER_STATE_DUMMY; + p->dp_owned_pmas = p->dp_idle_pmas = 0; + p->aux_power_requested = false; + p->dp_link_rate = -1; + p->host_active = fixed; + registers[ACIOPHY_TOP_PHY_STAT / 4] = ACIOPHY_TOP_PHY_STAT_LN0_UNK0; + registers[ATCPHY_MISC / 4] = BIT(7) | ATCPHY_MISC_LANE_SWAP; + expected_j700_swap = true; + registers[hw.aciophy_lane_mode / 4] = 0xa5000000; + registers[hw.aciophy_crossbar / 4] = 0x80000000; + pipe_regs[PIPEHANDLER_MUX_CTRL / 4] = + FIELD_PREP(hw.pipe_mux_data, PIPEHANDLER_MUX_CTRL_DATA_DUMMY) | + FIELD_PREP(hw.pipe_mux_clk, PIPEHANDLER_MUX_CTRL_CLK_DUMMY); +} +static void check_route(struct apple_atcphy *p, enum atcphy_mode mode, bool reverse) +{ + assert(p->mode == mode && p->swap_lanes == reverse && !p->mode_dirty); + assert(registers[ATCPHY_MISC / 4] & BIT(7)); + /* Fixed public route expectations, independent of the production mode table. */ + assert(mode == APPLE_ATCPHY_MODE_USB3 || mode == APPLE_ATCPHY_MODE_USB3_DP || + mode == APPLE_ATCPHY_MODE_DP); + u32 lane = registers[hw.aciophy_lane_mode / 4]; + u32 crossbar = registers[hw.aciophy_crossbar / 4]; + u32 mask = hw.has_t8140_dp && mode != APPLE_ATCPHY_MODE_DP ? 0x1ffff : 0x3ffff; + assert((lane & 0xfff) == (mode == APPLE_ATCPHY_MODE_DP ? 0x492 : + (reverse ? 0x252 : 0x489))); + assert((lane & 0xfffff000) == 0xa5000000); + assert((crossbar & mask) == (mode == APPLE_ATCPHY_MODE_DP ? + (reverse ? 0x114 : 0x22014) : (0x110 | reverse))); + assert(crossbar & 0x80000000); + /* J700 preserves the boot swap bit; generic routes apply their explicit swap. */ + assert(!!(registers[ATCPHY_MISC / 4] & ATCPHY_MISC_LANE_SWAP) == + (hw.has_t8140_dp ? expected_j700_swap : + (mode != APPLE_ATCPHY_MODE_DP && reverse))); + assert(p->pipe_state == (p->fixed_usb2 && mode != APPLE_ATCPHY_MODE_DP ? + ATCPHY_PIPEHANDLER_STATE_USB3 : ATCPHY_PIPEHANDLER_STATE_DUMMY)); +} +static void orientation_roundtrip(void) +{ + struct apple_atcphy p; + const enum atcphy_mode modes[] = { + APPLE_ATCPHY_MODE_USB3, APPLE_ATCPHY_MODE_USB3_DP, APPLE_ATCPHY_MODE_DP, + }; + unsigned int cases = 0; + + for (unsigned int generation = 0; generation < 4; generation++) { + hw.gen = generation == 0 ? ATCPHY_GENERATION_T8103 : ATCPHY_GENERATION_T8122; + hw.has_t8140_dp = generation == 2; + hw.has_usb4 = !hw.has_t8140_dp; + hw.aciophy_lane_mode = generation == 0 ? ACIOPHY_LANE_MODE_T8103 : + ACIOPHY_LANE_MODE_T8122; + hw.aciophy_crossbar = generation == 0 ? ACIOPHY_CROSSBAR_T8103 : + ACIOPHY_CROSSBAR_T8122; + hw.pipe_mux_clk = generation == 3 ? PIPEHANDLER_MUX_CTRL_CLK_T8142 : + PIPEHANDLER_MUX_CTRL_CLK_T8103; + hw.pipe_mux_data = generation == 3 ? PIPEHANDLER_MUX_CTRL_DATA_T8142 : + PIPEHANDLER_MUX_CTRL_DATA_T8103; + for (unsigned int fixed = 0; fixed < 2; fixed++) { + for (unsigned int m = 0; m < ARRAY_SIZE(modes); m++) { + for (unsigned int first = 0; first < 2; first++) { + orientation_initial(&p, fixed); + for (unsigned int turn = 0; turn < 3; turn++) { + bool reverse = first ^ (turn & 1); + orient(&p, reverse); + if (turn) { + assert(p.mode_dirty); + struct phy phy = { &p }; + unsigned int before = writes; + assert(atcphy_usb3_set_mode(&phy, PHY_MODE_USB_HOST, 0) == -EBUSY); + assert(writes == before); + } + require_parked = true; + assert(select_mode(&p, modes[m]) == 0); + require_parked = false; + check_route(&p, modes[m], reverse); + unsigned int before = writes, before_mux = mux_writes; + orient(&p, reverse); + assert(select_mode(&p, modes[m]) == 0); + assert(writes == before && mux_writes == before_mux); + struct typec_switch_dev sw = { &p }; + assert(atcphy_sw_set(&sw, TYPEC_ORIENTATION_NONE) == 0); + assert(!p.mode_dirty && p.swap_lanes == reverse); + cases++; + } + /* A USB3 protocol change also rebuilds the live backend. */ + if (m == 0) { + require_parked = true; + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + require_parked = false; + check_route(&p, APPLE_ATCPHY_MODE_USB3_DP, first); + } + assert(select_mode(&p, APPLE_ATCPHY_MODE_OFF) == 0); + assert(p.mode == (fixed ? APPLE_ATCPHY_MODE_USB2 : APPLE_ATCPHY_MODE_OFF)); + assert(!p.mode_dirty && !p.aux_power_requested && !p.dp_idle_pmas); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_DUMMY); + } + } + } + } + printf("PASS: %u production orientation routes across T8103/T8122/T8140/T8142, fixed/ordinary ports, both flip roundtrips\n", cases); +} +static void orientation_faults(void) +{ + struct apple_atcphy p; + unsigned int cases = 0; + + for (unsigned int fixed = 0; fixed < 2; fixed++) { + for (unsigned int first = 0; first < 2; first++) { + orientation_initial(&p, fixed); + orient(&p, first); + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + struct phy_configure_opts_dp rate = { + .set_rate = true, .set_lanes = true, .link_rate = 8100, .lanes = 2, + }; + assert(j700_dp_configure(&p, &rate) == 0); + assert(p.dp_owned_pmas == BIT(!first)); + orient(&p, !first); + unsigned int before = writes; + struct phy_configure_opts_dp training = { .set_voltages = true, .lanes = 2 }; + assert(j700_dp_configure(&p, &training) == -EBUSY && writes == before); + assert(j700_dp_configure(&p, &rate) == -EBUSY && writes == before); + /* Derive all actual poll phases, then inject each in a fresh transition. */ + polls = 0; + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + unsigned int total = polls; + bool advisory[256] = { false }; + for (unsigned int i = 0; i < total; i++) { + const char *caller = poll_caller[i]; + /* Every observed BIST and USB3 handoff failure is now fatal. */ + if (fixed && i + 1 == total) { + assert(!strcmp(caller, "atcphy_pipehandler_unlock")); + advisory[i] = false; + } else if (!strcmp(caller, "atcphy_configure_pipehandler_usb3_host_t8122")) { + assert(fixed); + advisory[i] = false; + } else { + assert(!strcmp(caller, "j700_dp_wait") || + !strcmp(caller, "j700_field_update") || + !strcmp(caller, "j700_usb_field_update") || + !strcmp(caller, "atcphy_power_on") || + !strcmp(caller, "atcphy_configure") || + (fixed && (!strcmp(caller, "atcphy_pipehandler_lock") || + !strcmp(caller, "atcphy_pipehandler_unlock")))); + } + } + for (unsigned int fault = 1; fault <= total; fault++) { + orientation_initial(&p, fixed); + orient(&p, first); + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + assert(j700_dp_configure(&p, &rate) == 0); + unsigned int owned = p.dp_owned_pmas; + orient(&p, !first); + polls = 0; + fail_poll = fault; + int ret = select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP); + /* A fatal phase must never be accepted as advisory success. */ + assert((ret == 0) == advisory[fault - 1]); + if (!ret) { + check_route(&p, APPLE_ATCPHY_MODE_USB3_DP, !first); + assert(!p.dp_owned_pmas && !p.dp_idle_pmas); + cases++; + continue; + } + assert(ret == -ETIMEDOUT); + if (p.mode_dirty || p.pipe_pending) { + before = writes; + assert(j700_dp_configure(&p, &training) == -EBUSY && writes == before); + assert(j700_dp_configure(&p, &rate) == -EBUSY && writes == before); + if (fault == 1 && !fixed) + assert(p.dp_owned_pmas == owned && p.aux_power_requested); + } else { + assert(fixed && p.pipe_state == ATCPHY_PIPEHANDLER_STATE_DUMMY); + } + recover(); + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + check_route(&p, APPLE_ATCPHY_MODE_USB3_DP, !first); + assert(!p.dp_owned_pmas && !p.dp_idle_pmas); + assert(j700_dp_configure(&p, &rate) == 0); + assert(p.dp_owned_pmas == BIT(first)); + assert(j700_dp_configure(&p, &training) == 0); + cases++; + } + /* A fatal parking check preserves the old owner and all lane words. */ + if (fixed) { + orientation_initial(&p, true); + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + assert(j700_dp_configure(&p, &rate) == 0); + orient(&p, true); + before = writes; + inject_parking(0); + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == -ETIMEDOUT); + assert(writes == before && p.dp_owned_pmas == 2 && p.mode_dirty); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_USB3 && p.host_active); + recover(); + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + check_route(&p, APPLE_ATCPHY_MODE_USB3_DP, true); + } + /* Reversing a pending flip cannot erase cleanup obligations. */ + orientation_initial(&p, fixed); + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + assert(j700_dp_configure(&p, &rate) == 0); + orient(&p, true); + orient(&p, false); + assert(p.mode_dirty && p.dp_owned_pmas == 2); + p.ss_tunables = false; + before = writes; + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == -EOPNOTSUPP); + assert(writes == before && p.mode_dirty && p.dp_owned_pmas == 2); + p.ss_tunables = true; + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + check_route(&p, APPLE_ATCPHY_MODE_USB3_DP, false); + assert(!p.dp_owned_pmas && !p.dp_idle_pmas); + } + } + printf("PASS: %u orientation transition poll injections, dirty training guard, ownership and retry controls\n", cases); +} +static void backend_change(void) +{ + struct apple_atcphy p; + + orientation_initial(&p, true); + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3) == 0); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_USB3); + require_parked = true; + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + require_parked = false; + check_route(&p, APPLE_ATCPHY_MODE_USB3_DP, false); + puts("PASS: a live fixed-hub USB3 backend parks before same-orientation protocol changes"); +} + + +static void failed_flip_return(void) +{ + struct apple_atcphy p; + unsigned int cases = 0; + struct phy_configure_opts_dp rate = { + .set_rate = true, .set_lanes = true, .link_rate = 8100, .lanes = 2, + }; + + for (unsigned int fixed = 0; fixed < 2; fixed++) { + for (unsigned int first = 0; first < 2; first++) { + orientation_initial(&p, fixed); + orient(&p, first); + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + assert(j700_dp_configure(&p, &rate) == 0); + orient(&p, !first); + polls = 0; + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + unsigned int route_fault = 0; + for (unsigned int i = 0; i < polls; i++) + if (!strcmp(poll_caller[i], "j700_field_update")) route_fault = i + 1; + assert(route_fault); + + orientation_initial(&p, fixed); + orient(&p, first); + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + assert(j700_dp_configure(&p, &rate) == 0); + orient(&p, !first); + polls = 0; + fail_poll = route_fault; + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == -ETIMEDOUT); + /* The route changed before PMA idle completed; completed mode is still old. */ + assert((registers[0x60 / 4] & 0xfff) == (first ? 0x489 : 0x252)); + assert(p.mode_dirty && p.dp_idle_pmas == BIT(first) && !p.dp_owned_pmas); + orient(&p, first); + assert(p.mode_dirty && p.dp_idle_pmas == BIT(first)); + unsigned int before = writes; + assert(j700_dp_configure(&p, &rate) == -EBUSY && writes == before); + recover(); + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + check_route(&p, APPLE_ATCPHY_MODE_USB3_DP, first); + assert(!p.dp_idle_pmas && !p.dp_owned_pmas); + assert(j700_dp_configure(&p, &rate) == 0); + assert(p.dp_owned_pmas == BIT(!first)); + cases++; + } + } + printf("PASS: %u partially applied flip cancellations preserve pending PMA ownership\n", cases); +} + +static void safe_first_recovery(void) +{ + struct apple_atcphy p; + unsigned int cases = 0; + + for (unsigned int first = 0; first < 2; first++) { + orientation_initial(&p, true); + orient(&p, first); + bist_status_stuck = true; + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3) == -ETIMEDOUT); + assert(bist_faults == 2 && p.mode_dirty && p.pipe_pending); + bist_status_stuck = false; + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3) == 0); + check_route(&p, APPLE_ATCPHY_MODE_USB3, first); + polls = 0; + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + unsigned int host_lock_poll = 0; + for (unsigned int i = 0; i < polls; i++) + if (!strcmp(poll_caller[i], "atcphy_pipehandler_lock")) host_lock_poll = i + 1; + assert(host_lock_poll > 1); /* Inject the host lock, after acknowledged parking. */ + orientation_initial(&p, true); + orient(&p, first); + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3) == 0); + polls = 0; + fail_poll = host_lock_poll; + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == -ETIMEDOUT); + assert(p.mode == APPLE_ATCPHY_MODE_USB3_DP && !p.mode_dirty); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_DUMMY && p.host_active); + assert(p.aux_power_requested); + + /* Persistent AUX cleanup failure blocks SAFE before orientation publication. */ + fail_caller = "j700_field_update"; + u32 route = registers[0x60 / 4], crossbar = registers[0x64 / 4]; + for (unsigned int attempt = 0; attempt < 3; attempt++) { + assert(select_mode(&p, APPLE_ATCPHY_MODE_OFF) == -ETIMEDOUT); + assert(p.swap_lanes == first && p.mode_dirty && p.aux_power_requested); + assert(p.mode == APPLE_ATCPHY_MODE_USB3_DP && p.host_active); + assert(registers[0x60 / 4] == route && registers[0x64 / 4] == crossbar); + assert(usb2_regs[USB2PHY_USBCTL / 4] == USB2PHY_USBCTL_RUN); + } + /* Returning the plug cannot clear an unavailable cleanup handshake. */ + assert(select_mode(&p, APPLE_ATCPHY_MODE_OFF) == -ETIMEDOUT); + assert(p.swap_lanes == first && p.mode_dirty); + recover(); /* Model hardware readiness returning; production performs the retry. */ + assert(select_mode(&p, APPLE_ATCPHY_MODE_OFF) == 0); + assert(p.mode == APPLE_ATCPHY_MODE_USB2 && !p.mode_dirty && !p.aux_power_requested); + /* SAFE uses the existing generic USB2 route and clears its swap control. */ + assert(!(registers[ATCPHY_MISC / 4] & ATCPHY_MISC_LANE_SWAP)); + expected_j700_swap = false; + orient(&p, !first); + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + check_route(&p, APPLE_ATCPHY_MODE_USB3_DP, !first); + + /* Real reconnect order: SAFE must succeed before the new orientation is published. */ + struct phy_configure_opts_dp rate = { + .set_rate = true, .set_lanes = true, .link_rate = 8100, .lanes = 2, + }; + assert(j700_dp_configure(&p, &rate) == 0); + assert(p.dp_owned_pmas == BIT(first)); + assert(select_mode(&p, APPLE_ATCPHY_MODE_OFF) == 0); + assert(!p.dp_owned_pmas && !p.dp_idle_pmas && !p.aux_power_requested); + orient(&p, first); + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + check_route(&p, APPLE_ATCPHY_MODE_USB3_DP, first); + assert(j700_dp_configure(&p, &rate) == 0); + assert(p.dp_owned_pmas == BIT(!first)); + unsigned int before = writes, before_mux = mux_writes; + orient(&p, first); + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + assert(writes == before && mux_writes == before_mux); + cases++; + } + printf("PASS: %u chained BIST/host-ACK failure, persistent SAFE failure and SAFE-first roundtrip controls\n", cases); +} + +static void live_parking_handshake(void) +{ + struct apple_atcphy p; + struct phy_configure_opts_dp rate = { + .set_rate = true, .set_lanes = true, .link_rate = 8100, .lanes = 2, + }; + struct phy_configure_opts_dp training = { .set_voltages = true, .lanes = 2 }; + unsigned int cases = 0; + for (unsigned int reverse = 0; reverse < 2; reverse++) { + for (unsigned int fault = 0; fault < 3; fault++) { + orientation_initial(&p, true); + orient(&p, reverse); + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + assert(j700_dp_configure(&p, &rate) == 0); + u32 lanes = registers[0x60 / 4], route = registers[0x64 / 4]; + u32 native = pipe_regs[PIPEHANDLER_NONSELECTED_OVERRIDE / 4]; + unsigned int before = writes, owned = p.dp_owned_pmas; + if (fault == 2) { + /* A previous host unlock left ACK asserted before parking. */ + pipe_regs[PIPEHANDLER_LOCK_ACK / 4] = PIPEHANDLER_LOCK_EN; + hold_unlock_ack = true; + } else if (fault == 1) { + hold_unlock_ack = true; + } else { + stuck_ack = true; + } + for (unsigned int retry = 0; retry < 3; retry++) { + unsigned int before_mux = mux_writes; + assert(select_mode(&p, APPLE_ATCPHY_MODE_OFF) == -ETIMEDOUT); + assert(p.mode == APPLE_ATCPHY_MODE_USB3_DP && p.mode_dirty); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_USB3 && p.host_active); + assert(p.dp_owned_pmas == owned && p.aux_power_requested); + assert(writes == before && registers[0x60 / 4] == lanes && registers[0x64 / 4] == route); + assert(pipe_regs[PIPEHANDLER_NONSELECTED_OVERRIDE / 4] == native); + if (fault != 1) assert(mux_writes == before_mux); + /* A failed SAFE cannot be forgotten by returning to the cached mode. */ + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == -ETIMEDOUT); + assert(j700_dp_configure(&p, &training) == -EBUSY && writes == before); + assert(!usb2_writes && !reset_writes); + } + recover(); + assert(select_mode(&p, APPLE_ATCPHY_MODE_OFF) == 0); + assert(p.mode == APPLE_ATCPHY_MODE_USB2 && !p.mode_dirty && p.host_active); + assert(!p.dp_owned_pmas && !p.dp_idle_pmas && !p.aux_power_requested); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_DUMMY); + expected_j700_swap = false; + orient(&p, !reverse); + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + check_route(&p, APPLE_ATCPHY_MODE_USB3_DP, !reverse); + assert(j700_dp_configure(&p, &rate) == 0 && p.dp_owned_pmas == BIT(reverse)); + cases++; + } + } + printf("PASS: %u live parking entry-ACK/LOCK/UNLOCK failure, ownership, write ordering and retry roundtrips\n", cases); +} + +int main(int argc, char **argv) +{ + assert(argc == 2); + if (!strcmp(argv[1], "mux")) mux_parking(); + else if (!strcmp(argv[1], "power")) power_parking(); + else if (!strcmp(argv[1], "reset")) reset_parking(); + else if (!strcmp(argv[1], "cleanup")) power_cleanup(); + else if (!strcmp(argv[1], "orientation")) orientation_roundtrip(); + else if (!strcmp(argv[1], "orientation-faults")) orientation_faults(); + else if (!strcmp(argv[1], "backend-change")) backend_change(); + else if (!strcmp(argv[1], "safe-first")) safe_first_recovery(); + else if (!strcmp(argv[1], "live-park")) live_parking_handshake(); + else if (!strcmp(argv[1], "failed-flip-return")) failed_flip_return(); + else assert(false); + return 0; +} +''' + return {**locals(), "block": block} + +def adverse(root): + + # Preserve continued source macros; the parking fixture does not use the + # continued PLL clock macro exercised by the legacy rate helper here. + ns = parking(root) + production = ns['production'] + '\n' + ns['function'](ns['atc'], 'atcphy_usb2_init') + production = production.replace('bool pipe_pending, usb_off_pending, usb_off_drained;', + 'bool pipe_pending, usb_off_pending, usb_off_drained, tunnel_clock_on, tunnel_saved;', 1) + production += '\n' + ns['block'](ns['atc'], 'enum atcphy_lane {') + ';' + production += '\n' + ns['block'](ns['atc'], 'struct atcphy_dp_link_rate_configuration {') + ';' + production += '\n' + ns['block'](ns['atc'], 'static const struct atcphy_dp_link_rate_configuration dp_lr_config[] = {') + ';' + # These leaf operations are outside the pending/cache gate under test. + production += r''' +static int atcphy_auspll_apb_command(struct apple_atcphy *p, u32 command) +{ (void)p; (void)command; assert(false); return -EIO; } +static int atcphy_dp_configure_lane(struct apple_atcphy *p, enum atcphy_lane lane, + const struct atcphy_dp_link_rate_configuration *cfg) +{ (void)p; (void)lane; (void)cfg; assert(false); return -EIO; } +''' + production += '\n' + ns['function'](ns['atc'], 'atcphy_dp_configure') + for profile in ('t8103', 't8122', 't8140', 't8142'): + production += '\n' + ns['block'](ns['atc'], + 'static const struct atcphy_hw atcphy_hw_' + profile + ' = {') + ';' + production = production.replace('static unsigned int polls,', 'static int poll_errors[256];\nstatic unsigned int polls,', 1) + production = production.replace('polls == fail_poll || bist_fault ||', + 'poll_errors[polls] ? poll_errors[polls] : polls == fail_poll || bist_fault ||', 1) + production = production.replace('static u32 readl(void *address) { return *(u32 *)address; }', + '''static unsigned int mmio_reads; +static bool only_power_mmio; +static u32 readl(void *address) +{ + mmio_reads++; + if (only_power_mmio && (uintptr_t)address >= (uintptr_t)registers && + (uintptr_t)address < (uintptr_t)registers + sizeof(registers)) { + uintptr_t offset=(uintptr_t)address-(uintptr_t)registers; + assert(offset==ATCPHY_POWER_CTRL || offset==ATCPHY_POWER_STAT || offset==ATCPHY_MISC); + } + return *(u32 *)address; +}''', 1) + production = production.replace(' (void)p;\n assert(offset < sizeof(registers)', + ' (void)p;\n if (only_power_mmio) assert(offset==ATCPHY_POWER_CTRL || offset==ATCPHY_MISC);\n assert(offset < sizeof(registers)', 1) + controls = r''' +static void legacy_setup(struct apple_atcphy *p, unsigned int gen, bool fixed) +{ + hw = gen==0 ? atcphy_hw_t8103 : gen==1 ? atcphy_hw_t8122 : + gen==2 ? atcphy_hw_t8140 : atcphy_hw_t8142; + cold_initial(p, fixed, false); + p->mode = p->typec_mode = APPLE_ATCPHY_MODE_USB3; + registers[ACIOPHY_TOP_PHY_STAT/4] = ACIOPHY_TOP_PHY_STAT_LN0_UNK0; + memset(poll_errors, 0, sizeof(poll_errors)); +} +static void shared_bist(void) +{ + for (unsigned int gen=0; gen<2; gen++) { + unsigned int count=gen ? 5 : 3; + for (unsigned int fault=1; fault<=count; fault++) { + struct apple_atcphy p; + legacy_setup(&p,gen,false); + poll_errors[fault]=-EIO; + if (fault1 && !strcmp(argv[1],"epoch")) { epoch_controls(); return 0; } + if(argc>1 && !strcmp(argv[1],"bist")) { bist_controls(); return 0; } + if(argc>1)return original_fixture_main(argc,argv); + epoch_controls();bist_controls();return 0; +} +''' + return ns['prefix'] + production + ns['suffix'].replace('int main(', 'int original_fixture_main(', 1) + controls + +def warm_reset(root): + + ns = parking(root) + production = ns['production'] + reset_block = '''\t/* +\t * Fixed-hub SAFE keeps USB2 up rather than taking the block OFF. +\t * With PIPE parked and DP owners released, reinitialize USB3 before +\t * programming its lanes; the existing sequence releases this reset. +\t */ +\tif (atcphy->hw->has_t8140_dp && atcphy->fixed_usb2) +\t\tcore_clear32(atcphy, ATCPHY_POWER_CTRL, ATCPHY_POWER_PHY_RESET_N); + +''' + assert production.count(reset_block) == 1 + production = production.replace('static u32 registers[', '''static unsigned int warm_reset_edges, warm_unreset_lanes, warm_ownership_errors; +static u32 registers[''', 1) + production = production.replace(' registers[offset / 4] = (registers[offset / 4] & ~mask) | value;', ''' if (offset == ATCPHY_POWER_CTRL && (mask & ATCPHY_POWER_PHY_RESET_N) && + !(value & ATCPHY_POWER_PHY_RESET_N)) { + warm_reset_edges++; + if (p->fixed_usb2 && p->hw->has_t8140_dp && + (p->pipe_state != ATCPHY_PIPEHANDLER_STATE_DUMMY || p->dp_owned_pmas || + p->dp_idle_pmas || p->aux_power_requested)) warm_ownership_errors++; + } + if (offset == p->hw->aciophy_lane_mode && p->fixed_usb2 && p->hw->has_t8140_dp && + (registers[ATCPHY_POWER_CTRL / 4] & ATCPHY_POWER_PHY_RESET_N)) warm_unreset_lanes++; + registers[offset / 4] = (registers[offset / 4] & ~mask) | value;''', 1) + controls = r''' +static void warm_initial(struct apple_atcphy *p) +{ + orientation_initial(p, true); + registers[ATCPHY_POWER_CTRL / 4] |= ATCPHY_POWER_PHY_RESET_N; + warm_reset_edges = warm_unreset_lanes = warm_ownership_errors = 0; +} +static void warm_roundtrip(void) +{ + bool failed = false; + for (unsigned int first = 0; first < 2; first++) { + struct apple_atcphy p; + const struct phy_configure_opts_dp rate = { + .set_rate = true, .set_lanes = true, .link_rate = 8100, .lanes = 2, + }; + warm_initial(&p); + for (unsigned int attach = 0; attach < 3; attach++) { + orient(&p, first ^ (attach & 1)); + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_USB3 && !p.mode_dirty); + assert(j700_dp_configure(&p, &rate) == 0); + unsigned int edges = warm_reset_edges; + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + assert(warm_reset_edges == edges); /* cached same-mode must not reset */ + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB2) == 0); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_DUMMY && p.host_active); + assert(!p.dp_owned_pmas && !p.dp_idle_pmas && !p.aux_power_requested); + assert(registers[ATCPHY_POWER_CTRL / 4] & ATCPHY_POWER_PHY_RESET_N); + assert(!reset_writes && !usb2_writes); + } + assert(usb2_on == 6); /* no extra USB2 power-on call introduced */ + printf("first=%u reset_assertions=%u unreset_lane_writes=%u ownership_errors=%u\n", + first, warm_reset_edges, warm_unreset_lanes, warm_ownership_errors); + failed |= warm_reset_edges != 6 || warm_unreset_lanes || warm_ownership_errors; + } + fflush(stdout); + assert(!failed); +} +static void warm_failure_controls(void) +{ + struct apple_atcphy p; + const struct phy_configure_opts_dp rate = { + .set_rate = true, .set_lanes = true, .link_rate = 8100, .lanes = 2, + }; + warm_initial(&p); + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + assert(j700_dp_configure(&p, &rate) == 0); + unsigned int edges = warm_reset_edges, owned = p.dp_owned_pmas; + stuck_ack = true; + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB2) == -ETIMEDOUT); + assert(warm_reset_edges == edges && p.dp_owned_pmas == owned && p.mode_dirty); + assert(!usb2_writes && !reset_writes); + + warm_initial(&p); + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + assert(j700_dp_configure(&p, &rate) == 0); + edges = warm_reset_edges; owned = p.dp_owned_pmas; + fail_caller = "j700_field_update"; + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB2) == -ETIMEDOUT); + assert(warm_reset_edges == edges && p.dp_owned_pmas == owned && p.mode_dirty); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_DUMMY); + assert(!usb2_writes && !reset_writes); + + warm_initial(&p); + fail_caller = "atcphy_configure"; /* existing RCAL poll, after reset release */ + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == -ETIMEDOUT); + assert(warm_reset_edges == 1 && !warm_ownership_errors && p.mode_dirty); + assert(registers[ATCPHY_POWER_CTRL / 4] & ATCPHY_POWER_PHY_RESET_N); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_DUMMY); + struct phy phy = { &p }; + unsigned int before = writes; + assert(atcphy_usb3_set_mode(&phy, PHY_MODE_USB_HOST, 0) == -EBUSY); + assert(j700_dp_configure(&p, &rate) == -EBUSY && writes == before); + fail_caller = NULL; + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + assert(!p.mode_dirty && !warm_ownership_errors); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_USB3); + assert(!usb2_writes && !reset_writes); + puts("PASS: park/DP-cleanup failures precede reset; released-reset RCAL failure stays dirty and retryable"); +} +static void warm_scope_controls(void) +{ + struct apple_atcphy p; + for (unsigned int fixed = 0; fixed < 2; fixed++) { + for (unsigned int t8140 = 0; t8140 < 2; t8140++) { + orientation_initial(&p, fixed); + hw.has_t8140_dp = t8140; + registers[ATCPHY_POWER_CTRL / 4] |= ATCPHY_POWER_PHY_RESET_N; + warm_reset_edges = warm_unreset_lanes = warm_ownership_errors = 0; + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB3_DP) == 0); + assert(warm_reset_edges == (fixed && t8140)); + assert(!warm_ownership_errors && !usb2_writes && !reset_writes); + } + } + hw.has_t8140_dp = true; + puts("PASS: reset scope requires both fixed hub and T8140; ordinary/legacy paths unchanged"); +} +int main(int argc, char **argv) +{ + if (argc > 1) return original_fixture_main(argc, argv); + warm_roundtrip(); warm_failure_controls(); warm_scope_controls(); + puts("PASS: warm reset source ordering; no electrical recovery is modeled"); + return 0; +} +''' + suffix = ns['suffix'].replace('int main(', 'int original_fixture_main(', 1) + controls + return ns['prefix'] + production + suffix + +def usb_context(root): + ns = parking(root) + production=ns['production'].replace('static u32 registers[', 'static u32 blocked_offset;\nstatic unsigned int mmio_reads;\nstatic bool track_dp_writes;\nstatic u32 dp_write_masks[0x30000 / 4];\nstatic u32 registers[', 1) + production=production.replace('static u32 readl(void *address) { return *(u32 *)address; }', 'static u32 readl(void *address) { mmio_reads++; return *(u32 *)address; }', 1) + production=production.replace(' registers[offset / 4] = (registers[offset / 4] & ~mask) | value;', ' if (track_dp_writes) dp_write_masks[offset / 4] |= mask | value;\n if (blocked_offset && offset == blocked_offset) { writes++; return; }\n registers[offset / 4] = (registers[offset / 4] & ~mask) | value;', 1) + controls = r''' +static void seed_usb(struct apple_atcphy *p, unsigned int first) +{ + orientation_initial(p, true); + registers[0x200 / 4] = 0xee; + registers[0xc / 4] = 0; + /* Synthetic USB context, never a private factory table or captured register image. */ + for (unsigned int offset = 0x9000; offset < 0x15000; offset += 4) + registers[offset / 4] = 0x5a5a1234 ^ offset; + registers[0x8 / 4] = 0xa5000000; + registers[0x10c / 4] = 0xa5000000; + orient(p, first); + assert(select_mode(p, APPLE_ATCPHY_MODE_USB3_DP) == 0); +} + +static void mask_coverage(void) +{ + struct apple_atcphy p; + memset(dp_write_masks, 0, sizeof(dp_write_masks)); + seed_usb(&p, 1); + for (unsigned int rate = 0; rate < 4; rate++) { + track_dp_writes = true; + assert(j700_dp_start(&p, 1, rate, false) == 0); + j700_dp_equalize(&p, 0, true, 0x123); + j700_dp_equalize(&p, 0, false, 0x123); + assert(j700_dp_stop(&p) == 0); + track_dp_writes = false; + } + unsigned int entries = 0; + for (unsigned int offset = 0; offset < sizeof(dp_write_masks); offset += 4) { + if (!((offset >= 0x9000 && offset <= 0xdfff) || offset == 8 || + offset == 0xc || offset == 0x10c || offset == 0x200)) continue; + u32 expected = 0; + for (unsigned int i = 0; i < ARRAY_SIZE(j700_usb_fields); i++) + if (j700_usb_fields[i].offset == offset) expected = j700_usb_fields[i].mask; + assert(dp_write_masks[offset / 4] == expected); + entries += !!expected; + } + assert(entries == 27); + puts("PASS: 27 masks equal the actual public DP start/voltage/stop write union at every rate"); +} +static void masked_roundtrip(void) +{ + const struct phy_configure_opts_dp rate = { + .set_rate=true, .set_lanes=true, .link_rate=8100, .lanes=2, + }; + bool failure=false; + for (unsigned int first=0; first<2; first++) { + struct apple_atcphy p; + seed_usb(&p, first); + unsigned int old_dp=!first; +#ifdef PROPOSAL + assert(p.dp_usb_context_valid & BIT(old_dp)); + assert(p.dp_usb_context[old_dp][1] == 0); + assert(p.dp_usb_context[old_dp][3] == (0xeU << (4 * old_dp))); + assert(((registers[0x200 / 4] >> (4 * old_dp)) & 0xf) == 1); +#endif + u32 saved[27], outside[27]; + for (unsigned int i=0;i<27;i++) { + u32 offset,mask; + j700_usb_field(i,old_dp,&offset,&mask); + saved[i]=core_read32(&p,offset)&mask; + } + /* Power/receiver were saved before route idle, not at this post-route sample. */ + saved[1]=0; saved[3]=0xeU<<(4*old_dp); + assert(j700_dp_configure(&p,&rate)==0); + unsigned int edges_before=writes; + assert(select_mode(&p,APPLE_ATCPHY_MODE_USB3_DP)==0); + assert(writes==edges_before); /* same mode never resets or recaptures */ + assert(select_mode(&p,APPLE_ATCPHY_MODE_USB2)==0); + for(unsigned int i=0;i<27;i++) { + u32 offset,mask;j700_usb_field(i,old_dp,&offset,&mask); + u32 other_offset,other_mask; j700_usb_field(i,!old_dp,&other_offset,&other_mask); + outside[i]=registers[offset/4]&~(mask | (i<4 ? other_mask : 0)); + } + orient(&p,!first); + assert(select_mode(&p,APPLE_ATCPHY_MODE_USB3_DP)==0); + unsigned int mismatch=0; + for(unsigned int i=0;i<27;i++) { + u32 offset,mask;j700_usb_field(i,old_dp,&offset,&mask); + if((registers[offset/4]&mask)!=saved[i]) mismatch++; + u32 other_offset,other_mask; j700_usb_field(i,!old_dp,&other_offset,&other_mask); + assert((registers[offset/4]&~(mask | (i<4 ? other_mask : 0)))==outside[i]); + } + printf("first=%u former_dp_pma=%u masked_mismatches=%u receiver=%#x power=%#x\n", + first,old_dp,mismatch,registers[0xc/4],registers[0x200/4]); + failure |= mismatch!=0; + assert(!usb2_writes&&!reset_writes); + assert(j700_dp_configure(&p,&rate)==0); + assert(select_mode(&p,APPLE_ATCPHY_MODE_USB2)==0); + orient(&p,first); + assert(select_mode(&p,APPLE_ATCPHY_MODE_USB3_DP)==0); + assert(!p.mode_dirty&&p.pipe_state==ATCPHY_PIPEHANDLER_STATE_USB3); + } + fflush(stdout);assert(!failure); +} +static void restore_controls(void) +{ +#ifdef PROPOSAL + struct apple_atcphy p; + const struct phy_configure_opts_dp rate={.set_rate=true,.set_lanes=true,.link_rate=8100,.lanes=2}; + seed_usb(&p,1); + assert(j700_dp_configure(&p,&rate)==0); + unsigned int valid=p.dp_usb_context_valid,dirty=p.dp_usb_context_dirty; + u32 snapshot[2][27];memcpy(snapshot,p.dp_usb_context,sizeof(snapshot)); + stuck_ack=true; + assert(select_mode(&p,APPLE_ATCPHY_MODE_USB2)==-ETIMEDOUT); + assert(p.dp_owned_pmas==1&&p.dp_usb_context_valid==valid&&p.dp_usb_context_dirty==dirty); + assert(!memcmp(snapshot,p.dp_usb_context,sizeof(snapshot))); + recover(); + fail_caller="j700_field_update"; + assert(select_mode(&p,APPLE_ATCPHY_MODE_USB2)==-ETIMEDOUT); + assert(p.dp_owned_pmas==1&&p.dp_usb_context_valid==valid); + assert(!memcmp(snapshot,p.dp_usb_context,sizeof(snapshot))); + recover();assert(select_mode(&p,APPLE_ATCPHY_MODE_USB2)==0); + orient(&p,0); + assert(j700_usb_capture(&p,APPLE_ATCPHY_MODE_USB3_DP)==0); + p.dp_owned_pmas=1; + unsigned int before=writes; + assert(j700_usb_restore(&p,APPLE_ATCPHY_MODE_USB3_DP)==-EBUSY&&writes==before); + p.dp_owned_pmas=0;p.dp_idle_pmas=1; + assert(j700_usb_restore(&p,APPLE_ATCPHY_MODE_USB3_DP)==-EBUSY&&writes==before); + p.dp_idle_pmas=0;p.pipe_state=ATCPHY_PIPEHANDLER_STATE_USB3; + assert(j700_usb_restore(&p,APPLE_ATCPHY_MODE_USB3_DP)==-EBUSY&&writes==before); + p.pipe_state=ATCPHY_PIPEHANDLER_STATE_DUMMY; + fail_caller="j700_usb_field_update"; + assert(j700_usb_restore(&p,APPLE_ATCPHY_MODE_USB3_DP)==-ETIMEDOUT); + assert(p.dp_usb_context_valid&1); + assert(p.dp_usb_context_dirty&1); + recover();assert(j700_usb_restore(&p,APPLE_ATCPHY_MODE_USB3_DP)==0); + assert(!(p.dp_usb_context_valid&1)&&!(p.dp_usb_context_dirty&1)); + seed_usb(&p,1); + assert(j700_dp_configure(&p,&rate)==0); + assert(select_mode(&p,APPLE_ATCPHY_MODE_USB2)==0); + orient(&p,0); + u32 old_context[27];memcpy(old_context,p.dp_usb_context[0],sizeof(old_context)); + fail_caller="j700_usb_field_update"; + assert(select_mode(&p,APPLE_ATCPHY_MODE_USB3_DP)==-ETIMEDOUT); + assert(p.mode_dirty&&p.pipe_state==ATCPHY_PIPEHANDLER_STATE_DUMMY); + assert((p.dp_usb_context_valid&1)&&(p.dp_usb_context_dirty&1)); + assert(!memcmp(old_context,p.dp_usb_context[0],sizeof(old_context))); + before=writes; + assert(j700_dp_configure(&p,&rate)==-EBUSY&&writes==before); + recover();assert(select_mode(&p,APPLE_ATCPHY_MODE_USB3_DP)==0); + assert(!p.mode_dirty&&!(p.dp_usb_context_valid&1)); + seed_usb(&p,1); + assert(j700_dp_configure(&p,&rate)==0); + assert(select_mode(&p,APPLE_ATCPHY_MODE_USB2)==0); + orient(&p,0); + blocked_offset=0x9244; + assert(select_mode(&p,APPLE_ATCPHY_MODE_USB3_DP)==-EIO); + assert(p.mode_dirty&&p.pipe_state==ATCPHY_PIPEHANDLER_STATE_DUMMY); + assert((p.dp_usb_context_valid&1)&&(p.dp_usb_context_dirty&1)); + before=writes; + assert(j700_dp_configure(&p,&rate)==-EBUSY&&writes==before); + blocked_offset=0; + assert(select_mode(&p,APPLE_ATCPHY_MODE_USB3_DP)==0&&!p.mode_dirty); + puts("PASS: actual mux static/power restore failures keep dirty route/context, block training and retry"); + seed_usb(&p,1); + assert(j700_dp_configure(&p,&rate)==0); + assert(select_mode(&p,APPLE_ATCPHY_MODE_USB2)==0); + orient(&p,0); + registers[ACIOPHY_BIST_MAC_USBPHY_STATUS2/4]=BIT(7); + bist_status_stuck=true; bist_faults=0; + assert(select_mode(&p,APPLE_ATCPHY_MODE_USB3_DP)==-ETIMEDOUT); + assert(p.mode_dirty && p.pipe_pending); + assert(bist_faults==2 && (registers[ACIOPHY_BIST_MAC_USBPHY_STATUS2/4]&BIT(7))); + assert((registers[0x200/4]&0xf)==0xe && !(registers[0xc/4]&0x44)); + stuck_ack=true; + before=writes; + assert(select_mode(&p,APPLE_ATCPHY_MODE_USB2)==-ETIMEDOUT && writes==before); + puts("PASS: restoration never clears simulated BIST/ACK faults or forces readiness"); + for(unsigned int fixed=0;fixed<2;fixed++) { + seed_usb(&p,0);p.fixed_usb2=fixed;hw.has_t8140_dp=false; + before=writes; + assert(j700_usb_capture(&p,APPLE_ATCPHY_MODE_USB3_DP)==0); + assert(j700_usb_restore(&p,APPLE_ATCPHY_MODE_USB3_DP)==0&&writes==before); + hw.has_t8140_dp=true; + } + puts("PASS: park/DP-idle failure retention, immutable snapshot, live/pending-owner restore guards, power readback failure/retry, legacy exclusions"); +#endif +} + +static void inactive_host_gates(void) +{ +#ifdef PROPOSAL + const struct phy_configure_opts_dp rate = { + .set_rate=true, .set_lanes=true, .link_rate=8100, .lanes=2, + }; + unsigned int violations = 0; + for (unsigned int backend = ATCPHY_PIPEHANDLER_STATE_USB3; + backend <= ATCPHY_PIPEHANDLER_STATE_USB4; backend++) { + for (unsigned int entry = 0; entry < 3; entry++) { + struct apple_atcphy p; + seed_usb(&p, 1); + assert(j700_dp_configure(&p, &rate) == 0); + assert(select_mode(&p, APPLE_ATCPHY_MODE_USB2) == 0); + orient(&p, 0); + p.host_active = false; + p.pipe_state = backend; + unsigned int before_writes = writes, before_reads = mmio_reads; + unsigned int valid = p.dp_usb_context_valid, dirty = p.dp_usb_context_dirty; + u32 snapshot[2][27]; memcpy(snapshot, p.dp_usb_context, sizeof(snapshot)); + int ret = entry == 0 ? j700_usb_capture(&p, APPLE_ATCPHY_MODE_USB3_DP) : + entry == 1 ? j700_usb_restore(&p, APPLE_ATCPHY_MODE_USB3_DP) : + atcphy_configure(&p, APPLE_ATCPHY_MODE_USB3_DP); + bool blocked = ret == -EBUSY && writes == before_writes && + mmio_reads == before_reads && valid == p.dp_usb_context_valid && + dirty == p.dp_usb_context_dirty && + !memcmp(snapshot, p.dp_usb_context, sizeof(snapshot)); + printf("inactive-host backend=%u entry=%u ret=%d new_writes=%u new_reads=%u blocked=%u\n", + backend, entry, ret, writes-before_writes, mmio_reads-before_reads, blocked); + violations += !blocked; + } + } + fflush(stdout); assert(!violations); + puts("PASS: six inactive-host/live-PIPE entry gates retain snapshots and perform zero MMIO"); +#endif +} +static void failure_diagnostics(void) +{ +#ifdef PROPOSAL + struct apple_atcphy p; + const struct phy_configure_opts_dp rate={.set_rate=true,.set_lanes=true,.link_rate=8100,.lanes=2}; + const char *stages[] = { "stage=static", "stage=power", "stage=receiver" }; + for (unsigned int stage=0; stage<3; stage++) { + seed_usb(&p,1); assert(j700_dp_configure(&p,&rate)==0); + assert(select_mode(&p,APPLE_ATCPHY_MODE_USB2)==0); orient(&p,0); + diagnostic_records=0; last_diagnostic[0]=0; + blocked_offset=stage==0 ? 0x9244 : stage==1 ? 0x200 : 0xc; + int ret=select_mode(&p,APPLE_ATCPHY_MODE_USB3_DP); + assert(ret==(stage==0 ? -EIO : -ETIMEDOUT)); + assert(diagnostic_records==1 && strstr(last_diagnostic,stages[stage])); + assert(strstr(last_diagnostic,"pma=0") && strstr(last_diagnostic,"reg=")); + assert(strstr(last_diagnostic,"mask=") && strstr(last_diagnostic,"expected=")); + assert(strstr(last_diagnostic,"observed=")); + assert(p.mode_dirty && (p.dp_usb_context_valid&1) && (p.dp_usb_context_dirty&1)); + blocked_offset=0; + } + puts("PASS: static/power/receiver errors preserve state and report stage/PMA/register/mask/expected/final observed"); +#endif +} +int main(int argc,char **argv) +{ + if(argc>1 && !strcmp(argv[1],"inactive-host")) { inactive_host_gates(); return 0; } + if(argc>1)return original_fixture_main(argc,argv); + mask_coverage();masked_roundtrip();restore_controls();inactive_host_gates();failure_diagnostics(); + puts("PASS: masked restoration roundtrips; no BIST readiness or USB speed is modeled"); + return 0; +} +''' + logger = r''' +#include +static unsigned int diagnostic_records; +static char last_diagnostic[512]; +static void record_debug(const char *format, ...) +{ + if (!strstr(format, "USB restore stage=")) return; + va_list args; va_start(args,format); + vsnprintf(last_diagnostic,sizeof(last_diagnostic),format,args); + va_end(args); diagnostic_records++; + fputs(last_diagnostic,stdout); +} +#undef dev_dbg +#define dev_dbg(dev, format, ...) record_debug(format, ##__VA_ARGS__) +''' + return ns["prefix"] + logger + "#define PROPOSAL\n" + production + ns["suffix"].replace("int main(", "int original_fixture_main(", 1) + controls + +def pipe(root): + + source = (root / 'drivers/phy/apple/atc.c').read_text() + dp = (root / 'drivers/phy/apple/atc-j700-dp.h').read_text() + + + def block(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(name): + if name == 'atcphy_mux_set' and 'static int atcphy_mux_set_locked(' in source: + marker = re.search(r'^static [^\n]+\batcphy_mux_set_locked\(', source, re.M).group() + return block(marker).replace('atcphy_mux_set_locked(', 'atcphy_mux_set(', 1) + return block(re.search(r'^static [^\n]+\b' + name + r'\(', source, re.M).group()) + + + parts = re.findall(r'^#define [^\n]*(?:\\\n[^\n]*)*', source, re.M) + for name in ('atcphy_generation', 'atcphy_lane_mode', 'atcphy_pipehandler_state', 'atcphy_mode'): + parts.append(block('enum ' + name + ' {') + ';') + for name in ('atcphy_mode_configuration', 'atcphy_hw'): + parts.append(block('struct ' + name + ' {') + ';') + parts.append(r''' +struct apple_atcphy { + const struct atcphy_hw *hw; + struct { unsigned char *core, *pipehandler; } regs; + void *dev; + int lock; + bool fixed_usb2, host_active, ss_tunables, dp_tunables, mode_dirty; + unsigned int dp_owned_pmas, dp_idle_pmas; + unsigned int dp_usb_context_valid, dp_usb_context_dirty; + u32 dp_usb_context[2][27]; + bool pipe_pending, usb_off_pending, usb_off_drained; + bool swap_lanes; + enum atcphy_mode mode, typec_mode; + enum atcphy_pipehandler_state pipe_state; +}; +''') + start = source.index('static const struct {\n\tconst struct atcphy_mode_configuration normal;') + parts.append(source[start:source.index('\n};', start) + 3]) + parts.append(r''' +static u32 pipe_regs[0x1000 / 4], core_regs[0x30000 / 4]; +static unsigned int polls, fail_poll, mux_writes, lane_calls; +static bool stuck_ack; +static u32 readl(void *address) { return *(u32 *)address; } +static void mask32(void *address, u32 mask, u32 value) +{ + *(u32 *)address = (*(u32 *)address & ~mask) | value; + if (address == (void *)&pipe_regs[PIPEHANDLER_LOCK_REQ / 4] && !stuck_ack) + pipe_regs[PIPEHANDLER_LOCK_ACK / 4] = *(u32 *)address & PIPEHANDLER_LOCK_EN; + if (address == (void *)&pipe_regs[PIPEHANDLER_MUX_CTRL / 4]) + mux_writes++; +} +static void set32(void *p, u32 v) { mask32(p, 0, v); } +static void clear32(void *p, u32 v) { mask32(p, v, 0); } +static void writel(u32 v, void *p) { mask32(p, ~0U, v); } +static void core_mask32(struct apple_atcphy *p, u32 o, u32 m, u32 v) +{ assert(o < sizeof(core_regs) && o % 4 == 0); mask32(p->regs.core + o, m, v); } +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); } +#define readl_poll_timeout_atomic readl_poll_timeout +#define readl_poll_timeout(address, value, condition, interval, timeout) \ + ({ (value) = readl(address); polls++; \ + polls == fail_poll || !(condition) ? -ETIMEDOUT : 0; }) +/* Lane configuration is outside this test: count calls and commit its result. */ +/* Observation is read-only and separately tested; retain production caller control flow. */ +static int atcphy_configure(struct apple_atcphy *p, enum atcphy_mode mode); +''') + for name in ('core_read32', 'atcphy_get_mode_config'): + parts.append(function(name)) + # Actual restoration functions and gates; analog lane setup stays outside this PIPE unit. + parts.append(dp[dp.index('/* Masks are the union'):dp.index('/* Retry only the idle operations')]) + parts.append(r''' +static int atcphy_configure(struct apple_atcphy *p, enum atcphy_mode mode) +{ + int ret; + lane_calls++; p->mode_dirty = true; + ret = j700_usb_capture(p, mode); + if (!ret) ret = j700_usb_restore(p, mode); + if (ret) return ret; + p->mode = mode; p->mode_dirty = false; return 0; +} +''') + for name in ('atcphy_pipehandler_lock', 'atcphy_pipehandler_unlock', 'atcphy_pipehandler_check', + 'atcphy_pipehandler_set_mux', 'atcphy_clear_nonselected_phy_reset', + 'atcphy_configure_pipehandler_usb3_host_t8122', + 'atcphy_configure_pipehandler_usb3_host_t8103', + 'atcphy_configure_pipehandler_usb3', 'atcphy_configure_pipehandler_usb4', + 'atcphy_enable_dummy_phy', 'atcphy_configure_pipehandler_dummy', + 'atcphy_configure_pipehandler', 'atcphy_park_pipehandler', + 'atcphy_usb3_set_mode', 'atcphy_mux_set'): + parts.append(function(name)) + production = '\n'.join(parts) + prefix = r''' +#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 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 field_prep FIELD_PREP +#define dev_dbg(...) ((void)0) +#define dev_warn(...) ((void)0) +#define dev_err(...) ((void)0) +#define lockdep_assert_held(...) ((void)0) +#define WARN_ON_ONCE(v) assert(!(v)) +#define guard(kind) (void) +#define fallthrough __attribute__((fallthrough)) +#define usleep_range(...) ((void)0) +#define udelay(n) ((void)(n)) +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 }; +enum phy_mode { PHY_MODE_USB_HOST, PHY_MODE_USB_DEVICE, PHY_MODE_INVALID }; +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; }; +struct phy { struct apple_atcphy *data; }; +#define typec_mux_get_drvdata(mux) ((mux)->data) +#define phy_get_drvdata(phy) ((phy)->data) +''' + suffix = r''' +static struct atcphy_hw hw = { + .gen = ATCPHY_GENERATION_T8122, .has_t8140_dp = true, + .pipe_mux_clk = PIPEHANDLER_MUX_CTRL_CLK_T8103, + .pipe_mux_data = PIPEHANDLER_MUX_CTRL_DATA_T8103, +}; +static void initial(struct apple_atcphy *p, enum atcphy_mode mode, + enum atcphy_pipehandler_state backend) +{ + memset(pipe_regs, 0, sizeof(pipe_regs)); + memset(core_regs, 0, sizeof(core_regs)); + core_regs[ACIOPHY_TOP_PHY_STAT / 4] = ACIOPHY_TOP_PHY_STAT_LN0_UNK0; + *p = (struct apple_atcphy) { + .hw = &hw, .regs.core = (unsigned char *)core_regs, + .regs.pipehandler = (unsigned char *)pipe_regs, + .mode = mode, .typec_mode = mode, .pipe_state = backend, + .ss_tunables = true, .dp_tunables = true, + }; + polls = fail_poll = mux_writes = lane_calls = 0; + stuck_ack = false; +} +static int request(struct apple_atcphy *p, enum atcphy_mode mode) +{ + struct typec_mux_dev mux = { p }; + struct typec_altmode dp = { .svid = USB_TYPEC_DP_SID }; + struct enter_usb_data data = { .eudo = EUDO_USB_MODE_USB4 }; + struct typec_mux_state state = { .mode = TYPEC_STATE_SAFE }; + if (mode == APPLE_ATCPHY_MODE_USB3) + state.mode = TYPEC_STATE_USB; + else if (mode == APPLE_ATCPHY_MODE_USB3_DP || mode == APPLE_ATCPHY_MODE_DP) { + state.mode = mode == APPLE_ATCPHY_MODE_DP ? TYPEC_DP_STATE_C : TYPEC_DP_STATE_D; + state.alt = &dp; + } else if (mode == APPLE_ATCPHY_MODE_USB4) { + state.mode = TYPEC_MODE_USB4; + state.data = &data; + } + return atcphy_mux_set(&mux, &state); +} +static void backend_failure(void) +{ + struct apple_atcphy p; + unsigned int cases = 0; + for (enum atcphy_mode mode = APPLE_ATCPHY_MODE_OFF; mode <= APPLE_ATCPHY_MODE_DP; mode++) { + enum atcphy_pipehandler_state target = atcphy_modes[mode].pipehandler_state; + enum atcphy_pipehandler_state old = target == ATCPHY_PIPEHANDLER_STATE_DUMMY ? + ATCPHY_PIPEHANDLER_STATE_USB3 : ATCPHY_PIPEHANDLER_STATE_DUMMY; + for (unsigned int host = 0; host < 2; host++) { + initial(&p, mode, old); + /* A previous lock ACK cannot be cleared: fatal before mux writes. */ + pipe_regs[PIPEHANDLER_LOCK_ACK / 4] = PIPEHANDLER_LOCK_EN; + stuck_ack = true; + assert(atcphy_configure_pipehandler(&p, host) == -ETIMEDOUT); + assert(p.pipe_state == old && mux_writes == 0); + stuck_ack = false; + assert(atcphy_configure_pipehandler(&p, host) == 0); + assert(p.pipe_state == target && mux_writes == 3); + const unsigned int data[] = { PIPEHANDLER_MUX_CTRL_DATA_DUMMY, + PIPEHANDLER_MUX_CTRL_DATA_USB3, PIPEHANDLER_MUX_CTRL_DATA_USB4 }; + const unsigned int clk[] = { PIPEHANDLER_MUX_CTRL_CLK_DUMMY, + PIPEHANDLER_MUX_CTRL_CLK_USB3, PIPEHANDLER_MUX_CTRL_CLK_USB4 }; + u32 mux = pipe_regs[PIPEHANDLER_MUX_CTRL / 4]; + assert(FIELD_GET(hw.pipe_mux_data, mux) == data[target]); + assert(FIELD_GET(hw.pipe_mux_clk, mux) == clk[target]); + cases++; + } + } + printf("PASS: %u backend/role check-failure and retry cases\n", cases); +} +static void host_lock_failure(void) +{ + struct apple_atcphy p; + for (unsigned int gen = 0; gen < 2; gen++) { + hw.gen = gen ? ATCPHY_GENERATION_T8122 : ATCPHY_GENERATION_T8103; + initial(&p, APPLE_ATCPHY_MODE_USB3, ATCPHY_PIPEHANDLER_STATE_DUMMY); + struct phy phy = { &p }; + stuck_ack = true; + assert(atcphy_usb3_set_mode(&phy, PHY_MODE_USB_HOST, 0) == -ETIMEDOUT); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_DUMMY && mux_writes == 0); + assert(!(pipe_regs[PIPEHANDLER_LOCK_REQ / 4] & PIPEHANDLER_LOCK_EN)); + stuck_ack = false; + assert(atcphy_usb3_set_mode(&phy, PHY_MODE_USB_HOST, 0) == 0); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_USB3 && mux_writes == 6); + unsigned int before = polls; + assert(atcphy_usb3_set_mode(&phy, PHY_MODE_USB_HOST, 0) == 0); + assert(polls == before && mux_writes == 6); + } + puts("PASS: both USB3 host BIST generations retain fatal lock ACK and retry"); +} +static void mux_retry(void) +{ + struct apple_atcphy p; + const enum atcphy_mode modes[] = { APPLE_ATCPHY_MODE_USB3, APPLE_ATCPHY_MODE_USB3_DP }; + for (unsigned int m = 0; m < 2; m++) { + initial(&p, APPLE_ATCPHY_MODE_USB2, ATCPHY_PIPEHANDLER_STATE_DUMMY); + p.fixed_usb2 = p.host_active = true; + stuck_ack = true; + assert(request(&p, modes[m]) == -ETIMEDOUT); + assert(p.mode == modes[m] && !p.mode_dirty && lane_calls == 1); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_DUMMY && mux_writes == 0); + p.dp_owned_pmas = 3; /* PIPE retry must not reconfigure an owned DP mode. */ + assert(request(&p, modes[m]) == -ETIMEDOUT); + assert(lane_calls == 1 && p.dp_owned_pmas == 3 && mux_writes == 0); + stuck_ack = false; + assert(request(&p, modes[m]) == 0); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_USB3 && lane_calls == 1); + assert(p.dp_owned_pmas == 3 && mux_writes == 6); + unsigned int before = polls; + assert(request(&p, modes[m]) == 0 && polls == before && lane_calls == 1); + } + puts("PASS: fixed-hub same-mode retry preserves lanes and DP ownership"); +} +static void same_mode_pending(void) +{ + struct apple_atcphy p; + initial(&p, APPLE_ATCPHY_MODE_USB3, ATCPHY_PIPEHANDLER_STATE_DUMMY); + p.fixed_usb2 = p.host_active = true; + stuck_ack = true; + assert(request(&p, APPLE_ATCPHY_MODE_USB3) == -ETIMEDOUT); + assert(lane_calls == 0 && mux_writes == 0); + stuck_ack = false; + assert(request(&p, APPLE_ATCPHY_MODE_USB3) == 0); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_USB3 && lane_calls == 0); + puts("PASS: same-mode pending PIPE setup independently retries"); +} +static void parking_and_guards(void) +{ + struct apple_atcphy p; + initial(&p, APPLE_ATCPHY_MODE_USB3, ATCPHY_PIPEHANDLER_STATE_USB3); + pipe_regs[PIPEHANDLER_LOCK_ACK / 4] = PIPEHANDLER_LOCK_EN; + stuck_ack = true; + assert(atcphy_park_pipehandler(&p) == -ETIMEDOUT); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_USB3 && mux_writes == 0); + stuck_ack = false; + assert(atcphy_park_pipehandler(&p) == 0); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_DUMMY && mux_writes == 3); + assert(atcphy_park_pipehandler(&p) == 0); + assert(mux_writes == 3); + initial(&p, APPLE_ATCPHY_MODE_USB2, ATCPHY_PIPEHANDLER_STATE_DUMMY); + p.fixed_usb2 = p.host_active = true; + assert(request(&p, APPLE_ATCPHY_MODE_OFF) == 0); + assert(p.mode == APPLE_ATCPHY_MODE_USB2 && lane_calls == 0 && mux_writes == 0); + p.mode_dirty = true; + assert(request(&p, APPLE_ATCPHY_MODE_OFF) == 0 && lane_calls == 1 && !p.mode_dirty); + /* Inactive hosts and ordinary ports retain their deferred PIPE setup. */ + initial(&p, APPLE_ATCPHY_MODE_USB3, ATCPHY_PIPEHANDLER_STATE_DUMMY); + p.fixed_usb2 = true; + assert(request(&p, APPLE_ATCPHY_MODE_USB3) == 0 && mux_writes == 0); + p.fixed_usb2 = false; + p.host_active = true; + assert(request(&p, APPLE_ATCPHY_MODE_USB3) == 0 && mux_writes == 0); + /* Capability rejection must still precede mode configuration. */ + initial(&p, APPLE_ATCPHY_MODE_USB2, ATCPHY_PIPEHANDLER_STATE_DUMMY); + p.fixed_usb2 = p.host_active = true; + p.ss_tunables = false; + assert(request(&p, APPLE_ATCPHY_MODE_USB3) == -EOPNOTSUPP && lane_calls == 0); + p.ss_tunables = true; + p.dp_tunables = false; + assert(request(&p, APPLE_ATCPHY_MODE_USB3_DP) == -EOPNOTSUPP && lane_calls == 0); + assert(request(&p, APPLE_ATCPHY_MODE_USB4) == -EOPNOTSUPP && lane_calls == 0); + puts("PASS: parking retry, SAFE fixed USB2, dirty-mode and capability guards"); +} +static void requested_lock(void) +{ + struct apple_atcphy p; + initial(&p, APPLE_ATCPHY_MODE_USB3, ATCPHY_PIPEHANDLER_STATE_DUMMY); + pipe_regs[PIPEHANDLER_LOCK_REQ / 4] = PIPEHANDLER_LOCK_EN; + stuck_ack = true; + assert(atcphy_pipehandler_lock(&p) == -ETIMEDOUT && polls == 1); + assert(!(pipe_regs[PIPEHANDLER_LOCK_REQ / 4] & PIPEHANDLER_LOCK_EN)); + pipe_regs[PIPEHANDLER_LOCK_REQ / 4] = PIPEHANDLER_LOCK_EN; + pipe_regs[PIPEHANDLER_LOCK_ACK / 4] = PIPEHANDLER_LOCK_EN; + assert(atcphy_pipehandler_lock(&p) == 0 && polls == 2); + puts("PASS: an existing LOCK request must still have actual ACK before success"); +} +static void live_policy_scope(void) +{ + struct apple_atcphy p; + for (unsigned int t8140 = 0; t8140 < 2; t8140++) { + for (unsigned int fixed = 0; fixed < 2; fixed++) { + for (unsigned int active = 0; active < 2; active++) { + hw.has_t8140_dp = t8140; + initial(&p, APPLE_ATCPHY_MODE_USB3, ATCPHY_PIPEHANDLER_STATE_USB3); + p.fixed_usb2 = fixed; + p.host_active = active; + stuck_ack = true; /* Request exists, but ACK never asserts. */ + int ret = atcphy_park_pipehandler(&p); + if (t8140 && fixed && active) { + assert(ret == -ETIMEDOUT && p.mode_dirty && mux_writes == 0); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_USB3); + assert(!pipe_regs[PIPEHANDLER_NONSELECTED_OVERRIDE / 4]); + } else { + assert(ret == 0 && !p.mode_dirty && mux_writes == 3); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_DUMMY); + } + /* Entry ACK release is fatal everywhere, but dirties only live T8140 parking. */ + initial(&p, APPLE_ATCPHY_MODE_USB3, ATCPHY_PIPEHANDLER_STATE_USB3); + p.fixed_usb2 = fixed; + p.host_active = active; + pipe_regs[PIPEHANDLER_LOCK_ACK / 4] = PIPEHANDLER_LOCK_EN; + stuck_ack = true; + assert(atcphy_park_pipehandler(&p) == -ETIMEDOUT); + assert(p.mode_dirty == (t8140 && fixed && active)); + assert(p.pipe_state == ATCPHY_PIPEHANDLER_STATE_USB3 && mux_writes == 0); + } + } + } + puts("PASS: eight live/inactive, fixed/ordinary and T8140/legacy LOCK and entry-ACK policy controls"); +} +int main(int argc, char **argv) +{ + assert(argc == 2); + if (!strcmp(argv[1], "backend")) backend_failure(); + else if (!strcmp(argv[1], "host")) host_lock_failure(); + else if (!strcmp(argv[1], "mux")) mux_retry(); + else if (!strcmp(argv[1], "same")) same_mode_pending(); + else if (!strcmp(argv[1], "park")) parking_and_guards(); + else if (!strcmp(argv[1], "requested")) requested_lock(); + else if (!strcmp(argv[1], "scope")) live_policy_scope(); + else assert(false); + return 0; +} +''' + return prefix + production + suffix + +def unwind(root): + + atc = (root / 'drivers/phy/apple/atc.c').read_text() + dp = (root / 'drivers/phy/apple/atc-j700-dp.h').read_text() + + + def function(source, name): + if name == 'atcphy_mux_set' and 'static int atcphy_mux_set_locked(' in source: + marker = re.search(r'^static [^\n]+\batcphy_mux_set_locked\(', source, re.M).group() + return block(source, marker).replace('atcphy_mux_set_locked(', 'atcphy_mux_set(', 1) + 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; + unsigned int dp_usb_context_valid, dp_usb_context_dirty; + u32 dp_usb_context[2][27]; + bool pipe_pending, usb_off_pending, usb_off_drained; + 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 int atcphy_park_pipehandler(struct apple_atcphy *p) +{ p->pipe_state = ATCPHY_PIPEHANDLER_STATE_DUMMY; return 0; } +/* Observation is read-only and separately tested; retain production caller control flow. */ +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 ('core_read32', '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) + 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; +} +''' + return prefix + production + suffix + +def aux(root): + + source = root / 'drivers/phy/apple/atc.c' + text = source.read_text() + 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 + end = begin + 1 + while depth: + depth += (text[end] == '{') - (text[end] == '}') + end += 1 + parts.append(text[start:end]) + production = '\n'.join(parts) + prefix = r''' +#include +#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); + } + 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; +} +''' + return prefix + production + suffix diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py b/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py new file mode 100644 index 0000000000000..6125dbf312b8f --- /dev/null +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py @@ -0,0 +1,652 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright 2026 Ryan Murray +"""Actual PM/HCD/mux/probe boundaries using the shared PHY extraction fixture.""" +from pathlib import Path +import os, re, subprocess +import phy +from common import block + +def ownership(root, usb_unreachable=False): + ns=phy.parking(root) + s=ns['atc'];has_pm='static int atcphy_usb3_pm_get(' in s + prod=ns['production'].replace('struct apple_atcphy {','struct apple_atcphy {\n struct device_node *np;',1) + if has_pm: prod=prod.replace('static int atcphy_mux_set(', 'static int atcphy_mux_set_locked(',1) + # Non-mux fixture entry points continue to exercise actual source. Only the new + # wrapper owns PM; the fixture's original helpers call it through this declaration. + prefix=ns['prefix']+'\nstruct device_node;\nstatic int atcphy_mux_set(struct typec_mux_dev *, struct typec_mux_state *);\n' + prefix=prefix.replace('struct typec_mux_dev { struct apple_atcphy *data; };', + '''struct typec_mux_dev { struct apple_atcphy *data; + int (*set)(struct typec_mux_dev *, struct typec_mux_state *); + int (*prepare)(struct typec_mux_dev *, struct typec_mux_state *); + int (*finish)(struct typec_mux_dev *, struct typec_mux_state *); + void (*commit)(struct typec_mux_dev *, struct typec_mux_state *); + void (*abort)(struct typec_mux_dev *, struct typec_mux_state *);bool removed; }; + struct typec_mux_context { struct { bool held; } lock; }; + struct task_struct { int unused; };static struct task_struct test_task; + #define current (&test_task) + static int typec_mux_srcu; + static int srcu_read_lock(int *s) { (void)s;return 0; } + static void srcu_read_unlock(int *s,int i) { (void)s;(void)i; } + #define mutex_trylock(m) ((m)->held ? 0 : ((m)->held=true)) + #define mutex_unlock(m) ((m)->held=false) + struct typec_mux { unsigned int num_mux_devs; struct typec_mux_dev **mux_devs; struct typec_mux_context *context; struct typec_mux_state transaction; struct task_struct *owner; int srcu_index; unsigned prepared; bool applied; }; + #define IS_ERR_OR_NULL(p) (!(p))''') + prefix=prefix.replace('#define WARN_ON_ONCE(v) assert(!(v))', 'static bool checked_warn(bool v) { assert(!v);return v; }\n#define WARN_ON_ONCE(v) checked_warn(v)') + prefix=prefix.replace('#define guard(kind) (void)',r''' +struct lock_guard { int *lock; }; +static struct lock_guard lock_take(int *lock) { assert(!*lock); *lock=1; return (struct lock_guard){lock}; } +static void lock_release(struct lock_guard *g) { assert(*g->lock==1); *g->lock=0; } +#define guard(kind) struct lock_guard __attribute__((cleanup(lock_release))) _guard = lock_take +''') + # Basic USB/device APIs are modeled, but get/put/wrapper and all parking/ + # restoration bodies are extracted verbatim from the chosen public source. + pmfixture=r''' +#define CONFIG_USB 1 +#define READ_ONCE(x) (x) +#define IS_REACHABLE(x) USB_REACHABLE +struct device_node { int id; }; +struct device_driver { const char *name; }; +struct device { int refs; struct device *parent; struct device_driver *driver; }; +struct platform_device { struct device dev; }; +struct usb_device; +struct usb_hcd; +struct usb_bus { struct device *sysdev, *controller; struct usb_hcd *hcd; struct usb_device *root_hub; }; +struct usb_hcd { int state; unsigned int rh_registered:1; + struct usb_bus self; unsigned long flags; + struct usb_hcd *shared_hcd,*primary_hcd; struct {bool pending;} died_work; }; +enum { HC_STATE_RUNNING=1, HC_STATE_QUIESCING=0x85, HC_STATE_HALT=0 }; +enum { HCD_FLAG_RH_RUNNING,HCD_FLAG_DEAD,HCD_FLAG_POLL_RH,HCD_FLAG_HW_ACCESSIBLE }; +static int __attribute__((unused)) hcd_root_hub_lock; +#define spin_lock_irqsave(lock, flags) do { assert(!*(lock));*(lock)=1;(flags)=0; } while(0) +#define spin_unlock_irqrestore(lock, flags) do { assert(*(lock));*(lock)=0;(void)(flags); } while(0) +#define clear_bit(bit, p) (*(p)&=~(1UL<<(bit))) +#define set_bit(bit, p) (*(p)|=1UL<<(bit)) +#define usb_hcd_is_primary_hcd(hcd) (!(hcd)->primary_hcd||(hcd)->primary_hcd==(hcd)) +#define usb_set_device_state(u, s) ((u)->state=(s)) +#define usb_kick_hub_wq(u) ((void)(u)) +#define schedule_work(w) ((w)->pending=true) +#define bus_to_hcd(bus) ((bus)->hcd) +#define HCD_DEAD(hcd) (!!((hcd)->flags&(1UL<flags&(1UL<refs++;return d; } +static void device_lock(struct device *d) { + if(d==&controller.dev){assert(!controller_locked&&!host_locked&&!root_locked);controller_locked=true;} + else {assert(d==&host_device&&controller_locked&&!host_locked&&!root_locked);host_locked=true;} +} +static void device_unlock(struct device *d) { + if(d==&host_device){assert(host_locked&&!root_locked);host_locked=false;} + else {assert(d==&controller.dev&&controller_locked&&!host_locked&&!root_locked);controller_locked=false;} +} +static void put_device(struct device *d) { assert(d->refs>0);d->refs--; } +static struct usb_device *usb_get_dev(struct usb_device *u) { u->refs++;return u; } +static void usb_put_dev(struct usb_device *u) { assert(u->refs>0);u->refs--; } +static int usb_for_each_dev(void *data,int (*fn)(struct usb_device *,void *)) { if(root_missing)return 0;for(unsigned int i=0;i<4;i++){int r=fn(&roots[i],data);if(r)return r;}return 0; } +static struct usb_device *usb_hub_find_child(struct usb_device *u, int port) { assert(u==&roots[3]&&root_locked&&port==1); + if(detach_at_lookup)usb_set_device_state(u,USB_STATE_NOTATTACHED); + if(quiesce_at_lookup)hcd.state=HC_STATE_QUIESCING; + return link_present?&link_device:NULL; } +static void usb_lock_device(struct usb_device *u) { + if(u==&roots[3]){assert(!root_locked);if(REQUIRES_PLATFORM_LOCKS)assert(controller_locked&&host_locked);root_locked=true;if(detach_on_lock)u->state=0;} + else {assert(u==&link_device&&root_locked&&!link_locked);link_locked=true;} +} +static void usb_unlock_device(struct usb_device *u) { + if(u==&roots[3]){assert(root_locked&&!link_locked);root_locked=false;} + else {assert(u==&link_device&&link_locked&&root_locked);link_locked=false;} +} +static int pm_runtime_resume_and_get(struct device *d) { + assert(root_locked&&!pm_phy->lock); + if(d==&link_device.dev){ + assert(link_locked);link_resumes++; + struct lock_guard g=lock_take(&pm_phy->lock);lock_release(&g); + if(link_error)return link_error; + link_pm_refs++;link_suspended=false;stuck_ack=false; + if(link_detach_on_resume)link_device.state=0; + if(link_dead_on_resume)set_bit(HCD_FLAG_DEAD,&hcd.flags); + return 0; + } + assert(d==&roots[3].dev);resumes++; + /* A re-entrant PHY lock acquisition during resume must remain possible. */ + struct lock_guard g=lock_take(&pm_phy->lock);lock_release(&g); + if(resume_error)return resume_error; + pm_refs++;stuck_ack=link_present&&link_device.state!=USB_STATE_NOTATTACHED&&link_suspended;if(detach_on_resume)roots[3].state=0; + if(inaccessible_on_resume)clear_bit(HCD_FLAG_HW_ACCESSIBLE,&hcd.flags); + if(dead_on_resume)set_bit(HCD_FLAG_DEAD,&hcd.flags); + return 0; +} +static void pm_runtime_put_noidle(struct device *d) { + if(d==&link_device.dev){assert(link_pm_refs==1);link_pm_refs--;} + else {assert(d==&roots[3].dev&&pm_refs==1);pm_refs--;} +} +static void usb_mark_last_busy(struct usb_device *u) { + assert(root_locked&&!pm_phy->lock); + if(u==&link_device)assert(link_locked);else assert(u==&roots[3]&&!link_locked); +} +static void pm_runtime_put_autosuspend(struct device *d) { + assert(root_locked&&!pm_phy->lock); + if(d==&link_device.dev){assert(link_locked&&link_pm_refs==1);link_pm_refs--;} + else {assert(d==&roots[3].dev&&!link_locked&&pm_refs==1);pm_refs--;} +} +''' + suffix=ns['suffix'].replace('int main(', 'int __attribute__((unused)) fixture_main(',1) + controls=r''' +static void prepare(struct apple_atcphy *p,bool fixed,bool t8140,bool swap) +{ + hw.has_t8140_dp=t8140;initial(p,fixed,false,swap); + p->np=&ctrl_node;pm_phy=p; + hcd=(struct usb_hcd){.state=HC_STATE_RUNNING,.rh_registered=true,.flags=1UL<lock&&!root_locked&&!link_locked&&!link_pm_refs&&!link_device.refs&&!pm_refs&&!node_refs&&!controller.dev.refs); + for(unsigned int i=0;i<4;i++)assert(!roots[i].refs); +} +int main(void) +{ + struct apple_atcphy p; +#if !USB_REACHABLE + { prepare(&p,true,true,false);unsigned int before=writes; + struct typec_mux_dev provider={.data=&p,.set=atcphy_mux_set}; + struct typec_mux_dev *providers[]={&provider};struct typec_mux_context typec_test_context={0};struct typec_mux mux={.num_mux_devs=1,.mux_devs=providers,.context=&typec_test_context}; + struct typec_mux_state state={.mode=TYPEC_STATE_SAFE}; + assert(typec_mux_set(&mux,&state)==-ENODEV&&writes==before);balanced(&p); + puts("PASS USB-unreachable fixed route blocks before MMIO");return 0; } +#endif + /* HC death before rh_registered is set need not mark the root NOTATTACHED. */ + prepare(&p,true,true,false);hcd.rh_registered=false;usb_hc_died(&hcd); + assert(roots[3].state!=USB_STATE_NOTATTACHED&&HCD_DEAD(&hcd));unsigned int before_hcd=writes; + int hcd_ret=safe(&p);fprintf(stderr,"dead HCD admission returned %d, PM resumes %d\n",hcd_ret,resumes); + assert(hcd_ret==-ENODEV&&writes==before_hcd&&resumes==0);balanced(&p); + if(HCD_DEAD_ONLY){puts("PASS dead HCD rejected before PM/MMIO");return 0;} + for(unsigned int gate=0;gate<2;gate++){ + prepare(&p,true,true,false);before_hcd=writes; + if(gate==0)inaccessible_on_resume=1; + if(gate==1)dead_on_resume=1; + assert(safe(&p)==-ENODEV&&writes==before_hcd);balanced(&p); + } + for(unsigned int swap=0;swap<2;swap++){ + prepare(&p,true,true,swap);stuck_ack=true; + assert(safe(&p)==0);assert(resumes==1);balanced(&p); + } + /* Resume failure must return before ANY lane/core programming. */ + prepare(&p,true,true,false);unsigned int before=writes;resume_error=-EIO; + unsigned int valid=p.dp_usb_context_valid,dirty=p.dp_usb_context_dirty,owned=p.dp_owned_pmas; + assert(safe(&p)==-EIO&&writes==before&&p.dp_usb_context_valid==valid&&p.dp_usb_context_dirty==dirty&&p.dp_owned_pmas==owned);balanced(&p); + resume_error=0;assert(safe(&p)==0);balanced(&p); + /* PM resume is NOT proof of ACK. Real parking timeout stays fatal. */ + prepare(&p,true,true,false);fail_caller="atcphy_pipehandler_lock";before=writes; + assert(safe(&p)==-ETIMEDOUT&&writes==before&&p.pipe_state==ATCPHY_PIPEHANDLER_STATE_USB3&&p.mode_dirty);balanced(&p); + fail_caller=NULL;assert(safe(&p)==0);balanced(&p); + for(unsigned int missing=0;missing<5;missing++){ + prepare(&p,true,true,false);before=writes; + if(missing==0)graph_missing=true; + if(missing==1)controller_missing=true; + if(missing==2)root_missing=true; + if(missing==3)detach_on_lock=1; + if(missing==4)detach_on_resume=1; + assert(safe(&p)==(missing==0||missing>=3?-ENODEV:-EAGAIN)&&writes==before);balanced(&p); + } + /* Root appears after independent fixed-host probe: missing root is retryable. */ + prepare(&p,true,true,false);root_missing=true;before=writes; + assert(safe(&p)==-EAGAIN&&writes==before);balanced(&p); + root_missing=false;assert(safe(&p)==0);balanced(&p); + /* BIST faults are independent of the runtime clock wake; never force readiness. */ + for(unsigned int swap=0;swap<2;swap++){ + prepare(&p,true,true,swap);assert(safe(&p)==0);balanced(&p); + orient(&p,!swap);bist_status_stuck=true; + assert(select_mode(&p,APPLE_ATCPHY_MODE_USB3_DP)==-ETIMEDOUT&&p.mode_dirty&&p.pipe_pending);balanced(&p); + bist_status_stuck=false;recover();assert(select_mode(&p,APPLE_ATCPHY_MODE_USB3_DP)==0);balanced(&p); + } + /* Scope and clean same-mode preserve MMIO guards; fixed no-op briefly owns PM. */ + struct typec_altmode alt={.svid=USB_TYPEC_DP_SID}; + struct typec_mux_dev mux={&p};struct typec_mux_state same={.mode=TYPEC_DP_STATE_D,.alt=&alt}; + prepare(&p,false,true,false);before=writes;assert(atcphy_mux_set(&mux,&same)==0&&resumes==0&&writes==before);balanced(&p); + prepare(&p,true,false,false);before=writes;assert(atcphy_mux_set(&mux,&same)==0&&resumes==0&&writes==before);balanced(&p); + prepare(&p,true,true,false);before=writes; + assert(atcphy_mux_set(&mux,&same)==0&&writes==before&&resumes==1);balanced(&p); + /* Binding/identity failure must not reach USB PM or PHY programming. */ + for(unsigned int missing=0;missing<3;missing++){ + prepare(&p,true,true,false);before=writes; + if(missing==0)controller.dev.driver=NULL; + if(missing==1)host_device.driver=NULL; + if(missing==2)host_device.parent=&wrong_controller; + assert(safe(&p)==(missing==2?-ENODEV:-EAGAIN)&&writes==before&&resumes==0);balanced(&p); + } + /* Root state can change during lookup despite USB device locks. */ + for(unsigned int event=0;event<2;event++){ + prepare(&p,true,true,false);before=writes; + if(event==0)detach_at_lookup=1;else quiesce_at_lookup=1; + int ret=safe(&p);fprintf(stderr,"post-child admission event %u returned %d writes %u\n",event,ret,writes-before); + assert(ret==-ENODEV&&writes==before);balanced(&p); + } + /* Selectively suspended upstream link is not resumed by root ownership alone. */ + for(unsigned int swap=0;swap<2;swap++){ + prepare(&p,true,true,swap);link_present=link_suspended=stuck_ack=true; + int link_ret=safe(&p);fprintf(stderr,"selective link SAFE returned %d, root resumes %d, link resumes %d\n",link_ret,resumes,link_resumes); + assert(link_ret==0&&resumes==1&&link_resumes==1&&!link_suspended);balanced(&p); + } + for(unsigned int fault=0;fault<3;fault++){ + prepare(&p,true,true,false);link_present=link_suspended=true;before=writes; + unsigned int retained=p.dp_usb_context_valid,owners=p.dp_owned_pmas; + if(fault==0)link_error=-EIO; + if(fault==1)link_detach_on_resume=1; + if(fault==2)link_dead_on_resume=1; + assert(safe(&p)==(fault?-ENODEV:-EIO)&&writes==before&&p.dp_usb_context_valid==retained&&p.dp_owned_pmas==owners);balanced(&p); + } + prepare(&p,true,true,false);link_present=link_suspended=true;link_error=-EIO; + assert(safe(&p)==-EIO);balanced(&p);link_error=0;assert(safe(&p)==0);balanced(&p); + prepare(&p,true,true,false);link_present=link_suspended=true;fail_caller="atcphy_pipehandler_lock";before=writes; + assert(safe(&p)==-ETIMEDOUT&&writes==before&&p.mode_dirty&&p.pipe_state==ATCPHY_PIPEHANDLER_STATE_USB3);balanced(&p); + fail_caller=NULL;assert(safe(&p)==0);balanced(&p); + for(unsigned int count=0;count<2;count++){ + prepare(&p,true,true,false);roots[3].maxchild=count?2:0;before=writes; + assert(safe(&p)==(count?-ENODEV:-EAGAIN)&&writes==before);balanced(&p); + } + prepare(&p,true,true,false);link_present=true;link_device.state=USB_STATE_NOTATTACHED; + assert(safe(&p)==0&&link_resumes==0);balanced(&p); + /* Startup with no old child is allowed; no fake dock or link is required. */ + prepare(&p,true,true,false);assert(safe(&p)==0&&link_resumes==0);balanced(&p); + puts("PASS actual upstream-link ownership: both PMAs, selective suspend, resume/removal/death/ACK failures, retry, unknown port map and no-child startup"); + puts("PASS actual PM wrapper + actual parking/restore: both PMAs, gated clock, PM error/retry, fatal ACK/retry, graph/controller/root/removal gates, legacy/ordinary and cached mode"); + return 0; +} +''' + helpers='' + if has_pm: + helpers=ns['block'](s,'struct atcphy_usb3_pm {')+';\n' + helpers+='#if IS_REACHABLE(CONFIG_USB)\n' + if 'static int atcphy_usb3_link_get(' in s: + helpers+=ns['function'](s,'atcphy_usb3_link_get')+'\n'+ns['function'](s,'atcphy_usb3_link_put')+'\n' + helpers+=ns['function'](s,'atcphy_usb3_pm_find')+'\n#endif\n' + helpers+=ns['function'](s,'atcphy_usb3_pm_get')+'\n'+ns['function'](s,'atcphy_usb3_pm_put')+'\n' + # The fixture extractor adapts the locked body; extract the wrapper directly. + marker=re.search(r'^static int atcphy_mux_set\(',s,re.M).group() + helpers+=ns['block'](s,marker)+'\n' + else: + # Parent runs its ACTUAL mux callback from production, without a substituted wrapper. + pmfixture=pmfixture.replace('static ', 'static __attribute__((unused)) ') + if usb_unreachable: + pmfixture=pmfixture.replace('static ', 'static __attribute__((unused)) ') + typec_source=(root/'drivers/usb/typec/mux.c').read_text() + typec_caller='\n'.join(ns['block'](typec_source, marker) for marker in + ('static void typec_mux_unlock(', 'void typec_mux_abort(', 'int typec_mux_begin(', 'int typec_mux_apply(', 'int typec_mux_end(', 'int typec_mux_set('))+'\n' + hcd_source=(root/'drivers/usb/core/hcd.c').read_text() + hcd_death=ns['block'](hcd_source,'void usb_hc_died (')+'\n' + text=prefix+prod+'\n#define REQUIRES_PLATFORM_LOCKS '+str(int('device_lock(&pm->controller->dev)' in s))+'\n#define USB_REACHABLE '+str(int(not usb_unreachable))+'\n#define HCD_DEAD_ONLY '+str(int(False))+'\n'+pmfixture+helpers+typec_caller+hcd_death+suffix+controls + return text + +def probe(root): + def source(path): + return (root/path).read_text() + + + def function(text, name): + match = re.search(r'^static (?:int|void)\s*\n?' + name + r'\(', text, re.M) + assert match, name + start = match.start() + opening = text.index('{', start) + depth = 1 + end = opening + 1 + while depth: + depth += (text[end] == '{') - (text[end] == '}') + end += 1 + return text[start:end] + + + prefix = r''' +#include +#include +#include +#include +#include +#include +#include +#define EPROBE_DEFER 517 +#define IS_ERR(p) ((intptr_t)(p)<0) +#define PTR_ERR(p) ((int)(intptr_t)(p)) +#define IS_ERR_OR_NULL(p) (!(p)||IS_ERR(p)) +#define container_of(p,t,m) ((t *)((char *)(p)-offsetof(t,m))) +#define guard(kind) (void) +#define TYPEC_STATE_SAFE 0 +#define INIT_DELAYED_WORK(p,fn) ((p)->pending=false) +#define atomic_set(p,n) (*(p)=(n)) +struct work { bool pending; }; +struct fwnode_handle { int refs; }; +struct typec_mux_state { void *alt,*data; unsigned long mode; }; +struct tps6598x { int lock; void *port; }; +struct notifier_block { int (*notifier_call)(struct notifier_block *,unsigned long,void *); }; +''' + pm = source('drivers/usb/typec/tipd/cd321x-pm.h').replace('#include ', '') + mocks = r''' +struct cd321x { + struct tps6598x tps; + struct work update_work,resume_work; + int link_event; + struct cd321x_pm_state pm; + void *mux,*tbt_switch,*port_altmode_dp,*port_altmode_tbt; + struct fwnode_handle *connector_fwnode; + struct typec_mux_state state; + struct notifier_block tbt_notifier; + bool state_valid,tbt_notifier_registered; +}; +static struct fwnode_handle connector; +static int mux_error, notifier_error, switch_error; +static int ports,altmodes,muxes,switches,notifiers,stop_calls; +static bool displayport=true; +static int tps6598x_register_port(struct tps6598x *t,struct fwnode_handle *f) { + (void)f;t->port=t;ports++;return 0; +} +static int cd321x_register_port_altmodes(struct cd321x *c) { + c->port_altmode_dp=c;c->port_altmode_tbt=c;altmodes+=2;return 0; +} +static void *fwnode_typec_mux_get(struct fwnode_handle *f) { (void)f;muxes++;return &muxes; } +static bool fwnode_property_present(struct fwnode_handle *f,const char *s) { (void)f;(void)s;return displayport; } +static struct fwnode_handle *fwnode_find_reference(struct fwnode_handle *f,const char *s,int n) { + (void)f;(void)s;(void)n;connector.refs++;return &connector; +} +static void __attribute__((unused)) fwnode_handle_put(struct fwnode_handle *f) { + if(f){assert(f->refs>0);f->refs--;} +} +static void *fwnode_typec_thunderbolt_switch_get(struct fwnode_handle *f) { + (void)f;if(switch_error)return (void *)(intptr_t)switch_error;switches++;return &switches; +} +static int typec_set_mode(void *p,unsigned long m) { (void)p;assert(m==TYPEC_STATE_SAFE);return mux_error; } +static int cd321x_tbt_notify(struct notifier_block *n,unsigned long e,void *d) { (void)n;(void)e;(void)d;return 0; } +static int typec_thunderbolt_switch_register_notifier(void *s,struct notifier_block *n) { + (void)s;(void)n;if(notifier_error)return notifier_error;notifiers++;return 0; +} +static void typec_thunderbolt_switch_put(void *s) { assert(s&&switches>0);switches--; } +static void typec_mux_put(void *m) { assert(m&&muxes>0);muxes--; } +static void typec_unregister_altmode(void *a) { if(a){assert(altmodes>0);altmodes--;} } +static void typec_unregister_port(void *p) { assert(p&&ports>0);ports--; } +static void cd321x_stop_work(struct cd321x *c) { + stop_calls++;if(c->tbt_notifier_registered){notifiers--;c->tbt_notifier_registered=false;} + cd321x_pm_remove(&c->pm);c->resume_work.pending=c->update_work.pending=false; +} +''' + core = source('drivers/usb/typec/tipd/core.c') + production = function(core, 'cd321x_register_port') + '\n' + function(core, 'cd321x_unregister_port') + controls = r''' +static void reset(struct cd321x *c) { + memset(c,0,sizeof(*c));connector.refs=0; + mux_error=notifier_error=switch_error=0;stop_calls=0; + assert(!ports&&!altmodes&&!muxes&&!switches&&!notifiers); +} +static void balanced(void) { + assert(!ports&&!altmodes&&!muxes&&!switches&&!notifiers&&!connector.refs); +} +int main(void) { + struct cd321x c; + struct cd321x_pm_state budget; + cd321x_pm_init(&budget);assert(cd321x_pm_ready(&budget)); + for(unsigned int i=0;iport=t;ports++;return 0;', '(void)f;t->port=&probe_port;ports++;return 0;') + # Real class dispatch requires the port's mux; its allocator remains a mock. + declarations = 'struct typec_port { struct typec_mux *mux; };\nstatic struct typec_port probe_port;\n' + dispatch = block((root / 'drivers/usb/typec/class.c').read_text(), 'int typec_set_mode(') + marker = port.index('static int tps6598x_register_port(') + port = port[:marker] + declarations + dispatch + '\n' + port[marker:] + import re + # Only helpers used at this boundary belong in this translation unit; + # the separate probe group exercises the complete retry state machine. + used = {'cd321x_pm_new_connection', 'cd321x_pm_init', 'cd321x_pm_remove', 'cd321x_pm_ready', + 'cd321x_pm_begin_read', 'cd321x_pm_snapshot_ready', + 'cd321x_pm_complete', 'cd321x_pm_can_update'} + for match in list(re.finditer(r'^static inline (?:void|bool) (cd321x_\w+)\(', port, re.M))[::-1]: + if match.group(1) not in used: + body = block(port, match.group()) + port = port[:match.start()] + port[match.start() + len(body):] + port = port.replace('static int mux_error, notifier_error, switch_error;', + 'static int notifier_error, switch_error;') + existing = set(re.findall(r'^\s*#define (\w+)', unit, re.M)) + port = '\n'.join(line for line in port.splitlines() + if not (line.startswith('#define ') and line.split()[1].split('(')[0] in existing)) + controls = r""" +static void port_balance(void) +{ + assert(!ports&&!altmodes&&!muxes&&!switches&&!notifiers&&!connector.refs); +} +int main(void) +{ + struct apple_atcphy p; + struct cd321x c={}; + struct typec_mux_dev provider={.data=&p,.set=atcphy_mux_set}; + struct typec_mux_dev *providers[]={&provider}; + struct typec_mux_context typec_test_context={0};struct typec_mux route={.num_mux_devs=1,.mux_devs=providers,.context=&typec_test_context};probe_port.mux=&route; + prepare(&p,true,true,false);root_missing=true;unsigned int before=writes; + assert(cd321x_register_port(&c.tps,&connector)==-EPROBE_DEFER); + assert(!c.state_valid&&!resumes&&writes==before);balanced(&p);port_balance(); + memset(&c,0,sizeof(c));root_missing=false; + assert(cd321x_register_port(&c.tps,&connector)==0 && c.state_valid && resumes==1); + assert(cd321x_pm_ready(&c.pm) && cd321x_pm_begin_read(&c.pm)); + cd321x_pm_snapshot_ready(&c.pm);cd321x_pm_complete(&c.pm); + assert(cd321x_pm_can_update(&c.pm)); + balanced(&p);cd321x_unregister_port(&c.tps);port_balance(); + memset(&c,0,sizeof(c));prepare(&p,true,true,false);notifier_error=-ENOMEM; + assert(cd321x_register_port(&c.tps,&connector)==-ENOMEM); + balanced(&p);port_balance();notifier_error=0; + memset(&c,0,sizeof(c));prepare(&p,true,true,false);resume_error=-EIO;before=writes; + assert(cd321x_register_port(&c.tps,&connector)==0 && !c.state_valid && writes==before); + balanced(&p);cd321x_unregister_port(&c.tps);port_balance(); + puts("PASS: actual CD321x probe -> typec_set_mode -> typec_mux_set -> bounded PM wrapper; delayed root reprobe and rollback references"); +} +""" + return unit + '\n' + port + '\n' + controls + + +def upstream_resume_boundary(root): + """Actual usbcore dispatch: root bus resume does not resume its child.""" + source = (root / 'drivers/usb/core/generic.c').read_text() + function = block(source, 'int usb_generic_driver_resume(') + prefix = r""" +#include +#include +#include +#include +typedef int pm_message_t; +struct usb_device { struct usb_device *parent; }; +static int bus_resumes, port_resumes, notifications, port_error; +static bool child_suspended = true; +static int hcd_bus_resume(struct usb_device *u, pm_message_t m) +{ (void)m; assert(!u->parent); bus_resumes++; return 0; } +static int usb_port_resume(struct usb_device *u, pm_message_t m) +{ (void)m; assert(u->parent); port_resumes++; if(port_error)return port_error; child_suspended=false; return 0; } +static void usbfs_notify_resume(struct usb_device *u) +{ (void)u; notifications++; } +""" + controls = r""" +int main(void) { + struct usb_device root={0}, child={.parent=&root}; + assert(usb_generic_driver_resume(&root,1)==0); + assert(bus_resumes==1&&port_resumes==0&&child_suspended&¬ifications==1); + assert(usb_generic_driver_resume(&child,1)==0); + assert(bus_resumes==1&&port_resumes==1&&!child_suspended&¬ifications==2); + child_suspended=true;port_error=-EIO; + assert(usb_generic_driver_resume(&child,1)==-EIO&&child_suspended&¬ifications==2); + puts("PASS actual usbcore root/child resume dispatch and failed child resume"); + return 0; +} +""" + return prefix + function + controls + + +def controller_lifecycle(root): + from teardown_pm import unit_for + return unit_for(root) + + +def asynchronous_root_state(root): + """Actual USB state API does not use the USB device mutex as its fence.""" + source = (root / 'drivers/usb/core/hub.c').read_text() + prefix = r""" +#include +#include +#include +enum usb_device_state { USB_STATE_NOTATTACHED, USB_STATE_CONFIGURED, USB_STATE_SUSPENDED }; +struct usb_device; +struct usb_port { struct usb_device *child; }; +struct usb_hub { struct usb_port **ports; }; +struct device { int unused; }; +struct config { struct { unsigned bmAttributes; } desc; }; +struct usb_device { struct device dev; struct usb_device *parent; struct usb_hub *hub; enum usb_device_state state; int maxchild; unsigned quirks; struct config *actconfig; unsigned long active_duration; bool device_locked; }; +static unsigned long jiffies=10; +static int device_state_lock; +#define USB_QUIRK_IGNORE_REMOTE_WAKEUP 1 +#define USB_CONFIG_ATT_WAKEUP 2 +#define spin_lock_irqsave(l,f) do { assert(!*(l));*(l)=1;(f)=0; } while(0) +#define spin_unlock_irqrestore(l,f) do { assert(*(l));*(l)=0;(void)(f); } while(0) +static struct usb_hub *usb_hub_to_struct_hub(struct usb_device *u) { return u->hub; } +static void update_port_device_state(struct usb_device *u) { (void)u; } +static void trace_usb_set_device_state(struct usb_device *u) { (void)u; } +static void device_set_wakeup_capable(struct device *d,int w) { (void)d;(void)w; } +""" + controls = r""" +int main(void) { + struct usb_device child={.state=USB_STATE_CONFIGURED,.device_locked=true}; + struct usb_port port={.child=&child};struct usb_port *ports[]={&port}; + struct usb_hub hub={.ports=ports}; + struct usb_device root={.state=USB_STATE_CONFIGURED,.maxchild=1,.hub=&hub,.device_locked=true}; + child.parent=&root; + usb_set_device_state(&root,USB_STATE_NOTATTACHED); + assert(root.state==USB_STATE_NOTATTACHED&&child.state==USB_STATE_NOTATTACHED); + assert(root.device_locked&&child.device_locked&&!device_state_lock); + usb_set_device_state(&child,USB_STATE_CONFIGURED); + assert(child.state==USB_STATE_NOTATTACHED); + puts("PASS actual asynchronous USB detach while device locks are held; no resurrection"); + return 0; +} +""" + return prefix + block(source, 'static void update_usb_device_state(') + block(source, 'static void recursively_mark_NOTATTACHED(') + block(source, 'void usb_set_device_state(') + controls diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/quiesce.py b/tools/testing/selftests/drivers/gpu/apple-dcp/quiesce.py new file mode 100644 index 0000000000000..6e7b9f69b2a25 --- /dev/null +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/quiesce.py @@ -0,0 +1,147 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright 2026 Ryan Murray +"""Actual mux prepare/set and TPS removal ordering; firmware/PHY leaves are mocks.""" +from common import block + +def make_unit(source, parent=False, reference=None): + reference=source if reference is None else reference + core=(source/'drivers/usb/typec/tipd/core.c').read_text();mux=(source/'drivers/usb/typec/mux.c').read_text() + dcp=(reference/'drivers/gpu/drm/apple/dcp.c').read_text() + prefix=r''' +#include +#include +#include +#include +#include +#include +typedef unsigned u32; +#define IS_ERR(p) false +#define IS_ERR_OR_NULL(p) (!(p)) +#define EXPORT_SYMBOL_GPL(x) +#define guard(k) (void) +#define scoped_guard(k,l) for(int once=((void)(l),1);once;once=0) +#define READ_ONCE(x) (x) +#define WARN_ON_ONCE(x) (x) +#define container_of(p,t,m) ((t *)((char *)(p)-offsetof(t,m))) +#define to_platform_device(p) (p) +#define typec_mux_get_drvdata(m) ((m)->data) +#define dev_warn(...) ((void)0) +#define dev_info(...) ((void)0) +#define dev_name(p) "device" +#define list_for_each_entry(p,h,m) for((void)(h),(p)=sole_port();(p);(p)=NULL) +#define lockdep_assert_held(p) ((void)(p)) +#define dev_err(dev,fmt,ret) do { (void)(dev);last_error=(ret); } while(0) +enum {TYPEC_STATE_SAFE,TYPEC_STATE_USB,TYPEC_ORIENTATION_NONE,TYPEC_PWR_MODE_USB,TYPEC_THUNDERBOLT_SWITCH_OFF}; +enum usb_role {USB_ROLE_NONE,USB_ROLE_HOST,USB_ROLE_DEVICE}; +enum typec_data_role {TYPEC_HOST,TYPEC_DEVICE}; +#define TPS_STATUS_TO_TYPEC_PORTROLE(s) (s) +#define TPS_STATUS_TO_TYPEC_VCONN(s) (s) +#define TPS_STATUS_TO_TYPEC_DATAROLE(s) (s) +struct typec_mux_state { void *alt; unsigned long mode; void *data; }; +struct typec_mux_dev { bool removed; void (*commit)(struct typec_mux_dev *,struct typec_mux_state *); void (*abort)(struct typec_mux_dev *,struct typec_mux_state *); int (*finish)(struct typec_mux_dev *,struct typec_mux_state *); void *data; int (*set)(struct typec_mux_dev *,struct typec_mux_state *); int (*prepare)(struct typec_mux_dev *,struct typec_mux_state *); }; +struct task_struct { int unused; }; +static _Thread_local struct task_struct this_task; +#define current (&this_task) +struct mutex { pthread_mutex_t value; }; +struct typec_mux_context { struct mutex lock; }; +static struct typec_mux_context shared_context={.lock={.value=PTHREAD_MUTEX_INITIALIZER}}; +static int typec_mux_srcu; +static int srcu_read_lock(int *s) { (void)s;return 0; } +static void srcu_read_unlock(int *s,int i) { (void)s;(void)i; } +static int mutex_trylock(struct mutex *m) { return pthread_mutex_trylock(&m->value)==0; } +static void mutex_unlock(struct mutex *m) { assert(!pthread_mutex_unlock(&m->value)); } +struct typec_mux { struct typec_mux_context *context; struct typec_mux_state transaction; struct task_struct *owner; int srcu_index; unsigned prepared; bool applied; unsigned num_mux_devs; struct typec_mux_dev *mux_devs[3]; }; +struct connector { bool connected; }; +struct apple_dcp { int external_lock,external_error; bool external_cable,external_stopped; struct { bool connected; } dptxport[1]; void *hdmi_hpd; bool active; bool external,typec_cable_connected; void *dev; struct connector *typec_connector; }; +struct apple_dcp_typec_route; +struct apple_dcp_typec_port { int link; void *connector_np; struct apple_dcp_typec_route *owner,*target,*secondary_target,*preferred_route; void *applied_alt; unsigned long applied_mode; unsigned applied_status,applied_conf; bool applied_valid,safe_pending,safe_finish_ready,dp_wanted,dp_hpd,hpd; }; +struct apple_dcp_typec_route { struct apple_dcp_typec_port *port; struct apple_dcp *dcp; void *xbar; bool deactivate_xbar_done; int deactivate_error; bool selected,tunnel; }; +static int dcp_typec_fabric_lock,dcp_typec_ports,deactivate_calls; +struct drm_device { int unused; }; +static struct apple_dcp display; +static struct connector connector; +static struct apple_dcp_typec_port port; +static struct apple_dcp_typec_route route; +static struct apple_dcp_typec_port *sole_port(void) { return &port; } +static char events[100];static unsigned count; +static int power_error,phy_error,tbt_error,role_error,last_error,phy_calls,reset_calls,gpio_calls,role_calls,stops,unregisters; +static void event(char c) { assert(count+1external; } +static int dcp_dptx_disconnect_oob(void *dev,u32 index) { (void)dev;assert(index==0);event('Q');stops++;if(power_error){display.external_error=power_error;return power_error;}display.external_cable=false;display.external_stopped=true;display.dptxport[0].connected=false;display.typec_cable_connected=false;connector.connected=false;return 0; } +static struct drm_device *dcp_typec_drm(void) { return NULL; } +static void dcp_typec_plan(struct drm_device *d,struct apple_dcp_typec_port *p,unsigned n) { (void)d;(void)p;(void)n; } +static bool dcp_typec_tunnel_held(struct apple_dcp *d) { (void)d;return false; } +static int dcp_typec_route_deactivate(struct apple_dcp_typec_route *r) { deactivate_calls++;r->selected=false;return 0; } +static void dcp_typec_port_attach(struct apple_dcp_typec_port *p) { (void)p; } +static int gpiod_get_value_cansleep(void *p) { (void)p;return 0; } +static int dcp_dptx_connect(struct apple_dcp *d,unsigned n) { (void)d;(void)n;return 0; } +static bool dcp_typec_route_available(struct apple_dcp_typec_route *r) { return !r->selected; } +static int dcp_typec_route_activate(struct apple_dcp_typec_route *r,void *x) { (void)x;r->selected=true;return 0; } +struct tps6598x; +struct tipd_data { REMOVE_RETURN (*remove)(struct tps6598x *); void (*unregister_port)(struct tps6598x *); int (*reset)(struct tps6598x *); }; +struct typec_port { struct typec_mux *mux; }; +struct tps6598x { bool remove_safe_prepared; struct typec_mux *mux; void *partner,*dev,*psy,*role_sw,*reset; int irq,wq_poll; struct typec_port *port; struct tipd_data *data; }; +struct cd321x { struct tps6598x tps; struct typec_mux *mux; void *tbt_switch; }; +struct typec_thunderbolt_switch_data { int state; }; +static void cd321x_stop_work(struct cd321x *c) { (void)c;event('W'); } +static int typec_thunderbolt_switch_set(void *p,void *s) { (void)p;(void)s;event('B');return tbt_error; } +static int usb_role_switch_set_role(void *p,int r) { (void)p;assert(r==USB_ROLE_NONE);event('R');role_calls++;return role_error; } +static void typec_unregister_partner(void *p) { (void)p;event('P'); } +#define typec_set_pwr_opmode(...) ((void)0) +#define typec_set_pwr_role(...) ((void)0) +#define typec_set_vconn_role(...) ((void)0) +static int typec_set_orientation(void *p,int o) { (void)p;assert(o==TYPEC_ORIENTATION_NONE);return 0; } +static void typec_set_data_role(struct typec_port *p,enum typec_data_role r) { (void)p;(void)r;event('D'); } +static void dcp_typec_pipeline_freed(void) { event('F'); } +static void power_supply_changed(void *p) { (void)p; } +static void tipd_debugfs_unregister(struct tps6598x *t) { (void)t; } +static void cancel_delayed_work_sync(void *w) { (void)w; } +static void devm_free_irq(void *d,int i,void *t) { (void)d;(void)i;(void)t; } +static void typec_mux_put(struct typec_mux *m) { (void)m; } +static void usb_role_switch_put(void *p) { (void)p;event('V'); } +static void gpiod_set_value_cansleep(void *p,int v) { (void)p;assert(v==1);gpio_calls++;event('G'); } + +'''.replace('REMOVE_RETURN','void' if parent else 'int') + functions='\n'.join(block(mux, marker) for marker in ('static void typec_mux_unlock(', 'void typec_mux_abort(', 'int typec_mux_begin(', 'int typec_mux_apply(', 'int typec_mux_end(', 'int typec_mux_prepare(')) if not parent else block((reference/'drivers/usb/typec/mux.c').read_text(),'int typec_mux_prepare(')+'\n' + functions+=block(mux,'int typec_mux_set(')+'\n'+block(dcp,'static int dcp_typec_route_prepare(')+'\n'+block(dcp,'static int dcp_typec_route_finish(')+'\n'+block(dcp,'static void dcp_typec_route_commit(')+'\n' + functions+=block(dcp,'static struct apple_dcp_typec_route *\ndcp_typec_rebalance_locked(')+'\n' + if not parent:functions+=block(core,'static void tps6598x_drop_partner(')+'\n'+block(core,'static int tps6598x_set_data_role(')+'\n' + functions+=block(core,'static '+('void' if parent else 'int')+' tps6598x_disconnect(')+'\n'+block(core,'static '+('void' if parent else 'int')+' cd321x_remove(')+'\n'+block(core,'void tipd_remove(')+'\n' + controls=r''' +static struct typec_mux_dev phy_provider,display_provider; +static struct typec_mux mux; +static struct typec_port class_port; +static struct tipd_data data; + +static int phy_set(struct typec_mux_dev *m,struct typec_mux_state *s) { (void)m;assert(s->mode==TYPEC_STATE_SAFE);event('L');phy_calls++;return phy_error; } +static int display_set(struct typec_mux_dev *m,struct typec_mux_state *s) { (void)m;(void)s;event('S');if(port.owner&&display.external&&connector.connected) {int r=dcp_dptx_disconnect_oob(display.dev,0);if(r)return r;}if(port.safe_pending)return 0;port.owner=NULL;return 0; } +static void unregister_port(struct tps6598x *t) { (void)t;unregisters++;event('U'); } +static int reset(struct tps6598x *t) { (void)t;reset_calls++;event('X');return 0; } +static void initial(struct cd321x *c) { + count=0;events[0]=0;power_error=phy_error=tbt_error=role_error=last_error=phy_calls=reset_calls=gpio_calls=role_calls=stops=unregisters=deactivate_calls=0; + connector=(struct connector){.connected=true};display=(struct apple_dcp){.external=true,.external_cable=true,.dptxport={{.connected=true}},.typec_cable_connected=true,.typec_connector=&connector}; + port=(struct apple_dcp_typec_port){.owner=&route,.applied_valid=true,.dp_wanted=true,.dp_hpd=true,.hpd=true};route=(struct apple_dcp_typec_route){.port=&port,.dcp=&display,.selected=true}; + phy_provider=(struct typec_mux_dev){.set=phy_set};display_provider=(struct typec_mux_dev){.data=&route,.set=display_set,.prepare=dcp_typec_route_prepare,.finish=dcp_typec_route_finish,.commit=dcp_typec_route_commit}; + mux=(struct typec_mux){.context=&shared_context,.num_mux_devs=2,.mux_devs={&phy_provider,&display_provider}};class_port.mux=&mux; + data=(struct tipd_data){.remove=cd321x_remove,.unregister_port=unregister_port,.reset=reset}; + *c=(struct cd321x){.tps={.mux=&mux,.partner=(void *)1,.port=&class_port,.data=&data,.reset=(void *)1},.mux=&mux}; +} +static int position(char c) { for(unsigned i=0;i=0&&position('Q')', '') + timing_fixture = [{ + "HorizontalAttributes": {"Total": 2200, "FrontPorch": 88, "SyncWidth": 44, + "Active": 1920, "PreciseSyncRate": 4423680, "SyncPolarity": 1, "PixelRepetition": 0}, + "VerticalAttributes": {"Total": 1125, "FrontPorch": 4, "SyncWidth": 5, + "Active": 1080, "PreciseSyncRate": 3932160, "SyncPolarity": 1, "PixelRepetition": 0}, + "ColorModes": [{"ID": 85, "Score": 1, "IsVirtual": False, "PixelEncoding": 0, + "Colorimetry": 10, "Depth": 10, "DynamicRange": 0, "EOTF": 0}], + "ID": 62, "Score": 16224, "IsPreferred": True, "IsInterlaced": False, "IsVirtual": False, + }] + timing_raw = bytearray(struct.pack(' +#include +#include +#include +#include +#include +#include +#include +typedef uint8_t u8; +typedef uint16_t u16; +typedef uint32_t u32; +typedef uint64_t u64; +typedef int64_t s64; +typedef u16 __le16; +typedef u16 __be16; +typedef u32 __le32; +#define __packed __attribute__((packed)) +#undef static_assert +#define static_assert(c) _Static_assert(c, #c) +#define IS_ENABLED(x) 0 +#define U32_MAX UINT32_MAX +#define U16_MAX UINT16_MAX +#define S64_MAX INT64_MAX +#define S64_MIN INT64_MIN +#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) +#define round_up(n, a) (((n) + (a) - 1) & ~((a) - 1)) +#define ERR_PTR(n) ((void *)(intptr_t)(n)) +#define PTR_ERR(p) ((int)(intptr_t)(p)) +#define IS_ERR(p) ((uintptr_t)(p) > (uintptr_t)-4096) +#define IS_ERR_OR_NULL(p) (!(p) || IS_ERR(p)) +#define GFP_KERNEL 0 +#define kmalloc(n, f) malloc(n) +#define kcalloc(n, s, f) calloc(n,s) +#define kfree(p) free(p) +#define mul_u32_u32(a, b) ((u64)(u32)(a) * (u32)(b)) +#define DIV_ROUND_CLOSEST_ULL(n, d) (((n) + (d) / 2) / (d)) +#define DIV_ROUND_CLOSEST(n, d) (((n) + (d) / 2) / (d)) +#define trace_iomfb_color_mode(...) ((void)0) +#define trace_iomfb_parse_mode_fail(...) ((void)0) +#define trace_iomfb_parse_mode_success(...) ((void)0) +#define trace_iomfb_timing_mode(...) ((void)0) +#define DRM_MODE_TYPE_DRIVER 1 +#define DRM_MODE_TYPE_PREFERRED 2 +#define DRM_MODE_FLAG_PHSYNC 1 +#define DRM_MODE_FLAG_NHSYNC 2 +#define DRM_MODE_FLAG_PVSYNC 4 +#define DRM_MODE_FLAG_NVSYNC 8 +#define cpu_to_le16(v) (v) +#define cpu_to_le32(v) (v) +#define cpu_to_be16(v) __builtin_bswap16(v) +struct drm_display_mode { + unsigned int type, flags; + int clock, vdisplay, vsync_start, vsync_end, vtotal; + int hdisplay, hsync_start, hsync_end, htotal, width_mm, height_mm; +}; +static void drm_mode_set_name(struct drm_display_mode *mode) { (void)mode; } +''' + stubs = r''' +struct drm_edid { struct edid raw; }; +static const struct drm_edid *drm_edid_alloc(const void *data, size_t size) +{ + assert(size == 128); + struct drm_edid *result = malloc(sizeof(*result)); + memcpy(&result->raw, data, size); + return result; +} +static void drm_edid_free(const struct drm_edid *edid) { free((void *)edid); } +static bool drm_edid_valid(const struct drm_edid *edid) +{ + const u8 *raw = (const u8 *)&edid->raw; + unsigned int sum = 0; + for (unsigned int i = 0; i < 128; i++) sum += raw[i]; + return !(sum & 255) && !raw[126] && !memcmp(raw, "\0\xff\xff\xff\xff\xff\xff\0", 8); +} +''' + suffix = r''' +static void epochs(void) +{ + struct dcp_metadata_epoch e = {0}; + struct dcp_metadata_ticket before = dcp_metadata_begin(&e); + assert(!dcp_metadata_matches(&e, before)); + dcp_metadata_epoch_connection(&e, 1, true); + struct dcp_metadata_ticket current = dcp_metadata_begin(&e); + assert(dcp_metadata_matches(&e, current)); + assert(!dcp_metadata_matches(&e, before)); /* Never retag pre-association metadata. */ + dcp_metadata_epoch_connection(&e, 1, true); + assert(dcp_metadata_matches(&e, current)); + dcp_metadata_epoch_connection(&e, 2, false); + assert(!dcp_metadata_matches(&e, current)); + dcp_metadata_epoch_connection(&e, 3, true); + assert(!dcp_metadata_matches(&e, dcp_metadata_begin(&e))); /* Later attachment ineligible. */ + e = (struct dcp_metadata_epoch){0}; + dcp_metadata_epoch_connection(&e, 9, true); + current = dcp_metadata_begin(&e); + dcp_metadata_epoch_connection(&e, 10, true); /* A route change also invalidates. */ + assert(!dcp_metadata_matches(&e, current)); +} +int main(int argc, char **argv) +{ + if (argc == 3 && !strcmp(argv[1], "--property")) { + FILE *f=fopen(argv[2],"rb"); assert(f && !fseek(f,0,SEEK_END)); + long n=ftell(f); assert(n>20 && !fseek(f,0,SEEK_SET)); + unsigned char *b=malloc(n); assert(fread(b,n,1,f)==1); fclose(f); + assert(dcp_property_complete(b,n-1,n)); + assert(dcp_property_complete(b,n-1,n-1)); + for(unsigned int cut=1;cut<=16;cut++) + assert(!dcp_property_complete(b,n-1-cut,n-cut)); + assert(!dcp_property_complete(b,n-1,n+1)); + free(b); return 0; + } + assert(argc == 4 || argc == 5); + /* A missing final string byte must not be supplied by zero allocation. */ + unsigned char incomplete[]={0xd3,0,0,0,1,0,0,0x82,1,0,0,0x89,0x42}; + assert(dcp_property_complete(incomplete,sizeof(incomplete),sizeof(incomplete))); + assert(!dcp_property_complete(incomplete,12,13)); + assert(!dcp_property_complete(NULL,0,0)); + epochs(); + timing_proof(); + FILE *f = fopen(argv[1], "rb"); + assert(f && !fseek(f, 0, SEEK_END)); + long length = ftell(f); + assert(length > 0 && !fseek(f, 0, SEEK_SET)); + void *data = malloc(length); + assert(fread(data, length, 1, f) == 1); + fclose(f); + struct dcp_parse_ctx ctx; + struct dcp_display_metadata metadata, unchanged; + memset(&metadata, 0xa5, sizeof(metadata)); unchanged = metadata; + assert(!parse(data, length, &ctx)); + int ret = parse_display_metadata(&ctx, &metadata); + if (atoi(argv[2])) { + assert(ret == -EINVAL && !memcmp(&metadata, &unchanged, sizeof(metadata))); + free(data); return 0; + } + assert(!ret); + assert(dcp_property_complete(data,length,length)); + size_t aligned=round_up((size_t)length,4); + unsigned char *padded=malloc(aligned+4); + memset(padded,0xa5,aligned+4); memcpy(padded,data,length); + assert(dcp_property_complete(padded,aligned,aligned+1)); + assert(!dcp_property_complete(data,length-1,length)); + assert(!dcp_property_complete(padded,aligned,aligned+2)); + assert(!dcp_property_complete(padded,aligned+4,aligned+5)); + assert(!dcp_property_complete(padded,aligned-1,aligned+1)); + free(padded); + struct dcp_parse_ctx timing_ctx; + unsigned int count = 0; + assert(!parse(timing_blob, sizeof(timing_blob), &timing_ctx)); + struct dcp_display_mode *timings = enumerate_modes(&timing_ctx, &count, + metadata.width_cm * 10, metadata.height_cm * 10, 0, false, true); + assert(!IS_ERR(timings) && count == 1 && timings[0].metadata_timing_valid); + struct drm_display_mode mode = timings[0].mode; + assert(mode.flags == (DRM_MODE_FLAG_PHSYNC | DRM_MODE_FLAG_PVSYNC)); + assert(mode.clock == 148500 && mode.hdisplay == 1920 && mode.vdisplay == 1080); + free(timings); + const struct drm_edid *edid = dcp_metadata_edid(&metadata, &mode); + assert(!IS_ERR(edid)); + f = fopen(argv[3], "wb"); assert(f); + assert(fwrite(&edid->raw, 128, 1, f) == 1); fclose(f); + drm_edid_free(edid); + mode.flags = 0; + assert(IS_ERR(dcp_metadata_edid(&metadata, &mode))); + mode.flags = DRM_MODE_FLAG_PHSYNC | DRM_MODE_FLAG_PVSYNC; + mode.clock++; + assert(IS_ERR(dcp_metadata_edid(&metadata, &mode))); + free(data); + if (argc == 5) { + f=fopen(argv[4],"rb"); assert(f && !fseek(f,0,SEEK_END)); + length=ftell(f); assert(length>0 && !fseek(f,0,SEEK_SET)); + data=malloc(length); assert(fread(data,length,1,f)==1); fclose(f); + assert(!parse(data,length,&timing_ctx)); count=0; + timings=enumerate_modes(&timing_ctx,&count,600,330,0,false,true); + assert(!IS_ERR(timings)); bool found60=false, found120=false; + for(unsigned int i=0;iusb4; } +static void schedule_work(struct work_struct *w) { w->scheduled++; } +static const struct edid *drm_edid_raw(const struct drm_edid *e) { return (const void *)e->data; } +static bool dcpavserv_edid_service_current(struct apple_dcp *d, u64 e) { + assert(d->dcpavserv.lock); return e==d->dcpavserv.epoch && d->dcpavserv.enabled && d->dcpavserv.service; +} +''' + callback_stubs=r''' +#define le64_to_cpu(v) (v) +#define le32_to_cpu(v) (v) +#define LINK_RATE_HBR3 0x1e +static int configure_error; +static int dptxport_call_set_active_lane_count(struct apple_epic_service *s, const void *data, size_t size, void *reply, size_t room) { + if (configure_error) return configure_error; + if(sizecookie)->lane_count=((const struct dptxport_apcall_set_active_lane_count *)data)->lane_count; return 0; +} +static int dptxport_call_set_link_rate(struct apple_epic_service *s, const void *data, size_t size, void *reply, size_t room) { + if(configure_error) return configure_error; + if(sizecookie)->link_rate=((const struct dptxport_apcall_link_rate *)data)->link_rate; return 0; +} +static bool dcp_firmware_metadata_enabled(struct apple_dcp *d) { return d->hw.iomfb_method_profile==2; } +#define dcp_trace_metadata(d,p,v) ((void)0) +''' + callback_types='typedef u64 __le64;\n'+callback_types + test=r''' +static void checksum(u8 *p) { unsigned sum=0; for(unsigned i=0;i<127;i++) sum+=p[i]; p[127]=-sum; } +static void qualify(struct apple_dcp *d, struct apple_epic_service *s) { + struct dptxport_apcall_link_rate rate={.link_rate=0x1e}, rate_reply={}; + struct dptxport_apcall_set_active_lane_count lanes={.lane_count=2}, lanes_reply={}; + assert(!dptxport_call_qualified_config(s,false,&rate,sizeof(rate),&rate_reply,sizeof(rate_reply))); + assert(!dptxport_call_qualified_config(s,true,&lanes,sizeof(lanes),&lanes_reply,sizeof(lanes_reply))); + (void)d; +} +static bool current(struct apple_dcp *d,const struct dcp_display_mode *m) { + d->hpd_mutex=d->dcpavserv.lock=d->metadata.lock=true; + bool result=dcp_180hz_mode_current(d,m); + d->hpd_mutex=d->dcpavserv.lock=d->metadata.lock=false; + return result; +} +int main(int argc,char **argv) { + struct dcp_metadata_epoch epoch={}; + dcp_metadata_epoch_connection(&epoch,7,true); + assert(dcp_metadata_matches(&epoch,dcp_metadata_begin(&epoch))); + PARSER_CHECKS + struct drm_edid raw={}; u8 *p=raw.data; + struct dcp_display_mode m={}; + if(argc==2) { FILE *f=fopen(argv[1],"rb"); assert(f); assert(fread(p,1,256,f)==256 && fgetc(f)==EOF); fclose(f); } + else { + memcpy(p,"\0\xff\xff\xff\xff\xff\xff\0",8); + p[8]=4;p[9]=0x72;p[10]=0x4f;p[11]=0x0b;p[18]=1;p[19]=4;p[20]=0xa0;p[126]=1; + u8 *d=p+54; unsigned clock=41409;d[0]=clock;d[1]=clock>>8; + d[2]=1920&255;d[3]=160;d[4]=0x70;d[5]=1080&255;d[6]=26;d[7]=0x40; + d[8]=48;d[9]=32;d[10]=0x35;d[17]=0x1e; + p[75]=0xfc;memcpy(p+77,"QG270 S3\n ",13); + u8 *e=p+128;e[0]=2;e[1]=3;e[2]=12;e[4]=0x67;e[5]=0xd8;e[6]=0x5d;e[7]=0xc4;e[8]=1;e[9]=120;e[10]=0x80; + checksum(p);checksum(e); + } + /* Get the advertised DTD's porch/polarity, without copying identity data. */ + const u8 *dt=NULL; + for(unsigned off=54;off+18<=126;off+=18) if((p[off]|p[off+1]<<8)==41409)dt=p+off; + for(unsigned off=p[130];off+18<=127;off+=18) if((p[128+off]|p[129+off]<<8)==41409)dt=p+128+off; + assert(dt); + m.mode=(struct drm_display_mode){.clock=414090,.hdisplay=1920,.htotal=2080,.vdisplay=1080,.vtotal=1106}; + m.mode.hsync_start=1920+(dt[8]|(dt[11]&0xc0)<<2);m.mode.hsync_end=m.mode.hsync_start+(dt[9]|(dt[11]&0x30)<<4); + m.mode.vsync_start=1080+(dt[10]>>4|(dt[11]&0xc)<<2);m.mode.vsync_end=m.mode.vsync_start+((dt[10]&15)|(dt[11]&3)<<4); + m.mode.flags=(dt[17]&2?DRM_MODE_FLAG_PHSYNC:DRM_MODE_FLAG_NHSYNC)|(dt[17]&4?DRM_MODE_FLAG_PVSYNC:DRM_MODE_FLAG_NVSYNC); + assert(dcp_180hz_edid(p,256,&m.mode)); + assert(!dcp_180hz_edid(p,255,&m.mode)); + u8 saved[256];memcpy(saved,p,256); + p[10]^=1;checksum(p);assert(!dcp_180hz_edid(p,256,&m.mode));memcpy(p,saved,256); + p[8]^=1;checksum(p);assert(!dcp_180hz_edid(p,256,&m.mode));memcpy(p,saved,256); + unsigned hf=0;for(unsigned off=132;off<128+p[130];off+=(p[off]&31)+1) if(p[off+1]==0xd8 && p[off+2]==0x5d && p[off+3]==0xc4)hf=off; + assert(hf);p[hf+5]=82;checksum(p+128);assert(!dcp_180hz_edid(p,256,&m.mode));memcpy(p,saved,256); + p[127]^=1;assert(!dcp_180hz_edid(p,256,&m.mode));memcpy(p,saved,256); + /* Serial changes do not participate in identity matching. */ + p[12]^=0xff;checksum(p);assert(dcp_180hz_edid(p,256,&m.mode));memcpy(p,saved,256); + assert(dcp_180hz_payload(0x1e,2,414090));assert(!dcp_180hz_payload(0x14,2,414090));assert(!dcp_180hz_payload(0x1e,1,414090)); + struct apple_dcp d={.dev=&d,.phy=&raw,.active_typec_route=&m,.typec_cable_connected=true,.typec_generation=7,.nr_modes=1}; + struct apple_connector con={.dcp=&d,.connected=true,.drm_edid=&raw,.drm_edid_generation=7,.drm_edid_service_epoch=1,.base.registration_state=1}; + struct apple_epic ep={.dcp=&d,.endpoint=0x2c,.wire_v2=true}; + struct apple_epic_service s={.ep=&ep,.cookie=&d.dptxport[0],.enabled=true}; + d.connector=&con;d.hw.iomfb_method_profile=2;d.metadata.epoch.connected=true;d.metadata.epoch.generation=7; + d.dcpavserv.enabled=true;d.dcpavserv.service=&raw;d.dcpavserv.epoch=1; + d.dptxport[0]=(struct dptx_port){.enabled=true,.connected=true,.atcphy=d.phy,.service=&s}; + m.unsafe_rgb8_ticket=(struct dcp_metadata_ticket){.associated=true,.generation=7};m.unsafe_rgb8_service_epoch=1; + m.sdr_rgb=(struct dcp_color_mode){.depth=8,.format=0,.range=0,.eotf=0,.colorimetry=10}; + assert(!current(&d,&m));qualify(&d,&s);assert(current(&d,&m));assert(con.hotplug_wq.scheduled==1); + #define DENY(field,value) do { __typeof__(field) old=field;field=value;assert(!current(&d,&m));field=old;assert(current(&d,&m)); } while(0) + DENY(d.typec_generation,8);DENY(con.drm_edid_generation,6);DENY(d.dcpavserv.epoch,3);DENY(con.drm_edid_service_epoch,3); + /* Timings may precede the very first AV announcement. Only fresh epoch-1 + * EDID plus epoch-1 callback proof may admit this initial receipt. */ + m.unsafe_rgb8_service_epoch=0;m.automatic_service_pending=true;assert(current(&d,&m)); + d.dcpavserv.epoch=3;con.drm_edid_service_epoch=3;qualify(&d,&s); + assert(!current(&d,&m)); /* Initial receipt never survives retirement. */ + m.unsafe_rgb8_service_epoch=3;m.automatic_service_pending=false;assert(current(&d,&m)); + d.dcpavserv.epoch=1;con.drm_edid_service_epoch=1;m.unsafe_rgb8_service_epoch=1;qualify(&d,&s); + DENY(con.drm_edid_firmware_derived,true);DENY(d.usb4,true);DENY(d.dptx_tunnel,true); + DENY(d.quiescing,true);DENY(d.metadata.epoch.connected,false);DENY(d.rate_proof.connection_sequence,1); + DENY(d.rate_proof.rate.value,0x14);DENY(d.rate_proof.lanes.value,1);DENY(ep.endpoint,0x28); + DENY(m.sdr_rgb.depth,10);DENY(m.sdr_rgb.colorimetry,1);DENY(m.sdr_rgb.range,1);DENY(m.mode.clock,414091); + struct dptxport_apcall_link_rate rate={.link_rate=0x1e}, response={}; + configure_error=-EIO; + assert(dptxport_call_qualified_config(&s,false,&rate,sizeof(rate),&response,sizeof(response))==-EIO); + assert(!current(&d,&m));configure_error=0;qualify(&d,&s); + assert(dptxport_call_qualified_config(&s,false,&rate,sizeof(rate)-1,&response,sizeof(response))==-EINVAL); + assert(!current(&d,&m));qualify(&d,&s); + dcp_firmware_metadata_connection(&d,7,false);dcp_firmware_metadata_connection(&d,7,true); + assert(!current(&d,&m));qualify(&d,&s);assert(!current(&d,&m)); + m.automatic_connection_sequence=d.rate_proof.connection_sequence;assert(current(&d,&m)); + dcp_firmware_metadata_hotplug(&d,true);dcp_firmware_metadata_hotplug(&d,false); + assert(!current(&d,&m)); + dcp_firmware_metadata_connection(&d,7,true);m.automatic_connection_sequence=d.rate_proof.connection_sequence;con.edid_invalidation_pending=false;qualify(&d,&s);assert(current(&d,&m)); + struct dcp_180hz_token old=dcp_180hz_config_begin(&s,false);d.typec_generation++; + dcp_180hz_config_complete(&s,false,old,0x1e,true);assert(!d.rate_proof.rate.valid);d.typec_generation--;qualify(&d,&s); + old=dcp_180hz_config_begin(&s,true);d.dcpavserv.epoch+=2; + dcp_180hz_config_complete(&s,true,old,2,true);assert(d.rate_proof.lanes.valid);assert(!current(&d,&m));d.dcpavserv.epoch-=2;qualify(&d,&s); + old=dcp_180hz_config_begin(&s,false);dcp_180hz_invalidate(&d); + dcp_180hz_config_complete(&s,false,old,0x1e,true);assert(!current(&d,&m));qualify(&d,&s); + old=dcp_180hz_config_begin(&s,false);dcp_180hz_config_complete(&s,false,old,0x1e,false);assert(!current(&d,&m)); + d.dcpavserv.epoch=0;old=dcp_180hz_config_begin(&s,false);d.dcpavserv.epoch=1; + dcp_180hz_config_complete(&s,false,old,0x1e,true);assert(d.rate_proof.rate.valid);qualify(&d,&s);assert(current(&d,&m)); + puts("PASS: actual binary parser, Acer EDID, capacity, callback success, late-proof event, generation/AV lifetime and foreign-sink controls"); +} +'''.replace('PARSER_CHECKS', checks) + unit=namespace['prefix']+namespace['metadata']+namespace['epoch']+namespace['header']+namespace['edid_header']+namespace['parser']+blobs+policy+proof+state+source+callback_types+callback_stubs+callback+connection+test + return {**locals(), "block": block} + +def publication(root): + + n = policy(root) + n.update({key:n['namespace'][key] for key in ('prefix','metadata','epoch','header','edid_header','parser')}) + base = n['base'] + block = n['block'] + av = (base/'epic/dpavservep.c').read_text() + iomfb = (base/'iomfb.c').read_text() + template = (base/'iomfb_template.c').read_text() + + state = n['state'] + state = state.replace('struct apple_connector {', '''struct drm_device { void *dev; struct { bool mutex; } mode_config; }; +struct drm_connector { struct drm_device *dev; int registration_state; void *state; + struct { struct { unsigned min_vfreq,max_vfreq; } monitor_range; } display_info; +}; +struct apple_connector {''') + state = state.replace(' struct { int registration_state; } base;', ' struct drm_connector base;') + state = state.replace('struct apple_epic_service { struct apple_epic *ep; void *cookie; bool enabled; };', '''struct apple_epic_service { struct apple_epic *ep; void *cookie; bool enabled,torndown; + unsigned users,channel; struct { u64 generation,epoch; } admission; +};''') + state = state.replace('struct { bool lock, enabled; void *service; u64 epoch; } dcpavserv;', 'struct { bool lock, enabled; void *service; u64 epoch; int enable_completion; } dcpavserv;') + state = state.replace('u64 typec_generation; unsigned int nr_modes;', '''u64 typec_generation; unsigned int nr_modes; + struct dcp_display_mode *modes; + struct apple_epic *dcpavservep; + int fw_compat,width_mm,height_mm,fixed_connector_type; unsigned notch_height; + struct { size_t length,received; void *data; bool contiguous,payload_complete; + u64 av_service_epoch,automatic_connection_sequence; bool automatic_service_pending; + struct dcp_metadata_ticket metadata_ticket,mode_ticket; + } chunks;''') + + start = state.index('static bool dcpavserv_edid_service_current(') + state = state[:start] + + stubs = r''' +#define WRITE_ONCE(p,v) ((p)=(v)) +#define platform_device apple_dcp +#define platform_get_drvdata(p) ((struct apple_dcp *)(p)) +#define to_apple_connector(p) ((struct apple_connector *)((char *)(p)-offsetof(struct apple_connector,base))) +#define xchg(p,v) __extension__ ({ __typeof__(*(p)) old=*(p); *(p)=(v); old; }) +#define DRM_MODE_MATCH_TIMINGS 1 +#define DRM_MODE_MATCH_CLOCK 2 +#define DCP_FIRMWARE_H17P 2 +#define DPAVSERV_ENDPOINT_V2 0x2a +#define DRM_CONNECTOR_eDP 14 +#define DRM_MODE_CONNECTOR_eDP 14 +#define DCP_FW_VER 26 +#define DCP_FW_VERSION(a,b,c) (a) +#define SZ_1M (1024*1024) +#define kzalloc(n,f) calloc(1,n) +#define dev_err(...) ((void)0) +#define dev_warn(...) ((void)0) +#define dev_info(...) ((void)0) +#define dev_dbg(...) ((void)0) +#define trace_dcpavserv_init(...) ((void)0) +#include +static bool *hpd_lock,*av_lock; +static void mutex_lock(bool *p) { assert(!*p); if(p==hpd_lock) assert(!*av_lock); *p=true; } +static void mutex_unlock(bool *p) { assert(*p); *p=false; } +static void complete(int *p) { (*p)++; } +static void reinit_completion(int *p) { *p=0; } +static struct apple_epic_service *afk_service_get(struct apple_epic_service *s) { if(!s->enabled) return NULL; s->users++; return s; } +static void afk_service_put(struct apple_epic_service *s) { assert(s->users); s->users--; } +static void afk_service_retire(struct apple_epic_service *s) { assert(!*hpd_lock); s->enabled=false; s->torndown=true; } +static void dpavservep_detach(struct apple_dcp *d); +static struct apple_dcp *probing; +static struct drm_edid transport_edid; +static unsigned raw_allocations,published,reads,retirement_during_read; +static bool fail_update,invalidate_during_duplicate,replace_catalog_during_duplicate; +static void conflicting_catalog_arrived(void); +static const struct drm_edid *dcpavserv_copy_edid(struct apple_dcp *d,u64 generation,u64 *epoch) { + assert(!d->hpd_mutex && !d->dcpavserv.lock && !d->metadata.lock); + if(!d->dcpavserv.enabled || !d->dcpavserv.service || generation!=d->typec_generation) return ERR_PTR(-ENODEV); + *epoch=d->dcpavserv.epoch;reads++; + struct drm_edid *e=malloc(sizeof(*e)); assert(e); *e=transport_edid;raw_allocations++; + if(retirement_during_read) { retirement_during_read--;dpavservep_detach(d); } + return e; +} +static void drm_edid_free(const struct drm_edid *e) { if(e) { assert(raw_allocations);raw_allocations--;free((void *)e); } } +static int drm_edid_connector_update(struct drm_connector *c,const struct drm_edid *e) { + assert(c->dev->mode_config.mutex); (void)e; return fail_update?-EIO:0; +} +static const struct drm_edid *dcp_firmware_edid(struct apple_dcp *d,u64 *generation) { (void)d; (void)generation; return ERR_PTR(-ENODATA); } +static bool dcp_firmware_metadata_current(struct apple_dcp *d,u64 generation) { (void)d; (void)generation; return false; } +static bool dcp_has_panel(struct apple_dcp *d) { return d->fixed_connector_type==DRM_CONNECTOR_eDP; } +static struct drm_display_mode *drm_mode_duplicate(struct drm_device *dev,const struct drm_display_mode *m) { + assert(dev->mode_config.mutex);struct drm_display_mode *copy=malloc(sizeof(*copy));assert(copy);*copy=*m; + if(invalidate_during_duplicate) { invalidate_during_duplicate=false;dcp_180hz_invalidate(probing); } + if(replace_catalog_during_duplicate) { replace_catalog_during_duplicate=false;conflicting_catalog_arrived(); } + return copy; +} +static void drm_mode_probed_add(struct drm_connector *c,struct drm_display_mode *m) { + assert(c->dev->mode_config.mutex && probing->hpd_mutex && probing->dcpavserv.lock && probing->metadata.lock); + published++;free(m); +} +static void drm_mode_destroy(struct drm_device *dev,struct drm_display_mode *m) { (void)dev; free(m); } +static bool drm_mode_match(const struct drm_display_mode *a,const struct drm_display_mode *b,unsigned flags) { + (void)flags;return a->clock==b->clock && a->hdisplay==b->hdisplay && a->vdisplay==b->vdisplay && a->htotal==b->htotal && a->vtotal==b->vtotal && a->hsync_start==b->hsync_start && a->hsync_end==b->hsync_end && a->vsync_start==b->vsync_start && a->vsync_end==b->vsync_end && a->flags==b->flags; +} +static void drm_connector_set_vrr_capable_property(struct drm_connector *c,bool capable) { (void)c; (void)capable; } +static void dcp_retry_placeholder_edid(struct apple_dcp *d,const struct drm_edid *e) { (void)d; (void)e; } +static unsigned pending_probes; +void dcp_180hz_retry(struct apple_dcp *d) { (void)d;pending_probes++; } +static void dcp_firmware_metadata_begin(struct apple_dcp *d) { + unsigned long flags;spin_lock_irqsave(&d->metadata.lock,flags); + d->chunks.metadata_ticket=dcp_metadata_begin(&d->metadata.epoch);spin_unlock_irqrestore(&d->metadata.lock,flags); +} +static void dcp_firmware_metadata_timing(struct apple_dcp *d) { (void)d; } +static void dcp_firmware_metadata_attributes(struct apple_dcp *d,const struct dcp_display_metadata *m) { (void)d; (void)m; } +static void dcp_set_dimensions(struct apple_dcp *d) { (void)d; } +struct dcp_set_dcpav_prop_end_req { char key[64]; }; +''' + transport = '\n'.join(block(av,marker) for marker in [ + 'static void dcpavserv_notify_edid(', 'static bool dcpavserv_identity(', + 'static bool dcpavserv_admission_current(', 'static int dcpavserv_prepare(', + 'static void dcpavserv_init(', 'static void dcpavserv_invalidate_edid(', + 'static void dcpavserv_teardown(', 'void dpavservep_detach(', + 'bool dcpavserv_edid_current(', 'bool dcpavserv_edid_service_current(', + ]) + mode_helpers = iomfb[iomfb.index('static struct dcp_display_mode *dcp_modes_snapshot('):iomfb.index('int dcp_get_modes(')] + publication = block(iomfb,'static void dcp_clear_connector_edid(')+'\n'+block(iomfb,'static void dcp_consume_edid_invalidation(')+mode_helpers+block(iomfb,'int dcp_get_modes(')+block(iomfb,'struct dcp_display_mode *lookup_mode(') + # Drive malformed replacements through the same chunk/end ingress as firmware. + # The dictionary cache is a boundary substitute and takes ownership of the blob. + receipt = r''' +struct dcp_set_dcpav_prop_chunk_req { u32 offset,length; u8 data[4096]; }; +#define dcp_connector_update_dict(con,key,chunks) do { (void)(con); (void)(key); free((chunks)->data); } while (0) +''' + '\n'.join(block(template,marker) for marker in [ + 'static u8 dcpep_cb_prop_start(', 'static u8 dcpep_cb_prop_chunk(', + 'static bool dcpep_process_chunks(', 'static u8 dcpep_cb_prop_end(']) + + main_prefix = n['test'].split('int main(int argc,char **argv) {',1)[1].split(' struct apple_dcp d={',1)[0] + helpers = n['test'].split('int main(int argc,char **argv) {',1)[0] + helpers = helpers.replace(block(helpers, 'static void qualify('), '') + fixture = r''' +static struct apple_dcp d; +static struct apple_connector con; +static struct drm_device drm; +static struct apple_epic ep,av_ep; +static struct apple_epic_service s,av_services[3]; +static void reset(void) { + assert(!raw_allocations); + d=(struct apple_dcp){.dev=&d,.phy=&transport_edid,.active_typec_route=&con,.typec_cable_connected=true,.typec_generation=7,.fw_compat=2,.width_mm=600,.height_mm=330}; + drm=(struct drm_device){}; + con=(struct apple_connector){.dcp=&d,.connected=true,.base={.dev=&drm,.registration_state=1}}; + ep=(struct apple_epic){.dcp=&d,.endpoint=0x2c,.wire_v2=true}; + av_ep=(struct apple_epic){.dcp=&d,.endpoint=0x2a,.wire_v2=true}; + s=(struct apple_epic_service){.ep=&ep,.cookie=&d.dptxport[0],.enabled=true}; + for(unsigned i=0;i<3;i++) av_services[i]=(struct apple_epic_service){.ep=&av_ep,.enabled=true,.channel=1+4*i}; + d.connector=&con;d.hw.iomfb_method_profile=2;d.metadata.epoch.connected=true;d.metadata.epoch.generation=7;dcp_metadata_epoch_connection(&d.metadata.epoch,7,true); + d.dptxport[0]=(struct dptx_port){.enabled=true,.connected=true,.atcphy=d.phy,.service=&s}; + d.dcpavservep=&av_ep;hpd_lock=&d.hpd_mutex;av_lock=&d.dcpavserv.lock;probing=&d; + published=reads=pending_probes=0;fail_update=invalidate_during_duplicate=false; +} +static void admit(unsigned index) { + struct apple_epic_service *a=&av_services[index]; + assert(!dcpavserv_prepare(a,"dcpav-service-epic","AppleDCPDP2HDMI",a->channel)); + dcpavserv_init(a,"dcpav-service-epic","AppleDCPDP2HDMI",a->channel); + assert(d.dcpavserv.enabled && d.dcpavserv.service==a); +} +static void start_receipt(void) { + u32 size=sizeof(input_0);assert(dcpep_cb_prop_start(&d,&size)); + memcpy(d.chunks.data,input_0,size);d.chunks.received=size;d.chunks.contiguous=true; +} +static void finish_receipt(void) { + struct dcp_set_dcpav_prop_end_req request={.key="TimingElements"}; + assert(dcpep_process_chunks(&d,&request));assert(d.nr_modes==1 && d.modes[0].automatic_rgb8_quirk); + free(d.chunks.data);d.chunks.data=NULL; +} +static void timings(void) { start_receipt();finish_receipt(); } +static void callbacks(bool lanes) { + if(lanes) { struct dptxport_apcall_set_active_lane_count a={.lane_count=2},b={}; + assert(!dptxport_call_qualified_config(&s,true,&a,sizeof(a),&b,sizeof(b))); } + else { struct dptxport_apcall_link_rate a={.link_rate=0x1e},b={}; + assert(!dptxport_call_qualified_config(&s,false,&a,sizeof(a),&b,sizeof(b))); } +} +static void probe(bool expected) { + drm.mode_config.mutex=true;unsigned before=published; + int count=dcp_get_modes(&con.base);drm.mode_config.mutex=false; + assert(count==(int)expected && published==before+(unsigned)expected); + if(d.nr_modes) assert(!!lookup_mode(&d,&d.modes[0].mode)==expected); + assert(!d.hpd_mutex && !d.dcpavserv.lock && !d.metadata.lock); +} +static void cleanup(void) { + drm_edid_free(con.drm_edid);con.drm_edid=NULL;free(d.modes);d.modes=NULL; + free(d.chunks.data);d.chunks.data=NULL; + if(d.dcpavserv.service)dpavservep_detach(&d); + assert(!raw_allocations); +} +static void permutations(void) { + unsigned runs=0; + for(unsigned a=0;a<4;a++) for(unsigned b=0;b<4;b++) for(unsigned c=0;c<4;c++) for(unsigned e=0;e<4;e++) { + if(a==b || a==c || a==e || b==c || b==e || c==e)continue; + reset();unsigned order[]={a,b,c,e};unsigned seen=0; + for(unsigned i=0;i<4;i++) { + unsigned notifications=con.hotplug_wq.scheduled; + switch(order[i]) { case 0:callbacks(false);break;case 1:callbacks(true);break;case 2:timings();break;case 3:admit(0);break; } + seen|=1u<notifications); + } + assert(current(&d,&d.modes[0]));cleanup();runs++; + } + assert(runs==24); +} +''' + main=r''' + transport_edid=raw; + /* Runtime EDID porches are encoded into the timing fixture by this + * harness, without embedding identity bytes or fabricating tickets. */ + permutations(); + reset();callbacks(false);callbacks(true);timings();probe(false);admit(0);probe(true); + assert(d.rate_proof.rate.valid && d.rate_proof.lanes.valid); /* No second callback. */ + struct dcp_display_mode old=d.modes[0]; + dcpavserv_teardown(&av_services[0]);probe(false); + admit(1);probe(false);assert(!memcmp(&old,d.modes,sizeof(old))); /* No retagging. */ + free(d.modes);d.modes=NULL;timings();probe(true); /* Existing physical config survives AV-only CLOSE. */ + dcpavserv_teardown(&av_services[0]);probe(true); /* Old CLOSE cannot revoke the new owner. */ + cleanup(); + + reset();callbacks(false);callbacks(true);timings();admit(0);probe(true); + dcp_firmware_metadata_connection(&d,8,false);d.typec_generation=8;d.typec_cable_connected=false;dpavservep_detach(&d);probe(false); + d.typec_generation=9;d.typec_cable_connected=true;dcp_firmware_metadata_connection(&d,9,true); + free(d.modes);d.modes=NULL;timings();admit(1);probe(false); /* Fresh EDID cannot rescue old PHY configuration. */ + callbacks(false);probe(false);callbacks(true);probe(true);cleanup(); + + reset();callbacks(false);callbacks(true);start_receipt();dpavservep_detach(&d);admit(0);finish_receipt();probe(false);cleanup(); + reset();callbacks(false);callbacks(true);admit(0);start_receipt();dcpavserv_teardown(&av_services[0]);admit(1);finish_receipt();probe(false);cleanup(); + reset();callbacks(false);callbacks(true);dpavservep_detach(&d);timings();admit(0);probe(true);cleanup(); /* Legitimate receipt awaiting post-retirement admission. */ + + reset();callbacks(false);callbacks(true);timings();admit(0);retirement_during_read=1;probe(false);cleanup(); + reset();callbacks(false);callbacks(true);timings();admit(0);invalidate_during_duplicate=true;probe(false);cleanup(); + reset();callbacks(false);callbacks(true);timings();admit(0);fail_update=true;probe(false);assert(!con.drm_edid);cleanup(); + reset();callbacks(false);callbacks(true);timings();admit(0);transport_edid.data[10]^=1;checksum(transport_edid.data);probe(false);cleanup();transport_edid=raw; + reset();callbacks(false);callbacks(true);timings();admit(0);configure_error=-EIO; + struct dptxport_apcall_link_rate req={.link_rate=0x1e},resp={};assert(dptxport_call_qualified_config(&s,false,&req,sizeof(req),&resp,sizeof(resp))==-EIO); + configure_error=0;probe(false);cleanup(); + puts("PASS: 24 callback/timing/AV arrival permutations and real get_modes/lookup publication; early configuration needs no second callback; CLOSE, physical reconnect, receipt epochs and publication races deny stale proof"); +} +''' + unit=n['prefix']+n['metadata']+n['epoch']+n['header']+n['edid_header']+n['parser']+n['blobs']+n['policy']+n['proof']+state+stubs+transport+n['source']+n['callback_types']+n['callback_stubs']+n['callback']+n['connection']+publication+receipt+helpers+fixture+'int main(int argc,char **argv) {'+main_prefix+main + # Conflicts are scanned across the full binary receipt, including filtered + # siblings. Exercise both orders, replacement receipts and equivalent aliases. + base_fixture=copy.deepcopy(n['fixture']) + conflict_blobs='' + conflict_checks='' + for kind in ('color', 'timing', 'other-color'): + for reverse in (False, True): + candidate=copy.deepcopy(base_fixture) + other=copy.deepcopy(candidate[0]) + if kind=='color': + other_color=copy.deepcopy(other['ColorModes'][0]) + other_color.update(Depth=10,EOTF=2,Score=999999) + candidate[0]['ColorModes'].insert(0 if reverse else 1,other_color) + elif kind=='other-color': + other.update(ID=79,DSCRequiredColorElementIDs=[80]) + other['ColorModes'][0].update(Depth=10,EOTF=2) + candidate.insert(0 if reverse else 1,other) + else: + other['HorizontalAttributes'].update(Total=2200,FrontPorch=88,SyncWidth=44) + other['VerticalAttributes'].update(Total=1125,FrontPorch=4,SyncWidth=5,PreciseSyncRate=60<<16) + other.update(DSCRequiredColorElementIDs=[80]) + candidate.insert(0 if reverse else 1,other) + raw_blob=bytearray(struct.pack('before);assert(!lookup_mode(&d,&saved));free(old); } + cleanup(); + if(late) { + reset();callbacks(false);callbacks(true);admit(0);timings();probe(true);old=d.modes;saved=old[0].mode; + u32 size=len;assert(dcpep_cb_prop_start(&d,&size));memcpy(d.chunks.data,blob,size); + d.chunks.received=size;d.chunks.contiguous=true; + before=con.hotplug_wq.scheduled; + replace_catalog_during_duplicate=true;probe(false); + assert(con.hotplug_wq.scheduled>before); + assert(!current(&d,old) && !lookup_mode(&d,&saved));free(old);cleanup(); + } +} +static void conflicting_receipts(void) { + CONFLICT_CHECKS + reset();callbacks(false);callbacks(true);admit(0);receipt_blob(equivalent_ids,sizeof(equivalent_ids));probe(true);cleanup(); + puts("PASS: conflicting color/timing IDs, discarded siblings, both receipt orders and late conflicts deny getter/lookup; equivalent color aliases remain eligible"); +} +""".replace('CONFLICT_CHECKS',conflict_checks) + unit=unit.replace('int main(int argc,char **argv) {',conflict_blobs+conflict_helper+'int main(int argc,char **argv) {') + unit=unit.replace(' permutations();',' permutations();conflicting_receipts();') + # Execute the production metadata notifier so a manual probe cannot hide a + # missing connector refresh after the last eligible mode is withdrawn. + unit=unit.replace('bool lock, attributes_valid, timing_valid, hpd_seen; struct dcp_metadata_epoch epoch;', + 'bool lock, attributes_valid, timing_valid, hpd_seen; struct dcp_metadata_ticket attributes_ticket,timing_ticket; struct drm_display_mode preferred; struct dcp_metadata_epoch epoch;') + dcp_source=(base/'dcp.c').read_text() + metadata_notifier='\n'.join(block(dcp_source,marker) for marker in ( + 'static void dcp_firmware_metadata_notify(', + 'static bool dcp_firmware_metadata_complete(', + 'void dcp_firmware_metadata_timing(')) + unit=unit.replace(block(unit,'static void dcp_firmware_metadata_timing('), + '#define dcp_trace_metadata(d,p,v) ((void)0)\nstatic bool dcp_firmware_metadata_enabled(struct apple_dcp *d);\n'+metadata_notifier) + + failed_receipts = r''' +static bool malformed_receipt(const char *key,unsigned fault) { + u32 size=fault==1 ? 3 : fault==2 ? 4 : sizeof(input_0); + assert(dcpep_cb_prop_start(&d,&size)); + struct dcp_set_dcpav_prop_chunk_req chunk={.length=size}; + assert(size<=sizeof(chunk.data));memcpy(chunk.data,input_0,size); + if(fault==0)chunk.data[0]^=1; + assert(dcpep_cb_prop_chunk(&d,&chunk)); + struct dcp_set_dcpav_prop_end_req request={}; + strcpy(request.key,key); + bool accepted=dcpep_cb_prop_end(&d,&request); + assert(!d.chunks.data && !d.chunks.length); + return accepted; +} +static void failed_replacements(void) { + for(unsigned fault=0;fault<3;fault++) { + reset();callbacks(false);callbacks(true);timings();admit(0);probe(true); + struct dcp_display_mode *old=d.modes; + struct drm_display_mode saved=old[0].mode; + unsigned before=con.hotplug_wq.scheduled; + assert(!malformed_receipt("TimingElements",fault)); + assert(!current(&d,old) && !lookup_mode(&d,&saved)); + assert(con.hotplug_wq.scheduled>before);probe(false); + /* Fresh catalog can recover using still-current EDID and link proof. */ + timings();probe(true);free(old);cleanup(); + } + reset();callbacks(false);callbacks(true);timings();admit(0);probe(true); + assert(!malformed_receipt("DisplayAttributes",0));probe(true);cleanup(); + puts("PASS: actual chunk/end callbacks withdraw malformed replacement exceptions and notify DRM; fresh catalogs recover; unrelated bad properties preserve admission"); +} +''' + unit=unit.replace('int main(int argc,char **argv) {',failed_receipts+'int main(int argc,char **argv) {') + unit=unit.replace(' permutations();conflicting_receipts();',' permutations();conflicting_receipts();failed_replacements();') + return {**locals(), "block": block} + +def hotplug(root): + n = publication(root) + unit = n['unit'] + block = n['block'] + base = root / 'drivers/gpu/drm/apple' + dptx = (base / 'dptxep.c').read_text() + training = (base / 'dptx-j700-training.h').read_text() + unit = unit.replace('struct dptx_port { u32 lane_count, link_rate;', ''' +union phy_configure_opts { struct { unsigned lanes,link_rate; bool set_lanes,set_rate,set_voltages; } dp; }; +struct dptx_port { union phy_configure_opts phy_ops; int linkcfg_completion; u32 pending_link_rate; +u32 lane_count, link_rate;''') + unit = unit.replace('int fw_compat,width_mm,height_mm,fixed_connector_type;', 'unsigned dptx_phy; int fw_compat,width_mm,height_mm,fixed_connector_type;') + real_handlers = r''' +#define cpu_to_le32(v) (v) +#define cpu_to_le64(v) (v) +#define LINK_RATE_RBR 0x06 +#define LINK_RATE_HBR 0x0a +#define LINK_RATE_HBR2 0x14 +#define PHY_MODE_DP 1 +#define trace_dptxport_call_set_link_rate(...) ((void)0) +static unsigned phy_capacity=2, phy_calls, hardware_rate, hardware_lanes; +static int validate_error; +static int phy_validate(void *phy,int mode,int submode,union phy_configure_opts *o) { + assert(phy);(void)mode;(void)submode; + if(validate_error)return validate_error; + o->dp.lanes=phy_capacity;return 0; +} +static int phy_configure(void *phy,union phy_configure_opts *o) { + assert(phy);phy_calls++; + if(configure_error)return configure_error; + if(o->dp.set_lanes)hardware_lanes=o->dp.lanes; + if(o->dp.set_rate)hardware_rate=o->dp.link_rate; + return 0; +} +static void dcp_tunnel_set_rate(struct apple_dcp *d,void *phy,unsigned rate) { + (void)d;(void)phy;(void)rate;assert(false); +} +''' + real_handlers += block(training, 'static int dptx_j700_validate_lanes(') + '\n' + real_handlers += block(dptx, 'static int dptxport_call_set_active_lane_count(') + '\n' + real_handlers += block(dptx, 'static int dptxport_call_set_link_rate(') + '\n' + for marker in ['static int dptxport_call_set_active_lane_count(', 'static int dptxport_call_set_link_rate(']: + unit = unit.replace(block(unit, marker), '') + unit = unit.replace('static int configure_error;', 'static int configure_error;\n' + real_handlers) + unit = unit.replace('published=reads=pending_probes=0;', 'phy_capacity=2;phy_calls=hardware_rate=hardware_lanes=0;validate_error=0;published=reads=pending_probes=0;') + unit = unit.replace('assert(current(&d,&d.modes[0]));cleanup();runs++;', 'assert(current(&d,&d.modes[0]));assert(hardware_rate==8100 && hardware_lanes==2 && phy_calls==2);cleanup();runs++;') + unit = unit.replace('struct apple_dcp {', '''struct apple_dcp { + struct { bool lock,presentation_paused; } iomfb; + struct { bool valid,changing; } mode_state; + struct work_struct external_stop_work,vblank_wq; + bool external_physically_absent,main_display,typec_crtc_off,external_cable,external_error,external_power_attempted,external_stopped; + bool external_hotplug_pending,external_hotplug_connected; + u64 external_stop_generation,external_rx_generation,external_crtc_generation; + unsigned swap_watchdog_retrains;int typec_iomfb_hpd_ready;''') + stubs = r''' +#define DCP_HOTPLUG_NOTIFY 1 +static bool dcp_is_external_h17p(struct apple_dcp *d) { return d->hw.iomfb_method_profile==2; } +static bool dcp_is_typec_output(struct apple_dcp *d) { (void)d;return true; } +static void complete_all(int *p) { (*p)++; } +#define dcp_mode_invalidate(s) ((s)->valid=false) +#define dcp_mode_hotplug(s,c,p) __extension__ ({ (void)(s);if(p)*(p)=(c);1u; }) +static void dcp_handle_hotplug_actions(struct apple_dcp *d,unsigned action) { (void)d;(void)action; } +''' + dcp = (root / 'drivers/gpu/drm/apple/dcp.c').read_text() + template = (root / 'drivers/gpu/drm/apple/iomfb_template.c').read_text() + functions = stubs + block(dcp, 'bool dcp_external_hotplug(') + '\n' + block(template, 'static void dcpep_cb_hotplug(') + + main=r"""int main(int argc,char **argv) { MAIN_PREFIX + transport_edid=raw;permutations();conflicting_receipts();failed_replacements(); + unsigned runs=0; + for(unsigned a=0;a<4;a++)for(unsigned b=0;b<4;b++)for(unsigned c=0;c<4;c++)for(unsigned e=0;e<4;e++) { + if(a==b||a==c||a==e||b==c||b==e||c==e)continue; + reset();callbacks(false);callbacks(true);timings();admit(0);probe(true); + d.external_cable=true;d.external_power_attempted=true;d.external_rx_generation=7; + d.iomfb.lock=true;u64 connected=1;dcpep_cb_hotplug(&d,&connected);d.iomfb.lock=false; + struct dcp_display_mode *old=d.modes;struct drm_display_mode saved=old[0].mode; + u64 sequence=d.rate_proof.sequence,connection=d.rate_proof.connection_sequence; + d.iomfb.lock=true;connected=0;dcpep_cb_hotplug(&d,&connected);d.iomfb.lock=false; + assert(d.external_stop_work.scheduled==1 && d.external_stop_generation==7); + assert(sequence!=d.rate_proof.sequence && connection!=d.rate_proof.connection_sequence); + assert(!d.rate_proof.rate.valid && !d.rate_proof.lanes.valid); + assert(!current(&d,old) && !lookup_mode(&d,&saved)); + assert(con.edid_invalidation_pending);probe(false); /* Work deliberately not executed. */ + dpavservep_detach(&d);dcp_180hz_invalidate(&d); /* Deferred retirement/release. */ + unsigned order[]={a,b,c,e}; + for(unsigned i=0;i<4;i++) { + switch(order[i]) { case 0:callbacks(false);break;case 1:callbacks(true);break;case 2:timings();break;case 3:admit(1);break; } + probe(false);assert(!current(&d,old)); + } + assert(d.rate_proof.rate.valid && d.rate_proof.lanes.valid); + /* Repeated low reports before reassertion are not a new HPD loss. */ + sequence=d.rate_proof.sequence;d.iomfb.lock=true;connected=0; + dcpep_cb_hotplug(&d,&connected);d.iomfb.lock=false; + assert(d.rate_proof.sequence==sequence && d.rate_proof.rate.valid && d.rate_proof.lanes.valid); + d.iomfb.lock=true;connected=1;dcpep_cb_hotplug(&d,&connected);d.iomfb.lock=false; + assert(d.metadata.epoch.connected && d.metadata.epoch.invalidated); + probe(true);assert(!current(&d,old));assert(phy_calls==4 && hardware_rate==8100 && hardware_lanes==2); + free(old);cleanup();runs++; + } + assert(runs==24); + /* Initial bootstrap false and power/paused suppression keep proof intact. */ + reset();callbacks(false);callbacks(true);timings();admit(0);probe(true); + d.iomfb.lock=true;u64 connected=0;u64 sequence=d.rate_proof.sequence; + dcpep_cb_hotplug(&d,&connected);assert(d.rate_proof.sequence==sequence && !con.edid_invalidation_pending); + connected=1;dcpep_cb_hotplug(&d,&connected); + connected=0;d.typec_crtc_off=true;dcpep_cb_hotplug(&d,&connected);assert(d.rate_proof.sequence==sequence); + d.typec_crtc_off=false;d.iomfb.presentation_paused=true;dcpep_cb_hotplug(&d,&connected); + assert(d.rate_proof.sequence==sequence && !con.edid_invalidation_pending);d.iomfb.lock=false;probe(true); + dcpavserv_teardown(&av_services[0]);cleanup(); + puts("PASS: actual RX HPD loss immediately denies getter/lookup before deferred work; 24 fresh-proof recovery orders need only one rate/lane callback; bootstrap/power/paused suppression preserved"); +}""".replace('MAIN_PREFIX',n['main_prefix']) + unit=unit.replace(block(unit,'int main(int argc,char **argv) {'),functions+'\n'+main) + return unit + + +def metadata(root, output): + """Keep malformed/duplicate metadata cases with synthetic bounded wire input.""" + n = parser_fixture(root) + fixture = n['fixture'] + cases = [(fixture, False)] + for path, value in [ + (('MaxHorizontalImageSize',), 256), (('MaxVerticalImageSize',), '33'), + (('Chromaticity', 'Red', 'X'), True), (('Chromaticity', 'Blue', 'Y'), 65536), + (('Chromaticity', 'Green', 'Y'), 64000), (('DefaultWhitePoint', 'Gamma'), 65535), + (('DefaultWhitePoint', 'Gamma'), 240000), (('ProductAttributes', 'ManufacturerID'), 'AC'), + (('ProductAttributes', 'ManufacturerID'), 'A\0R'), (('ProductAttributes', 'LegacyManufacturerID'), 1), + (('ProductAttributes', 'ProductName'), 'x' * 14), (('ProductAttributes', 'ProductID'), 65536), + (('ProductAttributes', 'YearOfManufacture'), 1989), (('ProductAttributes', 'WeekOfManufacture'), 54), + ]: + bad = copy.deepcopy(fixture) + target = bad + for key in path[:-1]: + target = target[key] + target[path[-1]] = value + cases.append((bad, True)) + for key in ('Chromaticity', 'DefaultWhitePoint', 'ProductAttributes'): + bad = copy.deepcopy(fixture) + del bad[key] + cases.append((bad, True)) + second = copy.deepcopy(fixture[key]) + cases.append((n['ObjectPairs'](list(fixture.items()) + [(key, second)]), True)) + for path, field in [(('Chromaticity', 'Red'), 'X'), (('ProductAttributes',), 'ProductName')]: + bad = copy.deepcopy(fixture) + target = bad + for key in path[:-1]: + target = target[key] + value = target[path[-1]] + target[path[-1]] = n['ObjectPairs'](list(value.items()) + [(field, value[field])]) + cases.append((bad, True)) + inputs = output / 'metadata-inputs' + inputs.mkdir(parents=True, exist_ok=True) + arguments = [] + for index, (value, bad) in enumerate(cases): + data = bytearray(struct.pack('H', 1138) + assert raw[21:24] == bytes([60, 33, 120]) + assert raw[38:54] == b'\x01' * 16 and raw[35:38] == b'\0' * 3 + assert struct.unpack_from('> 4) << 8) == 1920 + assert raw[57] + ((raw[58] & 15) << 8) == 280 + assert raw[59] + ((raw[61] >> 4) << 8) == 1080 + assert raw[60] + ((raw[61] & 15) << 8) == 45 + assert raw[62:65] == bytes([88, 44, 0x45]) and raw[71] == 0x1e + chroma = [raw[27] * 4 + ((raw[25] >> 6) & 3), raw[28] * 4 + ((raw[25] >> 4) & 3), + raw[29] * 4 + ((raw[25] >> 2) & 3), raw[30] * 4 + (raw[25] & 3), + raw[31] * 4 + ((raw[26] >> 6) & 3), raw[32] * 4 + ((raw[26] >> 4) & 3), + raw[33] * 4 + ((raw[26] >> 2) & 3), raw[34] * 4 + (raw[26] & 3)] + assert chroma == [697, 317, 265, 632, 146, 81, 321, 337] + assert raw[72:77] == bytes.fromhex('000000fc00') + + +def retry(root): + source = (root / 'drivers/gpu/drm/apple/dcp-180hz.c').read_text() + production = '\n'.join(block(source, marker) for marker in ( + 'static void dcp_180hz_retry_work(', 'void dcp_180hz_retry(', 'static void dcp_180hz_cancel(')) + prefix = r""" +#include +#include +#include +#include +#include +typedef uint64_t u64; +#define READ_ONCE(x) (x) +#define WRITE_ONCE(p,v) ((p)=(v)) +#define guard(kind) (void) +#define spin_lock_irqsave(p,f) do { assert(!*(p)); *(p)=true; (f)=0; } while (0) +#define spin_unlock_irqrestore(p,f) do { (void)(f); assert(*(p)); *(p)=false; } while (0) +#define container_of(p,t,m) ((t *)((char *)(p)-offsetof(t,m))) +#define to_delayed_work(p) container_of(p,struct delayed_work,work) +#define to_platform_device(p) (p) +#define msecs_to_jiffies(x) (x) +struct work_struct { unsigned scheduled; }; +struct delayed_work { struct work_struct work; unsigned delay; bool pending; }; +struct apple_connector { void *dcp; struct work_struct hotplug_wq; }; +struct apple_dcp { + bool hpd_mutex, typec_cable_connected, quiescing, rate_retry_initialized; + struct { bool lock; } metadata; + struct { u64 retry_generation; unsigned retries; } rate_proof; + u64 typec_generation; + struct apple_connector *connector; + void *dev; + struct delayed_work rate_retry_wq; +}; +static int system_freezable_wq; +static unsigned submissions, cancellations; +static void schedule_work(struct work_struct *w) { w->scheduled++; } +static void mod_delayed_work(int q,struct delayed_work *w,unsigned delay) +{ (void)q; submissions++; w->pending=true; w->delay=delay; } +static void cancel_delayed_work_sync(struct delayed_work *w) +{ cancellations++; w->pending=false; } +""" + controls = r""" +int main(void) +{ + struct apple_dcp d={.typec_cable_connected=true,.typec_generation=7}; + struct apple_connector c={.dcp=&d}; d.connector=&c; d.dev=&d; + dcp_180hz_retry(&d); assert(!submissions); + d.rate_retry_initialized=true; + for(unsigned i=0;i<100;i++) dcp_180hz_retry(&d); + assert(submissions==8 && d.rate_proof.retries==8 && d.rate_retry_wq.delay==300); + dcp_180hz_retry_work(&d.rate_retry_wq.work); assert(c.hotplug_wq.scheduled==1); + d.typec_generation++; dcp_180hz_retry_work(&d.rate_retry_wq.work); + assert(c.hotplug_wq.scheduled==1); + d.typec_cable_connected=false; dcp_180hz_retry(&d); assert(submissions==8); + d.typec_cable_connected=true; dcp_180hz_retry(&d); assert(submissions==9 && d.rate_proof.retries==1); + d.quiescing=true; dcp_180hz_retry(&d); dcp_180hz_retry_work(&d.rate_retry_wq.work); + assert(submissions==9 && c.hotplug_wq.scheduled==1); + d.quiescing=false; c.dcp=NULL; dcp_180hz_retry_work(&d.rate_retry_wq.work); + assert(c.hotplug_wq.scheduled==1); + c.dcp=&d; dcp_180hz_retry_work(&d.rate_retry_wq.work); assert(c.hotplug_wq.scheduled==2); + dcp_180hz_cancel(&d); assert(!d.rate_retry_initialized && !d.rate_retry_wq.pending && cancellations==1); + dcp_180hz_retry(&d); assert(submissions==9); + puts("PASS: actual eight-retry bound, 300ms delay, stale-generation/disconnect/quiesce/ownership gates and cancellation"); +} +""" + return prefix + production + controls diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/recovery.py b/tools/testing/selftests/drivers/gpu/apple-dcp/recovery.py new file mode 100644 index 0000000000000..310af408b0f96 --- /dev/null +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/recovery.py @@ -0,0 +1,439 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +# Copyright 2026 Ryan Murray +"""Retained reviewer controls: actual AFK sender/receive, queue and EDID functions. + +The wrap controls deliberately retain an expected replay assertion failure. +They document an unresolved peer contract, not successful replay protection. +""" +from pathlib import Path +import json, hashlib, subprocess + +def check(root, output): + import edid + out = output / 'service-recovery-controls' + def extract(text,signature): + start=text.index(signature);opening=text.index('{',start) + while ';' in text[start:opening]: + start=text.index(signature,start+1);opening=text.index('{',start) + end,depth=opening+1,1 + while depth: + depth+=(text[end]=='{')-(text[end]=='}');end+=1 + return text[start:end] + + unit=edid.rebind(root) + # Retain the original exact fixture separately. Correct only its causal ordering. + corrected=unit.replace('assert(afk_service_get(old)==old);old->cmd_map[0]=1;', + 'assert(afk_service_get(old)==old);old->cmd_tag=1;old->cmd_map[0]=1;') + corrected=corrected.replace(' old->cmd_tag=1;\n','') + + sender=extract((root/'drivers/gpu/drm/apple/afk.c').read_text(), + 'static int afk_send_command_timeout_len(') + unit=unit[:unit.index('int main(void)')] + unit=unit.replace('static unsigned int dma_frees,opens;',r''' + static unsigned int dma_frees,opens; + #define SZ_1M (1U<<20) + #define GFP_KERNEL 0 + #define DECLARE_COMPLETION_ONSTACK(c) int c=0 + #define init_completion(c) (*(c)=0) + #define msecs_to_jiffies(x) (x) + #define cpu_to_le64(x) (x) + #define kmalloc(n,g) malloc(n) + #define EPIC_TYPE_COMMAND 3 + #define EPIC_CAT_COMMAND 3 + static int bitmap_find_free_region(unsigned long *p,int n,int order) { + (void)order;for(int i=0;iinline_v2) { + struct afkv2_command decoded;u8 response[40]={0}; + assert(!afkv2_decode_command(p,n,&decoded) && !decoded.oob && decoded.value==32 && decoded.id==tag); + response[1]=decoded.id; + assert(!afkv2_encode_frame(current_reply,sizeof(current_reply),1,channel,0,subtype,AFKV2_RESPONSE,0,response,sizeof(response),¤t_size)); + current_channel=channel;current_tag=tag;return 0; + } + assert(n==sizeof(struct epic_cmd)); + const struct epic_cmd *cmd=p;u8 oob[32]; + assert(!afkv2_encode_oob_command(oob,sizeof(oob),tag,cmd->rxbuf,cmd->rxlen,cmd->txbuf,cmd->txlen)); + afkv2_put32(oob+4,0); + assert(!afkv2_encode_frame(current_reply,sizeof(current_reply),1,channel,0,subtype,AFKV2_RESPONSE,0,oob,sizeof(oob),¤t_size)); + current_channel=channel;current_tag=tag;return 0; + } + '''+marker) + + tests=r''' + static void setup(void) { + d.hw.iomfb_method_profile=2;d.typec_cable_connected=true;d.typec_generation=1; + d.nr_modes=1;d.connector=&con;d.dev=&d; + con.dcp=&d;con.connected=true;con.base.registration_state=1; + ep.dcp=&d;ep.wire_v2=true;ep.endpoint=0x2a;ep.wq=&ep;ep.command_tags=calloc(AFKV2_INTERFACES,1);assert(ep.command_tags); + d.dcpavservep=&ep;ep.ops=announcement_ops;ep.match_epic_name=true; + d.fw_compat=DCP_FIRMWARE_H17P;hpd_lock=&d.hpd_mutex;av_lock=&d.dcpavserv.lock;active_ep=&ep; + } + static int send_actual(struct apple_epic_service *s,bool ack,bool late) { + u8 payload[32]={0},output[32]={0};u32 code; + ack_current=ack;inject_late=late; + int result=afk_send_command_timeout_len(s,0xc0,payload,32,output,32,&code,1,NULL); + inject_late=false;return result; + } + static void save_ack(void) { memcpy(saved_reply,current_reply,current_size);saved_size=current_size; } + static void cleanup(void) { + dpavservep_detach(&d);run_retries(); + for(unsigned int i=0;ichannel==5); + assert(send_actual(middle,true,true)==0); + closed(5,0x6a01,0);announce(13,name,native_class,13);run_retries(); + assert(d.dcpavserv.service && d.dcpavserv.service==middle && d.dcpavserv.service!=old); + assert(send_actual(d.dcpavserv.service,true,true)==0); + afk_service_put(old); + } else if(!strcmp(argv[1],"multiple-first-late-acks")) { + reuse_dma=false;assert(afk_service_get(old)==old); + assert(send_actual(old,false,false)==-ETIMEDOUT);save_ack(); + u8 first[56];size_t first_size=saved_size;memcpy(first,saved_reply,first_size); + assert(send_actual(old,false,false)==-ETIMEDOUT);save_ack(); + assert(old->cmd_map[0]==3); + closed(13,0x6a01,0);announce(13,name,native_class,13);run_retries();assert(!d.dcpavserv.service); + afk_v2_receive(&ep,0,0,first,first_size);run_retries(); + assert(!d.dcpavserv.service && old->cmd_map[0]==2 && old->users==1); + unsigned int freed=dma_frees; + afk_v2_receive(&ep,0,0,first,first_size);run_retries(); + assert(dma_frees==freed && old->cmd_map[0]==2 && !d.dcpavserv.service); + afk_v2_receive(&ep,0,0,saved_reply,saved_size);run_retries(); + assert(d.dcpavserv.service && d.dcpavserv.service!=old && !old->cmd_map[0]); + afk_service_put(old); + } else if(!strcmp(argv[1],"tag-wrap") || !strcmp(argv[1],"inline-tag-wrap")) { + assert(send_actual(old,true,false)==0);save_ack(); + for(unsigned int i=0;i<15;i++) assert(send_actual(old,true,true)==0); + assert(send_actual(old,true,true)==0); + } else if(!strcmp(argv[1],"pending-closed")) { + assert(send_actual(old,false,false)==-ETIMEDOUT);save_ack(); + closed(13,0x6a01,0);announce(13,name,native_class,13);run_retries(); + assert(!d.dcpavserv.service); + closed(13,0x6a01,0); + afk_v2_receive(&ep,0,0,saved_reply,saved_size);run_retries(); + assert(!d.dcpavserv.service && "CLOSED during pending recovery must invalidate the pending OPEN"); + } else if(!strcmp(argv[1],"actual-tag-before-announcement")) { + assert(send_actual(old,false,false)==-ETIMEDOUT);save_ack();assert(ep.command_tags[13]==1); + closed(13,0x6a01,0);announce(13,name,native_class,13); + afk_v2_receive(&ep,0,0,saved_reply,saved_size);run_retries(); + assert(d.dcpavserv.service && send_actual(d.dcpavserv.service,true,true)==0); + } else if(!strncmp(argv[1],"open-race-",10)) { + assert(send_actual(old,false,false)==-ETIMEDOUT);save_ack(); + closed(13,0x6a01,0);announce(13,name,native_class,13);run_retries(); + if(!strcmp(argv[1],"open-race-generation")) open_action=1; + else if(!strcmp(argv[1],"open-race-epoch")) open_action=2; + else if(!strcmp(argv[1],"open-race-owner")) open_action=4; + else if(!strcmp(argv[1],"open-race-cable")) open_action=5; + else if(!strcmp(argv[1],"open-race-quiescing")) open_action=6; + else if(!strcmp(argv[1],"open-race-absence")) open_action=7; + else assert(!"unknown OPEN race"); + afk_v2_receive(&ep,0,0,saved_reply,saved_size);run_retries(); + if(open_action==4) assert(d.dcpavserv.service==injected_owner); + else assert(!d.dcpavserv.service); + open_action=0;d.quiescing=false;d.typec_cable_connected=true;d.external_physically_absent=false; + } else if(!strcmp(argv[1],"physical-absence")) { + assert(send_actual(old,false,false)==-ETIMEDOUT);save_ack(); + closed(13,0x6a01,0);announce(13,name,native_class,13);run_retries(); + d.external_physically_absent=true; + afk_v2_receive(&ep,0,0,saved_reply,saved_size);run_retries(); + assert(!d.dcpavserv.service && "physical absence authority must fence deferred OPEN before cable/generation update"); + } else if(!strncmp(argv[1],"fence-",6)) { + assert(send_actual(old,false,false)==-ETIMEDOUT);save_ack(); + closed(13,0x6a01,0);announce(13,name,native_class,13);run_retries(); + unsigned int before=opens; + if(!strcmp(argv[1],"fence-epoch")) dpavservep_detach(&d); + else if(!strcmp(argv[1],"fence-generation")) d.typec_generation++; + else if(!strcmp(argv[1],"fence-cable")) d.typec_cable_connected=false; + else if(!strcmp(argv[1],"fence-quiescing")) d.quiescing=true; + else if(!strcmp(argv[1],"fence-owner")) inject_owner(&ep,13); + else if(!strcmp(argv[1],"fence-open-failure")) open_error=-EIO; + else assert(!"unknown fence"); + afk_v2_receive(&ep,0,0,saved_reply,saved_size);run_retries(); + assert(opens==before+(!strcmp(argv[1],"fence-open-failure"))); + if(!strcmp(argv[1],"fence-owner")) assert(d.dcpavserv.service==injected_owner); + else assert(!d.dcpavserv.service); + for(unsigned int i=0;ifn=(f);(w)->pending=false;(w)->next=NULL; } while(0) + static struct work_struct *fifo_head,*fifo_tail;static bool destroyed,rx_running; + ''') + queued=queued.replace('static bool queue_work(void *wq,struct work_struct *w) { assert(wq);bool fresh=!w->pending;w->pending=true;return fresh; }',r''' + static bool queue_work(void *wq,struct work_struct *w) { + assert(wq && !destroyed);if(w->pending) return false; + w->pending=true;w->next=NULL;if(fifo_tail) fifo_tail->next=w;else fifo_head=w;fifo_tail=w;return true; + } + static void run_one_work(void) { + assert(fifo_head);struct work_struct *w=fifo_head;fifo_head=w->next;if(!fifo_head) fifo_tail=NULL; + w->pending=false;w->next=NULL;assert(w->fn);w->fn(w); + } + static void destroy_workqueue(void *wq) { assert(wq && !destroyed);while(fifo_head) run_one_work();destroyed=true; } + #define IS_ERR_OR_NULL(p) (!(p) || IS_ERR(p)) + #define debugfs_remove_recursive(p) ((void)(p)) + #define kzalloc(n,g) calloc(1,n) + #define kvfree(p) free(p) + #define ESHUTDOWN 108 + #define RBEP_TYPE 0xffff + #define FIELD_GET(mask,val) ((val)&(mask)) + #define RBEP_INIT_ACK 0xa0 + #define RBEP_START_ACK 0x86 + #define RBEP_SHUTDOWN_ACK 0xc1 + #define RBEP_GETBUF 0x89 + #define RBEP_INIT_TX 0x8a + #define RBEP_INIT_RX 0x8b + #define RBEP_RECV 0x85 + #define complete_all(p) complete(p) + ''') + queued=queued.replace('struct apple_dcp_afkep { u8 *command_tags;bool lock,stopping;', + 'struct apple_dcp_afkep { u8 *command_tags;int started,stopped,txbfr,rxbfr;void *debugfs_entry;bool lock,stopping;') + queued=queued.replace('bool external_physically_absent,hpd_mutex,quiescing,typec_cable_connected;', + 'bool retain_dma,external_physically_absent,hpd_mutex,quiescing,typec_cable_connected;') + queued=queued.replace('static void run_retries(void) { assert(!ep.lock);if(ep.service_work.pending) { ep.service_work.pending=false;afk_service_worker(&ep.service_work); } }', + 'static void run_retries(void) { assert(!ep.lock);while(fifo_head) run_one_work(); }') + queued=queued.replace(' if(type==EPIC_TYPE_COMMAND) {', + ' if(e->stopping) return -ESHUTDOWN;\n if(type==EPIC_TYPE_COMMAND) {') + queued=queued[:queued.index('int main(int argc')] + queued+=r''' + struct afk_receive_message_work { struct apple_dcp_afkep *ep;u64 message;struct work_struct work; }; + static void afk_getbuf(struct apple_dcp_afkep *e,u64 m) { (void)e;(void)m;assert(!"unexpected GETBUF"); } + static void afk_init_rxtx(struct apple_dcp_afkep *e,u64 m,int *r) { (void)e;(void)m;(void)r;assert(!"unexpected INIT"); } + static unsigned int rx_step;static bool close_pending; + static bool afk_recv(struct apple_dcp_afkep *e) { + assert(e==&ep);rx_running=true; + if(rx_step==0) closed(13,0x6a01,0); + else if(rx_step==1) announce(13,name,native_class,13); + else if(rx_step==2) afk_v2_receive(e,0,0,saved_reply,saved_size); + else if(rx_step==3 && close_pending) closed(13,0x6a01,0); + else { assert(!d.dcpavserv.service && "recovery must wait for the RX drain loop to finish");rx_running=false;return false; } + assert(!d.dcpavserv.service && "recovery must remain behind RX on the ordered queue");rx_step++;return true; + } + ''' + af=(root/'drivers/gpu/drm/apple/afk.c').read_text() + for signature in ['static void afk_receive_message_worker(', 'int afk_receive_message(', 'void afk_quiesce(', 'static void afk_release_context(']: + queued+=extract(af,signature)+'\n' + queued+=r''' + int main(int argc,char **argv) { + assert(argc==2);setup();reuse_dma=true;INIT_WORK(&ep.service_work,afk_service_worker); + announce(13,name,native_class,13);struct apple_epic_service *old=d.dcpavserv.service;assert(old); + assert(send_actual(old,false,false)==-ETIMEDOUT);save_ack();run_retries(); + unsigned int before=opens; + if(!strncmp(argv[1],"ordered-",8)) { + close_pending=!strcmp(argv[1],"ordered-pending-closed"); + assert(!afk_receive_message(&ep,RBEP_RECV));assert(fifo_head); + run_one_work();assert(!rx_running && opens==before && !d.dcpavserv.service); + assert(fifo_head==&ep.service_work);run_retries(); + if(close_pending) assert(!d.dcpavserv.service && "ordered CLOSED must cancel pending OPEN"); + else { assert(d.dcpavserv.service && opens==before+1);assert(send_actual(d.dcpavserv.service,true,true)==0); } + cleanup(); + } else { + assert(!strcmp(argv[1],"quiesce-pending"));assert(afk_service_get(old)==old); + closed(13,0x6a01,0);announce(13,name,native_class,13); + afk_v2_receive(&ep,0,0,saved_reply,saved_size);assert(ep.service_work.pending); + afk_quiesce(&ep);assert(destroyed && !ep.wq && ep.stopping && !fifo_head); + assert(opens==before && !d.dcpavserv.service && old->users==1); + for(unsigned int i=0;iusers); + assert(afk_receive_message(&ep,RBEP_RECV)==-ESHUTDOWN);assert(!fifo_head); + afk_quiesce(&ep);assert(!fifo_head); + struct apple_dcp_afkep *empty=calloc(1,sizeof(*empty));assert(empty);empty->dcp=&d; + afk_release_context(empty); + cleanup(); + } + printf("PASS actual RX worker/quiesce: %s\n",argv[1]); + } + ''' + run('actual-queue-quiesce',queued,[[c] for c in ['ordered-recovery','ordered-pending-closed','quiesce-pending']]) + + # Actual EDID wrapper exercises its owner/reader pins, epoch and generation exits. + reader=unit[:unit.index('int main(int argc')] + reader+=r''' + #define ESTALE 116 + struct drm_edid { int marker; }; + static unsigned int edid_allocations,edid_frees;static int read_action; + struct reader_guard { bool *lock;bool once; }; + static struct reader_guard lock_reader_guard(bool *lock) { mutex_lock(lock);return (struct reader_guard){lock,true}; } + static void unlock_reader_guard(struct reader_guard *guard) { mutex_unlock(guard->lock); } + #define scoped_guard(kind,p) for(struct reader_guard _guard __attribute__((cleanup(unlock_reader_guard)))=lock_reader_guard(p);_guard.once;_guard.once=false) + static void drm_edid_free(const struct drm_edid *edid) { assert(edid && !IS_ERR(edid));free((void *)edid);edid_frees++; } + static const struct drm_edid *dcpavserv_read_edid_inline(struct apple_epic_service *service) { + assert(service->users==2 && "owner plus actual EDID wrapper reader pin"); + assert(send_actual(service,true,false)==0); + if(read_action==1) { closed(service->channel,0x6a01,0);announce(5,name,class,5);assert(d.dcpavserv.service!=service); } + if(read_action==2) d.typec_generation++; + if(read_action==3) d.external_physically_absent=true; + if(read_action==4) { dpavservep_detach(&d);d.quiescing=true; } + struct drm_edid *edid=malloc(sizeof(*edid));assert(edid);edid->marker=1;edid_allocations++;return edid; + } + static const struct drm_edid *dcpavserv_read_edid_legacy(struct apple_epic_service *service) { return dcpavserv_read_edid_inline(service); } + ''' + dpav=(root/'drivers/gpu/drm/apple/epic/dpavservep.c').read_text() + for signature in ['bool dcpavserv_edid_current(', 'bool dcpavserv_edid_service_current(', 'const struct drm_edid *dcpavserv_copy_edid(']: + reader+=extract(dpav,signature)+'\n' + reader+=r''' + int main(int argc,char **argv) { + assert(argc==2);setup();reuse_dma=true;announce(13,name,class,13); + struct apple_epic_service *old=d.dcpavserv.service;assert(old); + if(!strcmp(argv[1],"reader-owner")) read_action=1; + else if(!strcmp(argv[1],"reader-generation")) read_action=2; + else if(!strcmp(argv[1],"reader-physical-absence")) read_action=3; + else if(!strcmp(argv[1],"reader-quiescing")) read_action=4; + else assert(!strcmp(argv[1],"reader-success")); + u64 epoch;const struct drm_edid *edid=dcpavserv_copy_edid(&d,1,&epoch); + if(!read_action) { assert(!IS_ERR(edid));drm_edid_free(edid); } + else { assert(IS_ERR(edid) && "actual EDID wrapper must reject invalidated ownership/physical state"); } + if(read_action==1 || read_action==4) assert(!old->users);else assert(old->users==1); + assert(edid_allocations==edid_frees && !d.hpd_mutex && !d.dcpavserv.lock); + d.quiescing=false;cleanup();printf("PASS actual copy_edid: %s\n",argv[1]); + } + ''' + run('actual-edid-reader',reader,[[c] for c in ['reader-success','reader-owner','reader-generation','reader-quiescing','reader-physical-absence']]) + mutations=[ + ('restart-tags','afkv2_command_id(ep->command_tags[service->channel]++, slot)','afkv2_command_id(0, slot)'), + ('no-drain-retry','\tafk_service_retry(service->ep);',''), + ('ignore-old-command','\t\tif (afk_epic_find_service(ep, service->channel))\n\t\t\tcontinue;',''), + ('ignore-epoch','service->admission.epoch != dcp->dcpavserv.epoch ||\n\t\t ','false ||\n\t\t '), + ('refresh-generation','!dcpavserv_admission_current(dcp, service->admission.generation))\n\t\tret = -ENODEV;','!dcpavserv_admission_current(dcp, dcp->typec_generation))\n\t\tret = -ENODEV;'), + ('ignore-owner','if (dcp->dcpavserv.enabled)\n\t\tret = -EBUSY;\n\telse if (service->admission.epoch','if (false)\n\t\tret = -EBUSY;\n\telse if (service->admission.epoch'), + ('ignore-stop','if (!READ_ONCE(ep->stopping))\n\t\tafk_retry_service_init(ep);','if (true)\n\t\tafk_retry_service_init(ep);'), + ] + + for name,text,case in [ + ('cancel-closed',unit.replace('if (pending->pending_open && pending->channel == channel)','if (false && pending->pending_open && pending->channel == channel)'), 'pending-closed'), + ('reader-lookup',unit.replace('(service->enabled ||','(service->users || service->enabled ||'), 'drained-reader-rebind'), + ('reset-interface-history',unit.replace('service->cookie = NULL;', 'service->cookie = NULL; if(ep->wire_v2 && ep->command_tags) ep->command_tags[channel]=0;',1), 'channel-reuse'), + ('absence-admission',unit.replace('!READ_ONCE(dcp->external_physically_absent) &&','true &&',1), 'initial-absence'), + ('absence-publication',unit.replace('!READ_ONCE(dcp->external_physically_absent) &&','true &&',1), 'initial-open-absence'), + ('absence-reader',reader.replace('!READ_ONCE(dcp->external_physically_absent)', 'true'), 'reader-physical-absence'), + ]: + run('mutation-'+name,text,[[case]]) + + for name,old,new in mutations: + assert corrected.count(old)==1,(name,corrected.count(old)) + run('mutation-'+name,corrected.replace(old,new),[[]]) + negative={ + 'actual-sender':{'tag-wrap','inline-tag-wrap'}, + 'actual-queue-quiesce':set(), + 'actual-edid-reader':set(), + } + for result in results: + case=result.get('case',[]) + fail_expected=bool('case' in result and (result['name'].startswith('mutation-') or (case and case[0] in negative.get(result['name'],set())))) + result['assertion_failure_expected']=fail_expected + result['expectation_met']=result['exit']==(-6 if fail_expected else 0) + if fail_expected: result['expectation_met'] = result['expectation_met'] and 'Assertion' in result.get('output','') + if case and case[0] in {'tag-wrap','inline-tag-wrap'}: + result['expectation_met'] = result['expectation_met'] and 'late ACK completed an actual new production command' in result.get('output','') + (out/'results.json').write_text(json.dumps(results,indent=2)+'\n') + + assert all(r['expectation_met'] for r in results), "retained recovery control expectation failed; see results.json" diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/run.py b/tools/testing/selftests/drivers/gpu/apple-dcp/run.py new file mode 100644 index 0000000000000..e64cc11a1eda4 --- /dev/null +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/run.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +# Copyright 2026 Ryan Murray +"""Host regressions for Apple external display ownership and recovery. + +Only public synthetic fixtures and extracted production functions are used. +The transport, MMIO, locks and work queues are mocks, not hardware validation. +""" +import argparse +import copy +import hashlib +import json +import os +from pathlib import Path +import struct +import tempfile +import traceback + +from common import run_unit +import edid +import recovery +import lifecycle +import phy +import pm +import quiesce +import transaction +import accounting +import idle +import rate +import boot + + +def parking_unit(root): + fixture = phy.parking(root) + return ''.join(fixture[k] for k in ('prefix', 'production', 'suffix')) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--source', type=Path, default=Path(__file__).resolve().parents[6]) + parser.add_argument('--output', type=Path, help='retain generated C, commands and results here') + parser.add_argument('--group', choices=('all', 'edid', 'rate', 'phy', 'lifecycle', 'boot', 'quiesce'), default='all') + args = parser.parse_args() + root = args.source.resolve() + output = args.output or Path(tempfile.mkdtemp(prefix='apple-dcp-selftest-')) + output.mkdir(parents=True, exist_ok=True) + apple = root / 'drivers/gpu/drm/apple' + includes = (apple, root / 'tools/testing/selftests/drivers/gpu/apple-afkv2/host-include') + parking_cases = tuple((c,) for c in ('mux', 'power', 'reset', 'cleanup', 'orientation', + 'orientation-faults', 'backend-change', 'safe-first', + 'failed-flip-return', 'live-park')) + tests = [ + ('boot', 'oslog-events', boot.oslog, ((),), ()), + ('edid', 'service-rebind', edid.rebind, ((),), ()), + ('edid', 'provider-admission', edid.admission, ((),), ()), + ('edid', 'ordered-rx', edid.rx_progress, ((),), ('-pthread',)), + ('rate', 'parser-policy', lambda r: rate.policy(r)['unit'], ((),), ()), + ('rate', 'publication-hpd', rate.hotplug, ((),), ()), + ('rate', 'metadata-decoder', lambda r: rate.metadata(r, output), ((),), ('-Wno-sign-compare',)), + ('rate', 'bounded-retry', rate.retry, ((),), ()), + ('phy', 'pm-ownership', pm.ownership, ((),), ()), + ('phy', 'controller-lifecycle', pm.controller_lifecycle, ((),), ('-pthread',)), + ('phy', 'asynchronous-root-state', pm.asynchronous_root_state, ((),), ()), + ('phy', 'upstream-resume-boundary', pm.upstream_resume_boundary, ((),), ()), + ('phy', 'usb-unreachable', lambda r: pm.ownership(r, True), ((),), ()), + ('lifecycle', 'cpuidle-s2idle', idle.s2idle, ((),), ('-Wno-unused-parameter', '-Wno-unused-variable')), + ('lifecycle', 'typec-probe', pm.probe, ((),), ()), + ('lifecycle', 'quiesce-before-safe', quiesce.make_unit, tuple((c,) for c in ('ordered','power','ack','role','bolt','mux','duplicate','error-latched','legacy','fabric','tunnel')), ()), + ('lifecycle', 'probe-mux-pm-dispatch', pm.probe_dispatch, ((),), ()), + ('phy', 'parking', lambda r: parking_unit(r), parking_cases, ()), + ('phy', 'adverse-retry', phy.adverse, tuple((c,) for c in ('bist', 'init', 'mux', 'gate', 'dp-cache', 'off', 'success')), ()), + ('phy', 'masked-context', phy.usb_context, ((),), ()), + ('phy', 'off-hardening', phy.hardening, ((),), ()), + ('phy', 'warm-reset', phy.warm_reset, ((),), ()), + ('phy', 'pipe', phy.pipe, tuple((c,) for c in ('backend', 'host', 'mux', 'same', 'park', 'requested', 'scope')), ()), + ('phy', 'unwind', phy.unwind, ((),), ()), + ('phy', 'aux', phy.aux, ((),), ()), + ('lifecycle', 'physical-disconnect', lifecycle.disconnect, ((),), + ('-Wno-unused-variable', '-Wno-unused-function', '-Wno-unused-but-set-variable')), + ('lifecycle', 'dwc3-teardown', lifecycle.teardown, (('all',),), ()), + ('lifecycle', 'resource-profile', lifecycle.profile, ((),), ('-Wno-sign-compare',)), + ('lifecycle', 'firmware-provider', lifecycle.firmware, ((),), ()), + ('lifecycle', 'readiness', lifecycle.readiness, ((),), ()), + ('lifecycle', 'vrr-rejection', lifecycle.vrr, ((),), ()), + ('lifecycle', 'power-reply', lifecycle.power_reply, ((),), ()), + ('lifecycle', 'dispatcher-scope', lifecycle.dispatcher, ((),), ()), + ] + transaction_tests = transaction.controls(root) + accounting.controls(root) + tests.extend(('lifecycle', name, lambda r, u=unit: u, cases, flags) + for name, unit, cases, flags in transaction_tests) + tests.append(('phy', 'usb-kconfig', lambda r: pm.kconfig(r, output), (), ())) + tests = [t for t in tests if args.group in ('all', t[0]) or (args.group == 'quiesce' and t[1] in ('quiesce-before-safe',) + tuple(n for n, _, _, _ in transaction_tests))] + print('TAP version 13', flush=True) + print('1..' + str(len(tests)), flush=True) + results = [] + for index, (group, name, build, cases, flags) in enumerate(tests, 1): + result = {'name': name, 'group': group, 'passed': False} + try: + unit = build(root) + if unit is None: + result['producer_passed'] = True + elif isinstance(unit, tuple): + unit, cases = unit + if unit is not None: + result['translation_unit_sha256'] = run_unit(name, unit, output, cases, includes if name == 'service-rebind' else (), flags) + if name == 'service-rebind': + recovery.check(root, output) + if name == 'metadata-decoder': + rate.check_metadata(output) + result['passed'] = True + except Exception: + result['failure'] = traceback.format_exc() + (output / (name + '-failure.log')).write_text(result['failure']) + print(('ok ' if result['passed'] else 'not ok ') + str(index) + ' - ' + name, flush=True) + results.append(result) + sources = {} + for area in ('drivers/gpu/drm/apple', 'drivers/phy/apple', 'drivers/usb/dwc3', 'drivers/usb/typec/tipd'): + for path in sorted((root / area).rglob('*')): + if path.is_file() and path.suffix in ('.c', '.h'): + sources[str(path.relative_to(root))] = hashlib.sha256(path.read_bytes()).hexdigest() + (output / 'results.json').write_text(json.dumps({'source': str(root), 'sources': sources, 'results': results}, indent=2) + '\n') + print('# Artifacts: ' + str(output), flush=True) + return int(any(not r['passed'] for r in results)) + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/run.sh b/tools/testing/selftests/drivers/gpu/apple-dcp/run.sh new file mode 100755 index 0000000000000..f4052ba6632f1 --- /dev/null +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/run.sh @@ -0,0 +1,4 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0-only OR MIT +PYTHONDONTWRITEBYTECODE=1; export PYTHONDONTWRITEBYTECODE +exec python3 "$(dirname "$0")/run.py" "$@" diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/teardown_pm.py b/tools/testing/selftests/drivers/gpu/apple-dcp/teardown_pm.py new file mode 100644 index 0000000000000..1f17261136a9c --- /dev/null +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/teardown_pm.py @@ -0,0 +1,156 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright 2026 Ryan Murray +"""Actual wrapper/driver-core/DWC3/xHCI teardown order with modeled scheduling. + +Device mutex APIs use pthread mutexes. Clock, unregister, PM and hardware leaves +are mocks; this is not lockdep, system-PM or electrical atomicity qualification. +""" +import pm +from common import block + +def unit_for(source): + unit=pm.ownership(source) + unit=unit.replace('int main(void)','int __attribute__((unused)) ownership_main(void)',1) + unit=unit.replace('struct device_driver { const char *name; };', 'struct device;\nstruct device_driver { const char *name; void (*remove)(struct device *); void *dev_groups; };') + unit=unit.replace('struct device { int refs;', 'struct bus_type { bool need_parent_lock; void (*remove)(struct device *); void (*dma_cleanup)(struct device *); };\nstruct device_private { int knode_driver; };\nstruct device { struct bus_type *bus; struct device_private *p; int kobj; int refs;') + unit=unit.replace('static struct device host_device;', 'static struct platform_device host_platform_device;\n#define host_device host_platform_device.dev') + unit=unit.replace('static bool controller_locked, host_locked;', 'static _Atomic bool controller_locked, host_locked;') + pre=r''' +#include +#include +static pthread_mutex_t controller_mutex=PTHREAD_MUTEX_INITIALIZER, host_mutex=PTHREAD_MUTEX_INITIALIZER; +static pthread_mutex_t schedule_mutex=PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t schedule_cond=PTHREAD_COND_INITIALIZER; +static _Thread_local bool remover; +static atomic_bool handoff_active, early_write; +static bool removal_started, blocked_attempt, release_remove, inject_remove; +static int remove_target, susphy_calls, host_remove_calls; +static pthread_t remove_thread; +static void start_remove(void); +static void removal_lock_attempt(bool held) { + if(!remover||!held)return; + pthread_mutex_lock(&schedule_mutex);blocked_attempt=true; + pthread_cond_broadcast(&schedule_cond);pthread_mutex_unlock(&schedule_mutex); +} +static void observed_controller_write(void) { + if(atomic_load(&handoff_active))atomic_store(&early_write,true); + pthread_mutex_lock(&schedule_mutex); + removal_started=true;pthread_cond_broadcast(&schedule_cond); + while(atomic_load(&handoff_active)&&!release_remove)pthread_cond_wait(&schedule_cond,&schedule_mutex); + pthread_mutex_unlock(&schedule_mutex); +} +''' + unit=unit.replace('#define CONFIG_USB 1',pre+'\n#define CONFIG_USB 1',1) + old=block(unit,'static void device_lock(') + new=r'''static void device_lock(struct device *d) { + if(d==&controller.dev){ + removal_lock_attempt(atomic_load(&controller_locked)); + pthread_mutex_lock(&controller_mutex);controller_locked=true; + }else{ + assert(d==&host_device);removal_lock_attempt(atomic_load(&host_locked)); + pthread_mutex_lock(&host_mutex);host_locked=true; + } +}''' + unit=unit.replace(old,new) + old=block(unit,'static void device_unlock(') + new=r'''static void device_unlock(struct device *d) { + if(d==&host_device){assert(host_locked);host_locked=false;pthread_mutex_unlock(&host_mutex);} + else {assert(d==&controller.dev&&controller_locked);if(!remover)atomic_store(&handoff_active,false);controller_locked=false;pthread_mutex_unlock(&controller_mutex);} +}''' + unit=unit.replace(old,new) + # The scheduler injection is a child-lookup interleaving, as in the review. + unit=unit.replace('if(detach_at_lookup)usb_set_device_state(u,USB_STATE_NOTATTACHED);','if(inject_remove){inject_remove=false;start_remove();}\n if(detach_at_lookup)usb_set_device_state(u,USB_STATE_NOTATTACHED);',1) + unit=unit.replace('static void pm_runtime_put_noidle(struct device *d) {','static void pm_runtime_put_noidle(struct device *d) {\n if(remover&&d==&host_device)return;',1) + # Old source lacks platform-lock helpers. Keep fixture-only API definitions + # available without claiming they are invoked by the parent. + if 'device_lock(&pm->controller->dev)' not in (source/'drivers/phy/apple/atc.c').read_text(): + unit=unit.replace('static struct device *get_device(', 'static __attribute__((unused)) struct device *get_device(') + prefix=r''' +struct clk { int unused; }; struct reset_control { int unused; }; +struct xhci_hcd { atomic_int xhc_state; struct clk *clk,*reg_clk; struct reset_control *reset; struct usb_hcd *shared_hcd; }; +static struct xhci_hcd xhci_context; +#define XHCI_STATE_REMOVING 1 +#define hcd_to_xhci(h) (&xhci_context) +struct dwc3 { bool sys_wakeup; struct platform_device *xhci; }; +enum { DWC3_APPLE_PROBE_PENDING, DWC3_APPLE_NO_CABLE, DWC3_APPLE_HOST, DWC3_APPLE_DEVICE }; +struct dwc3_apple { struct dwc3 dwc; struct device *dev; int lock,state; void *role_sw; }; +static struct dwc3_apple apple_context; +#define to_dwc3_apple(d) container_of(d,struct dwc3_apple,dwc) +static bool inside_host_remove; +static void *platform_get_drvdata(struct platform_device *d) { return d==&controller?(void *)&apple_context.dwc:(void *)&hcd; } +static void dwc3_enable_susphy(struct dwc3 *d,bool v) { (void)d;assert(v);susphy_calls++;observed_controller_write(); } +static void device_init_wakeup(struct device *d,bool b) { (void)d;(void)b; } +static void platform_device_unregister(struct platform_device *d); +static void dwc3_gadget_exit(struct dwc3 *d) { (void)d;assert(0); } +static int dwc3_apple_core_stop(struct dwc3_apple *d) { (void)d;return 0; } +static void usb_role_switch_unregister(void *s) { (void)s; } +static void dwc3_core_remove_no_shutdown(struct dwc3 *d) { (void)d; } +static void usb_remove_hcd(struct usb_hcd *h) { host_remove_calls++;h->state=HC_STATE_QUIESCING;usb_set_device_state(h->self.root_hub,USB_STATE_NOTATTACHED); } +static void usb_phy_shutdown(void *p) { (void)p; } +static void usb_put_hcd(struct usb_hcd *h) { (void)h; } +static void clk_disable_unprepare(struct clk *c) { (void)c; } +static void reset_control_assert(struct reset_control *r) { (void)r; } +static void pm_runtime_disable(struct device *d) { (void)d; } +static void pm_runtime_set_suspended(struct device *d) { (void)d; } +static int pm_runtime_get_sync(struct device *d) { if(inside_host_remove&&d==&host_device){assert(xhci_context.xhc_state&XHCI_STATE_REMOVING);observed_controller_write();}return 0; } +static void pm_runtime_put_sync(struct device *d) { (void)d; } +static void pm_runtime_put(struct device *d) { (void)d; } +#define dev_attr_state_synced 0 +#define BUS_NOTIFY_UNBIND_DRIVER 0 +#define BUS_NOTIFY_UNBOUND_DRIVER 1 +#define KOBJ_UNBIND 0 +static bool device_links_busy(struct device *d) { (void)d;return false; } +static void device_links_unbind_consumers(struct device *d) { (void)d;assert(0); } +static void driver_sysfs_remove(struct device *d) { (void)d; } +static void bus_notify(struct device *d,int n) { (void)d;(void)n; } +static void device_remove_file(struct device *d,void *a) { (void)d;(void)a; } +static void device_remove_groups(struct device *d,void *a) { (void)d;(void)a; } +static void device_unbind_cleanup(struct device *d) { d->driver=NULL; } +static void device_links_driver_cleanup(struct device *d) { (void)d; } +static void klist_remove(void *k) { (void)k; } +static void device_pm_check_callbacks(struct device *d) { (void)d; } +static void kobject_uevent(void *k,int n) { (void)k;(void)n; } +''' + prefix=prefix.replace('#define dev_attr_state_synced 0','static int dev_attr_state_synced;') + unit=unit.replace('struct usb_hcd { int state;', 'struct usb_hcd { void *usb_phy; int state;',1) + # Extract actual callback order; clock/USB/reset leaves remain mocks. + host=(source/'drivers/usb/dwc3/host.c').read_text() + glue=(source/'drivers/usb/dwc3/dwc3-apple.c').read_text() + plat=(source/'drivers/usb/host/xhci-plat.c').read_text() + dd=(source/'drivers/base/dd.c').read_text() + functions=block(host,'void dwc3_host_exit(')+'\n'+block(glue,'static int dwc3_apple_exit(')+'\n'+block(glue,'static void dwc3_apple_remove(')+'\n'+block(plat,'void xhci_plat_remove(')+'\n' + functions+='\n'.join(block(dd,x) for x in ['static void __device_driver_lock(', 'static void __device_driver_unlock(', 'static void device_remove(', 'static void __device_release_driver(', 'void device_release_driver_internal(']) + controls=r''' +static void controller_remove(struct device *d) { assert(d==&controller.dev);dwc3_apple_remove(&controller); } +static void host_remove(struct device *d) { assert(d==&host_device);inside_host_remove=true;xhci_plat_remove(&host_platform_device);inside_host_remove=false; } +static void platform_device_unregister(struct platform_device *d) { assert(d==&host_platform_device);device_release_driver_internal(&d->dev,NULL,NULL); } +static void *run_remove(void *unused) { (void)unused;remover=true;device_release_driver_internal(remove_target?&host_device:&controller.dev,NULL,NULL);return NULL; } +static void start_remove(void) { + pthread_mutex_lock(&schedule_mutex); + assert(pthread_create(&remove_thread,NULL,run_remove,NULL)==0); + while(!blocked_attempt&&!removal_started)pthread_cond_wait(&schedule_cond,&schedule_mutex); + pthread_mutex_unlock(&schedule_mutex); +} +int main(void) { + for(remove_target=0;remove_target<2;remove_target++) { + struct apple_atcphy p;prepare(&p,true,true,false); + struct device_private private={0};controller.dev.p=&private;host_device.p=&private; + controller_driver.remove=controller_remove;host_driver.remove=host_remove; + apple_context=(struct dwc3_apple){.dwc={.xhci=&host_platform_device},.dev=&controller.dev,.state=DWC3_APPLE_HOST}; + xhci_context=(struct xhci_hcd){0}; + inject_remove=true;removal_started=blocked_attempt=release_remove=false; + atomic_store(&early_write,false);atomic_store(&handoff_active,true); + susphy_calls=host_remove_calls=0; + int ret=safe(&p); + atomic_store(&handoff_active,false); + pthread_mutex_lock(&schedule_mutex);release_remove=true;pthread_cond_broadcast(&schedule_cond);pthread_mutex_unlock(&schedule_mutex); + assert(pthread_join(remove_thread,NULL)==0); + fprintf(stderr,"connected teardown target=%d ret=%d early_write=%d susphy=%d host_remove=%d\n",remove_target,ret,atomic_load(&early_write),susphy_calls,host_remove_calls); + assert(ret==0&&!atomic_load(&early_write)&&host_remove_calls==1); + assert(susphy_calls==(remove_target?0:1));balanced(&p); + } + puts("PASS actual PM wrapper connected to driver-core/DWC3/xHCI removal; synchronous teardown waits until owned transaction ends"); + return 0; +} +''' + return unit+prefix+functions+controls diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/transaction.py b/tools/testing/selftests/drivers/gpu/apple-dcp/transaction.py new file mode 100644 index 0000000000000..0ec0cc21c26c0 --- /dev/null +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/transaction.py @@ -0,0 +1,406 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright 2026 Ryan Murray +"""Exact production callbacks with the adverse-review counterexample assertions. + +Firmware, MMIO and USB role operations are synthetic leaves. The concurrency +case executes production transactions with real pthread connector/fabric locks. +""" +import quiesce +from common import block + + +def controls(ROOT): + tests = [] + def check(name, unit, cases=((),), negative=False, flags=()): + assert not negative + tests.append((name, unit, cases, flags)) + base=quiesce.make_unit(ROOT) + # Actual generic role helper, omitted by the author's role-leaf fixture. + core=(ROOT/'drivers/usb/typec/tipd/core.c').read_text() + generic=base.replace(block(base,'int main('),block(base,'int main(').replace('int main(','int __attribute__((unused)) prior_main(',1),1) + generic+=r''' + int main(void) { + struct cd321x c;initial(&c);data.remove=NULL;role_error=-EBUSY; + tipd_remove(&c.tps); + fprintf(stderr,"generic role failure: role_calls=%d error=%d reset=%d gpio=%d events=%s\n",role_calls,last_error,reset_calls,gpio_calls,events); + assert(last_error==-EBUSY&&reset_calls==0&&gpio_calls==0&&unregisters==1); + } + ''' + check('generic-role-error-lost',generic) + second_role=generic.replace('data.remove=NULL;role_error=-EBUSY;', 'role_error=-EBUSY;',1) + second_role=second_role.replace("role_calls++;return role_error;", "role_calls++;return role_calls==1?0:role_error;",1) + check('cd-second-role-error-lost',second_role) + + # Replace the modeled DCP set/deactivate bodies with exact production bodies. + dcp=(ROOT/'drivers/gpu/drm/apple/dcp.c').read_text() + actual=base.replace(block(base,'int main('),block(base,'int main(').replace('int main(','int __attribute__((unused)) prior_main(',1),1) + actual=actual.replace('struct typec_mux_state { void *alt;', 'struct typec_altmode { unsigned svid; };\nstruct typec_mux_state { struct typec_altmode *alt;',1) + actual=actual.replace('struct connector { bool connected; };','struct encoder { unsigned possible_crtcs; };\nstruct connector { bool connected; void *dcp; struct encoder *port_encoder; unsigned candidate_crtcs; };\n#define apple_connector connector\nstruct mux_control { int unused; };',1) + actual=actual.replace('struct apple_dcp { int external_lock,external_error;', '''struct apple_dcp { int external_lock,external_error; + bool external_power_attempted,dptx_tunnel,tb_clock_ok,fixed_route_selected; + int tb_lock; unsigned typec_generation,dptx_dfp_port,dptx_phy,fixed_dptx_phy,fixed_mux_index; + void *tb_dpin_set_active,*tb_dpin_ctx,*phy,*fixed_phy; + struct mux_control *xbar; + struct apple_dcp_typec_route *active_typec_route,*typec_routes; + struct connector *connector,*fixed_connector;int connector_type,fixed_connector_type; + ''',1) + actual=actual.replace('bool applied_valid,safe_pending,dp_wanted,dp_hpd,hpd;', 'struct typec_altmode *applied_alt; unsigned long applied_mode; unsigned applied_status,applied_conf; bool applied_valid,safe_pending,dp_wanted,dp_hpd,hpd;',1) + actual=actual.replace('void *xbar; bool deactivate_xbar_done; int deactivate_error; bool selected,tunnel;', 'struct mux_control *xbar,*active_xbar; void *phy; unsigned dptx_phy,tunnel_dpin,mux_index;bool deactivate_xbar_done; int deactivate_error;bool xbar_up,selected,tunnel;',1) + extra=r''' + #define WRITE_ONCE(x,v) ((x)=(v)) + #define USB_TYPEC_DP_SID 0xff01 + #define TYPEC_DP_STATE_A 10 + #define TYPEC_DP_STATE_F 15 + #define DP_STATUS_HPD_STATE 1 + #define DP_STATUS_IRQ_HPD 2 + #define DRM_MODE_CONNECTOR_USB 1 + #define MUX_IDLE_DISCONNECT -1 + #define symbol_get(fn) (&fn) + #define symbol_put(fn) ((void)0) + struct typec_displayport_data { u32 status,conf; }; + static int gpiod_get_value_cansleep(void *p); + static int dcp_dpxbar_preselect(struct mux_control *m,int s) { (void)m;(void)s;return 0; } + static int dcp_dpxbar_tunnel_select_source(struct mux_control *m,int s) { (void)m;(void)s;return 0; } + static int apple_atc_dp_tunnel_rate(void *p,unsigned n,unsigned rate) { (void)p;(void)n;(void)rate;return 0; } + static int xbar_error,xbar_calls; +static int mux_control_deselect(struct mux_control *m) { (void)m;event('C');xbar_calls++;return xbar_error; } + static int mux_control_select(struct mux_control *m,unsigned n) { (void)m;(void)n;return 0; } + static void dpavservep_detach(struct apple_dcp *d) { (void)d; } + static void dcp_firmware_metadata_connection(struct apple_dcp *d,unsigned n,bool c) { (void)d;(void)n;(void)c; } + static bool dcp_typec_keep_order(void) { return false; } + static struct apple_dcp_typec_route *dcp_typec_lowest_free(struct apple_dcp_typec_port *p) { (void)p;return &route; } + static int connect_calls; + static int dcp_dptx_connect_oob(void *dev,unsigned n) { (void)dev;(void)n;connect_calls++;display.external_cable=true;display.dptxport[0].connected=true;display.typec_cable_connected=true;connector.connected=true;return 0; } + static void dcp_retrain_oob(struct connector *c) { (void)c; } + ''' + pos=actual.index('static int dcp_typec_route_deactivate(') + actual=actual[:pos]+extra+actual[pos:] + actual=actual.replace(block(actual,'static bool dcp_typec_route_available('), 'static bool dcp_typec_route_fixed_output_busy(struct apple_dcp_typec_route *r) { (void)r;return false; }\n'+block(dcp,'static bool dcp_typec_route_available('),1) + actual=actual.replace(block(actual,'static int dcp_typec_route_deactivate('),block(dcp,dcp.rindex('static int dcp_typec_route_deactivate(')),1) + pos=actual.index('static struct typec_mux_dev phy_provider') + actual=actual[:pos]+block(dcp,'static bool dcp_typec_route_is_dp(')+'\n'+block(dcp,'static int dcp_typec_route_set(')+'\n'+actual[pos:] + actual=actual.replace(block(actual,'static int display_set('),'''static int display_set(struct typec_mux_dev *m,struct typec_mux_state *s) { event('S');return dcp_typec_route_set(m,s); }''',1) + actual=actual.replace("(void)m;assert(s->mode==TYPEC_STATE_SAFE);event('L');", "(void)m;(void)s;event('L');",1) + actual=actual.replace('connector=(struct connector){.connected=true};','connector=(struct connector){.connected=true};',1) + actual=actual.replace('phy_provider=(struct typec_mux_dev){.set=phy_set};','display.typec_routes=&route;display.fixed_connector_type=DRM_MODE_CONNECTOR_USB;display.active_typec_route=&route;\n phy_provider=(struct typec_mux_dev){.set=phy_set};',1) + controls=r''' +static int late_finish_error(struct typec_mux_dev *m,struct typec_mux_state *s) { (void)m;(void)s;return -EIO; } + int main(int argc,char **argv) { + (void)argc;struct cd321x c;initial(&c);struct typec_mux_state safe={.mode=TYPEC_STATE_SAFE}; + if(argv[1][0]=='o') { + assert(typec_mux_set(&mux,&safe)==0); + assert(stops==1&&!port.owner&&!port.safe_pending&&!route.selected); + puts("PASS actual DCP prepare + route_set + route_deactivate + framework normal SAFE"); + } + if(argv[1][0]=='r') { + mux.mux_devs[0]=&display_provider;mux.mux_devs[1]=&phy_provider;phy_error=-ETIMEDOUT; + assert(typec_mux_set(&mux,&safe)==-ETIMEDOUT); + fprintf(stderr,"display-first failed SAFE: owner=%p lease=%d selected=%d events=%s\n",(void *)port.owner,port.safe_pending,route.selected,events); + assert(port.owner==&route&&port.safe_pending&&route.selected); + } + if(argv[1][0]=='d') { + phy_error=-ETIMEDOUT;assert(typec_mux_set(&mux,&safe)==-ETIMEDOUT&&port.safe_pending&&port.owner==&route); + phy_error=0;struct typec_altmode alt={.svid=USB_TYPEC_DP_SID}; + struct typec_displayport_data data={.status=DP_STATUS_HPD_STATE}; + struct typec_mux_state dp={.alt=&alt,.mode=TYPEC_DP_STATE_A,.data=&data}; + int ret=typec_mux_set(&mux,&dp); + fprintf(stderr,"DP after failed SAFE: ret=%d connects=%d pending=%d wanted=%d hpd=%d\n",ret,connect_calls,port.safe_pending,port.dp_wanted,port.hpd); + assert(ret<0&&connect_calls==0&&!port.dp_wanted); + } + if(argv[1][0]=='a') { + struct altmode alt={.mux=&mux,.adev={.svid=USB_TYPEC_DP_SID}};phy_error=-ETIMEDOUT; + int ret=typec_altmode_set_mux(&alt,TYPEC_STATE_SAFE,NULL); + fprintf(stderr,"altmode SAFE: ret=%d stops=%d phy_calls=%d lease=%d\n",ret,stops,phy_calls,port.safe_pending); + assert(ret==-ETIMEDOUT&&stops==1&&port.safe_pending); + } + if(argv[1][0]=='x') { + xbar_error=-EIO;assert(typec_mux_set(&mux,&safe)==-EIO); + assert(port.owner==&route&&port.safe_pending&&!port.safe_finish_ready&&route.selected&&route.deactivate_xbar_done&&route.deactivate_error==-EIO); + xbar_error=0;assert(typec_mux_set(&mux,&safe)==-EIO&&port.owner==&route&&port.safe_pending&&xbar_calls==1); + } + if(argv[1][0]=='f') { + mux.mux_devs[0]=&display_provider;mux.mux_devs[1]=&phy_provider;phy_provider.finish=late_finish_error; + assert(typec_mux_set(&mux,&safe)==-EIO); + assert(port.owner==&route&&port.safe_pending&&port.safe_finish_ready&&!route.selected&&xbar_calls==1); + assert(!dcp_typec_route_available(&route)); + struct apple_dcp peer={0};struct apple_dcp_typec_route peer_route={.dcp=&peer}; + assert(dcp_typec_route_available(&peer_route)); + phy_provider.finish=NULL; + assert(typec_mux_set(&mux,&safe)==0&&!port.owner&&!port.safe_pending&&xbar_calls==1); + } + return 0; + } + ''' + bus=(ROOT/'drivers/usb/typec/bus.c').read_text() + actual+='\nstruct altmode { struct typec_mux *mux; struct typec_altmode adev; };\n'+block(bus,'static inline int\ntypec_altmode_set_mux(')+'\n'+controls + check('actual-dcp-normal-safe',actual,(('ordered',),)) + check('actual-dcp-finalizer-recovery',actual,(('xbar-error',),('finalizer',))) + check('provider-order-releases-failed-lease',actual,(('release',),)) + check('failed-safe-allows-new-dp-admission',actual,(('dp',),)) + check('altmode-safe-skips-quiescence',actual,(('altmode',),)) + + # Real pthread fabric/consumer mutexes. Pause the SAFE actor after its actual + # DCP prepare has returned and released its locks, before any set callback. + # A second handle can reference the same providers (class/altmode/CD handles + # are separately acquired in the real framework). + concurrent=actual.replace(block(actual,'int main('),block(actual,'int main(').replace('int main(','int __attribute__((unused)) route_controls_main(',1),1) + concurrent=concurrent.replace('#define guard(k) (void)','') + concurrent=concurrent.replace('#define scoped_guard(k,l) for(int once=((void)(l),1);once;once=0)','') + locks=r''' + #include + static pthread_mutex_t fabric_mutex=PTHREAD_MUTEX_INITIALIZER,external_mutex=PTHREAD_MUTEX_INITIALIZER,tb_mutex=PTHREAD_MUTEX_INITIALIZER; + struct real_guard { pthread_mutex_t *mutex; bool held; }; + static struct real_guard take_lock(int *key) { + pthread_mutex_t *m=key==&dcp_typec_fabric_lock?&fabric_mutex:key==&display.external_lock?&external_mutex:&tb_mutex; + assert(pthread_mutex_lock(m)==0);return (struct real_guard){m,true}; + } + static void drop_lock(struct real_guard *g) { if(g->held){g->held=false;assert(pthread_mutex_unlock(g->mutex)==0);} } + #define guard(k) struct real_guard __attribute__((cleanup(drop_lock))) _guard = take_lock + #define scoped_guard(k,l) for(struct real_guard __attribute__((cleanup(drop_lock))) _scope=take_lock(l);_scope.held;drop_lock(&_scope)) + static bool observed_unquiesced_safe; + ''' + pos=concurrent.index('static int dcp_typec_route_deactivate(') + concurrent=concurrent[:pos]+locks+concurrent[pos:] + concurrent=concurrent.replace("(void)m;(void)s;event('L');phy_calls++;", "(void)m;if(s->mode==TYPEC_STATE_SAFE&&display.dptxport[0].connected)observed_unquiesced_safe=true;event('L');phy_calls++;",1) + concurrent+=r''' + static pthread_mutex_t schedule_lock=PTHREAD_MUTEX_INITIALIZER; + static pthread_cond_t schedule_cond=PTHREAD_COND_INITIALIZER; + static bool prepared,continue_safe; + static int safe_ret,dp_ret; + static int scheduled_prepare(struct typec_mux_dev *m,struct typec_mux_state *s) { + int ret=dcp_typec_route_prepare(m,s); + if(s->mode==TYPEC_STATE_SAFE&&!s->alt) { + pthread_mutex_lock(&schedule_lock);prepared=true;pthread_cond_broadcast(&schedule_cond); + while(!continue_safe)pthread_cond_wait(&schedule_cond,&schedule_lock); + pthread_mutex_unlock(&schedule_lock); + } + return ret; + } + static void *safe_actor(void *unused) { + (void)unused;struct typec_mux_state safe={.mode=TYPEC_STATE_SAFE};safe_ret=typec_mux_set(&mux,&safe);return NULL; + } + int main(void) { + struct cd321x c;initial(&c);display_provider.prepare=scheduled_prepare; + pthread_t thread;assert(pthread_create(&thread,NULL,safe_actor,NULL)==0); + pthread_mutex_lock(&schedule_lock);while(!prepared)pthread_cond_wait(&schedule_cond,&schedule_lock);pthread_mutex_unlock(&schedule_lock); + struct typec_mux second_handle=mux; + struct typec_altmode alt={.svid=USB_TYPEC_DP_SID};struct typec_displayport_data data={.status=DP_STATUS_HPD_STATE}; + struct typec_mux_state dp={.alt=&alt,.mode=TYPEC_DP_STATE_A,.data=&data}; + dp_ret=typec_mux_set(&second_handle,&dp); + pthread_mutex_lock(&schedule_lock);continue_safe=true;pthread_cond_broadcast(&schedule_cond);pthread_mutex_unlock(&schedule_lock); + assert(pthread_join(thread,NULL)==0); + fprintf(stderr,"overlapping handles: dp_ret=%d safe_ret=%d connected_at_SAFE_PHY=%d events=%s\n",dp_ret,safe_ret,observed_unquiesced_safe,events); + assert(!observed_unquiesced_safe); + } + ''' + check('concurrent-prepare-set-stale-quiescence',concurrent,flags=('-pthread',)) + check('connector-context-lifetime', lifetime(ROOT), flags=('-pthread',)) + check('probe-transaction-rollback', probe(ROOT)) + check('provider-unregister-drain', unregister(ROOT), flags=('-pthread',)) + check('park-before-host-role-exit', clock_order(ROOT)) + return tests + + +def lifetime(root): + mux = (root / 'drivers/usb/typec/mux.c').read_text() + unit = quiesce.make_unit(root) + unit = unit.replace(block(unit, 'int main('), + block(unit, 'int main(').replace('int main(', 'int __attribute__((unused)) prior_main(', 1), 1) + unit = unit.replace('struct typec_mux_context { struct mutex lock; };', r""" +struct list_head { struct list_head *next,*prev; }; +struct fwnode_handle { unsigned refs; }; +struct typec_mux_context { struct list_head list; struct fwnode_handle *fwnode; struct mutex lock; unsigned users; }; +""") + helpers = r""" +#include +#include +#undef guard +#undef list_for_each_entry +static struct mutex typec_mux_contexts_lock={.value=PTHREAD_MUTEX_INITIALIZER}; +static struct list_head typec_mux_contexts={&typec_mux_contexts,&typec_mux_contexts}; +static struct fwnode_handle *fwnode_handle_get(struct fwnode_handle *f) { f->refs++;return f; } +static void fwnode_handle_put(struct fwnode_handle *f) { assert(f->refs);f->refs--; } +static void mutex_init(struct mutex *m) { assert(!pthread_mutex_init(&m->value,NULL)); } +static struct mutex *registry_lock(struct mutex *m) { assert(!pthread_mutex_lock(&m->value));return m; } +static void registry_unlock(struct mutex **m) { mutex_unlock(*m); } +#define guard(k) struct mutex * __attribute__((cleanup(registry_unlock))) held=registry_lock +#define kzalloc_obj(x) calloc(1,sizeof(x)) +#define kfree(x) free(x) +#define ERR_PTR(e) ((void *)(intptr_t)(e)) +#define list_for_each_entry(p,h,m) for(p=container_of((h)->next,__typeof__(*p),m); &(p)->m!=(h);p=container_of((p)->m.next,__typeof__(*p),m)) +static void list_add_tail(struct list_head *n,struct list_head *h) { n->prev=h->prev;n->next=h;h->prev->next=n;h->prev=n; } +static void list_del(struct list_head *n) { n->prev->next=n->next;n->next->prev=n->prev; } +""" + unit += helpers + block(mux, 'static struct typec_mux_context *typec_mux_context_get(') + '\n' + unit += block(mux, 'static void typec_mux_context_put(') + '\n' + unit += r""" +static struct typec_mux downstream; +static int forwarded,commits,aborts,finish_error; +static int forward_set(struct typec_mux_dev *m,struct typec_mux_state *s) { (void)m;forwarded++;return typec_mux_set(&downstream,s); } +static int finish(struct typec_mux_dev *m,struct typec_mux_state *s) { (void)m;(void)s;return finish_error; } +static void commit(struct typec_mux_dev *m,struct typec_mux_state *s) { (void)m;(void)s;commits++; } +static void abort_callback(struct typec_mux_dev *m,struct typec_mux_state *s) { (void)m;(void)s;aborts++; } +int main(void) { + struct fwnode_handle a={0},b={0}; + struct typec_mux_context *one=typec_mux_context_get(&a),*two=typec_mux_context_get(&a),*other=typec_mux_context_get(&b); + assert(one==two&&one!=other&&one->users==2&&a.refs==1&&b.refs==1); + struct cd321x c;initial(&c);struct typec_mux_state safe={.mode=TYPEC_STATE_SAFE}; + struct typec_mux second=mux; mux.context=one;second.context=two; + assert(typec_mux_begin(&mux,&safe)==0); + unsigned before=phy_calls;assert(typec_mux_set(&second,&safe)==-EBUSY&&phy_calls==before); + typec_mux_abort(&mux);assert(port.safe_pending&&port.owner==&route); + phy_error=-ETIMEDOUT;assert(typec_mux_set(&second,&safe)==-ETIMEDOUT&&port.owner==&route); + phy_error=0;assert(typec_mux_set(&second,&safe)==0&&!port.owner&&!port.safe_pending); + struct typec_mux_dev leaf={.set=phy_set,.finish=finish,.commit=commit,.abort=abort_callback}; + downstream=(struct typec_mux){.context=other,.num_mux_devs=1,.mux_devs={&leaf}}; + struct typec_mux_dev forward={.set=forward_set}; + mux=(struct typec_mux){.context=one,.num_mux_devs=1,.mux_devs={&forward}}; + assert(typec_mux_set(&mux,&safe)==0&&forwarded==1&&commits==1); + finish_error=-EIO;assert(typec_mux_set(&downstream,&safe)==-EIO&&commits==1&&aborts==1); + finish_error=0;assert(typec_mux_set(&downstream,&safe)==0&&commits==2); + downstream.context=one;before=phy_calls; + assert(typec_mux_set(&mux,&safe)==-EBUSY&&phy_calls==before); // cyclic same-connector forwarding + leaf.removed=true;downstream.context=other;before=phy_calls; + assert(typec_mux_set(&downstream,&safe)==-ENODEV&&phy_calls==before&&aborts==1); + typec_mux_context_put(one);assert(a.refs==1&&two->users==1); + typec_mux_context_put(two);assert(a.refs==0); + typec_mux_context_put(other);assert(b.refs==0&&typec_mux_contexts.next==&typec_mux_contexts); + puts("PASS actual connector registry, shared handles, forwarding, finalize/abort and removed-provider guards"); +} +""" + return unit + + +def probe(root): + core = (root / 'drivers/usb/typec/tipd/core.c').read_text() + unit = quiesce.make_unit(root) + unit = unit.replace(block(unit, 'int main('), block(unit, 'int main(').replace('int main(', 'int __attribute__((unused)) prior_main(', 1), 1) + unit = unit.replace('struct tipd_data {', 'struct fwnode_handle;\nstruct tipd_data { unsigned irq_mask1; int (*init)(struct tps6598x *); int (*switch_power_state)(struct tps6598x *, int); int (*register_port)(struct tps6598x *, struct fwnode_handle *); bool (*read_data_status)(struct tps6598x *); int (*connect)(struct tps6598x *,u32); void (*ready)(struct tps6598x *); void *irq_handler;') + unit = unit.replace('struct tps6598x {', 'struct tps6598x { bool dbma_at_probe,wakeup;') + helpers = r""" +#undef dev_err +#define dev_err(...) ((void)0) +#define TPS_SETUP_MS 1000 +#define TPS_SYSTEM_POWER_STATE_S0 0 +#define TPS_MODE_PTCH 2 +#define TPS_MODE_DBMA 3 +#define TPS_REG_VID 0 +#define TPS_REG_INT_MASK1 1 +#define TPS_STATUS_PLUG_PRESENT 1 +#define GPIOD_OUT_LOW 0 +#include +#undef IS_ERR +#define IS_ERR(p) ((uintptr_t)(p)>=(uintptr_t)-4095) +#define PTR_ERR(p) ((int)(intptr_t)(p)) +#define IRQF_SHARED 0 +#define IRQF_ONESHOT 0 +#define POLL_INTERVAL 1000 +#define INIT_DELAYED_WORK(...) ((void)0) +#define tps6598x_poll_work 0 +#define devm_device_init_wakeup(...) ((void)0) +#define enable_irq_wake(...) ((void)0) +#define dev_err_probe(...) (-EINVAL) +struct fwnode_handle { int unused; }; +static int connect_error,irq_error,register_error,node_puts,mux_get_error; +static void *devm_gpiod_get_optional(void *d,const char *s,int v) { (void)d;(void)s;(void)v;return NULL; } +static void msleep(unsigned ms) { (void)ms; } +static bool device_is_compatible(void *d,const char *s) { (void)d;(void)s;return false; } +static int tps6598x_read32(struct tps6598x *t,int reg,u32 *v) { (void)t;(void)reg;*v=1;return 0; } +static int tps6598x_check_mode(struct tps6598x *t) { (void)t;return 0; } +static int tps6598x_write64(struct tps6598x *t,int reg,unsigned v) { (void)t;(void)reg;(void)v;return 0; } +static bool tps6598x_read_status(struct tps6598x *t,u32 *s) { (void)t;*s=TPS_STATUS_PLUG_PRESENT;return true; } +static bool tps6598x_read_power_status(struct tps6598x *t) { (void)t;return true; } +static struct fwnode_handle node; +static struct fwnode_handle *device_get_named_child_node(void *d,const char *s) { (void)d;(void)s;return &node; } +static void fw_devlink_purge_absent_suppliers(struct fwnode_handle *f) { (void)f; } +static void *fwnode_usb_role_switch_get(struct fwnode_handle *f) { (void)f;return (void *)1; } +static struct typec_mux *fwnode_typec_mux_get(struct fwnode_handle *f) { (void)f;return mux_get_error?(struct typec_mux *)(intptr_t)mux_get_error:&mux; } +static void fwnode_handle_put(struct fwnode_handle *f) { (void)f;node_puts++; } +static int devm_tps6598_psy_register(struct tps6598x *t) { (void)t;return 0; } +static int devm_request_threaded_irq(void *d,int irq,void *h,void *th,int flags,const char *name,void *arg) { (void)d;(void)irq;(void)h;(void)th;(void)flags;(void)name;(void)arg;return irq_error; } +static int system_power_efficient_wq; +static unsigned msecs_to_jiffies(unsigned ms) { return ms; } +static void queue_delayed_work(int wq,int *work,unsigned delay) { (void)wq;(void)work;(void)delay; } +static bool device_property_read_bool(void *d,const char *p) { (void)d;(void)p;return false; } +static void tipd_debugfs_register(struct tps6598x *t) { (void)t; } +static int register_port(struct tps6598x *t,struct fwnode_handle *f) { (void)t;(void)f;return register_error; } +static bool read_data_status(struct tps6598x *t) { (void)t;return true; } +static int connect(struct tps6598x *t,u32 status) { (void)t;(void)status;return connect_error; } +""" + unit += helpers + block(core, 'int tipd_init(') + r""" +static void probe_initial(struct cd321x *c) { + initial(c);c->tps.irq=1;data.register_port=register_port;data.read_data_status=read_data_status;data.connect=connect; + connect_error=irq_error=register_error=node_puts=mux_get_error=0; +} +int main(void) { + struct cd321x c; + probe_initial(&c);irq_error=-ENOSPC;phy_error=-ETIMEDOUT; + assert(tipd_init(&c.tps)==-ENOSPC&&reset_calls==0&&gpio_calls==0&&port.owner==&route&&port.safe_pending&&unregisters==1&&node_puts==1); + probe_initial(&c);connect_error=-EAGAIN;role_error=-EBUSY; + assert(tipd_init(&c.tps)==-EAGAIN&&reset_calls==0&&gpio_calls==0&&phy_calls==1&&port.owner==&route&&port.safe_pending); + probe_initial(&c);irq_error=-ENOSPC; + assert(tipd_init(&c.tps)==-ENOSPC&&reset_calls==1&&!port.owner&&!port.safe_pending&&unregisters==1); + probe_initial(&c);mux_get_error=-ENODEV; + assert(tipd_init(&c.tps)==-ENODEV&&reset_calls==0&&stops==0&&phy_calls==0&&unregisters==0&&node_puts==1); + probe_initial(&c);register_error=-EIO; + assert(tipd_init(&c.tps)==-EIO&&reset_calls==0&&stops==0&&phy_calls==0&&unregisters==0); + puts("PASS actual probe rollback preserves primary errors and blocks reset after SAFE/role/registration errors"); +} +""" + return unit + + +def unregister(root): + """Actual unregister/begin/end with sleeping SRCU leaves and two threads.""" + source = (root / 'drivers/usb/typec/mux.c').read_text() + unit = quiesce.make_unit(root) + unit = unit.replace(block(unit, 'int main('), block(unit, 'int main(').replace('int main(', 'int __attribute__((unused)) prior_main(', 1), 1) + unit = unit.replace('struct typec_mux_dev {', 'struct device { bool unregistered; };\nstruct typec_mux_dev { struct device dev;') + old = block(unit, 'static int srcu_read_lock(') + '\n' + block(unit, 'static void srcu_read_unlock(') + leaves = r""" +static pthread_mutex_t lifetime_lock=PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t lifetime_cond=PTHREAD_COND_INITIALIZER; +static unsigned readers; +static bool draining; +static int srcu_read_lock(int *s) { (void)s;pthread_mutex_lock(&lifetime_lock);readers++;pthread_mutex_unlock(&lifetime_lock);return 0; } +static void srcu_read_unlock(int *s,int i) { (void)s;(void)i;pthread_mutex_lock(&lifetime_lock);assert(readers);readers--;pthread_cond_broadcast(&lifetime_cond);pthread_mutex_unlock(&lifetime_lock); } +static void synchronize_srcu(int *s) { (void)s;pthread_mutex_lock(&lifetime_lock);draining=true;pthread_cond_broadcast(&lifetime_cond);while(readers)pthread_cond_wait(&lifetime_cond,&lifetime_lock);pthread_mutex_unlock(&lifetime_lock); } +static void device_unregister(struct device *d) { assert(!readers);d->unregistered=true; } +#define WRITE_ONCE(x,v) ((x)=(v)) +""" + unit = unit.replace(old, leaves, 1) + unit += block(source, 'void typec_mux_unregister(') + r""" +static void *remove_provider(void *arg) { typec_mux_unregister(arg);return NULL; } +int main(void) { + struct cd321x c;initial(&c);struct typec_mux_state safe={.mode=TYPEC_STATE_SAFE}; + assert(typec_mux_begin(&mux,&safe)==0&&readers==1); + pthread_t thread;assert(!pthread_create(&thread,NULL,remove_provider,&phy_provider)); + pthread_mutex_lock(&lifetime_lock);while(!draining)pthread_cond_wait(&lifetime_cond,&lifetime_lock);pthread_mutex_unlock(&lifetime_lock); + assert(phy_provider.removed&&!phy_provider.dev.unregistered&&readers==1); + assert(typec_mux_end(&mux)==0); // pre-existing transaction finishes before removal returns + assert(!pthread_join(thread,NULL)&&phy_provider.dev.unregistered&&readers==0); + unsigned before=phy_calls; + assert(typec_mux_set(&mux,&safe)==-ENODEV&&phy_calls==before&&readers==0); + puts("PASS actual unregister drains the open transaction and tombstone blocks stale handles"); +} +""" + return unit + + +def clock_order(root): + unit = quiesce.make_unit(root) + unit = unit.replace(block(unit, 'int main('), block(unit, 'int main(').replace('int main(', 'int __attribute__((unused)) prior_main(', 1), 1) + unit = unit.replace('static int power_error,', 'static bool backend_clock=true,parked;\nstatic int power_error,') + unit = unit.replace("event('R');role_calls++;return role_error;", "event('R');role_calls++;if(!role_error)backend_clock=false;return role_error;", 1) + unit = unit.replace("event('L');phy_calls++;return phy_error;", "event('L');phy_calls++;if(parked)return 0;if(!backend_clock)return -ENODEV;if(!phy_error)parked=true;return phy_error;", 1) + unit += r""" +int main(void) { + (void)current;struct cd321x c;initial(&c); + tipd_remove(&c.tps); + assert(!last_error&&reset_calls==1&&gpio_calls==1&&parked&&!backend_clock); + assert(position('Q')