From bb0d66e5ad7b72ff6f9d0e7656a376274871cc2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20Carre=C3=B1o?= Date: Wed, 28 Oct 2026 11:00:56 +0100 Subject: [PATCH 01/22] soc: apple: report the T6031 GPU to the PMP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The T6031 (M3 Max) G15 GPU is powered by its PMGR domain alone, with no RTKit vote, and nothing sets its PTD power request. PMP then ignores the GPU's bandwidth demand and memory stays at its idle floor. Port the T6030 report (9bb74767dc) to T6031: AGX is the acknowledged soc-device ID 14 (record 13) of the J516C table, on the T6031 report apertures. Set it after firmware and backend setup, clearing it first, since an earlier request is acknowledged without enabling bandwidth scaling. A failure only warns and clears the request, which is how the GPU ran before. The session and the request stay until reboot. With Mesa unchanged, compute-copy bandwidth (test-backend-ops perf -o CPY, Vulkan) goes from 75 to 316 GB/s for 1.5 GB f32 copies and from 56 to 200 GB/s for 768 MB f16/bf16 copies. Tested on an M3 Max J516C, on top of local patches (HDMI, panel brightness, AVD AV1, in-flight cap); the panel, HDMI, brightness and AVD keep working. Generated-by: LLM Signed-off-by: Samuel Carreño --- drivers/gpu/drm/asahi/Kconfig | 1 + drivers/gpu/drm/asahi/g15/boot.rs | 1 + drivers/gpu/drm/asahi/g15/glue.c | 8 +++++ drivers/gpu/drm/asahi/g15/mod.rs | 15 ++++++++ drivers/pmdomain/apple/pmp-report.c | 52 ++++++++++++++++++++++++++++ drivers/soc/apple/Kconfig | 10 ++++++ include/linux/soc/apple/pmp-report.h | 7 ++++ 7 files changed, 94 insertions(+) diff --git a/drivers/gpu/drm/asahi/Kconfig b/drivers/gpu/drm/asahi/Kconfig index 0e989189206bad..a32095f36ca982 100644 --- a/drivers/gpu/drm/asahi/Kconfig +++ b/drivers/gpu/drm/asahi/Kconfig @@ -67,6 +67,7 @@ config DRM_ASAHI_G15 depends on DRM_ASAHI depends on DRM_APPLE=y || (DRM_APPLE=m && DRM_ASAHI=m) select APPLE_T6030_GPU_PMP_LINK + select APPLE_T6031_GPU_PMP_REPORT select CRYPTO_LIB_SHA256 select DRM_ASAHI_UAT_ADOPTION default n diff --git a/drivers/gpu/drm/asahi/g15/boot.rs b/drivers/gpu/drm/asahi/g15/boot.rs index 45e244531323d7..8ea95d88e90023 100644 --- a/drivers/gpu/drm/asahi/g15/boot.rs +++ b/drivers/gpu/drm/asahi/g15/boot.rs @@ -75,6 +75,7 @@ pub(crate) mod glue { pub(crate) fn asahi_g15_display_prepare(dev: *mut core::ffi::c_void) -> core::ffi::c_int; pub(crate) fn asahi_g15_pmp_get(dev: *mut core::ffi::c_void) -> core::ffi::c_int; pub(crate) fn asahi_g15_pmp_ready() -> core::ffi::c_int; + pub(crate) fn asahi_g15_pmp_report_t6031(enable: bool) -> core::ffi::c_int; pub(crate) fn asahi_g15_pmp_put(dev: *mut core::ffi::c_void); fn asahi_g15_reserved_uat(index: u32, start: u64, size: u64) -> bool; } diff --git a/drivers/gpu/drm/asahi/g15/glue.c b/drivers/gpu/drm/asahi/g15/glue.c index 385b9fde390538..8b09a012a4c7c5 100644 --- a/drivers/gpu/drm/asahi/g15/glue.c +++ b/drivers/gpu/drm/asahi/g15/glue.c @@ -19,6 +19,7 @@ #include #include #include +#include #include void asahi_g15_sha256(const u8 *data, size_t len, u8 *out); @@ -37,6 +38,7 @@ int asahi_g15_pmp_prepare(struct device *dev); int asahi_g15_display_prepare(struct device *dev); int asahi_g15_pmp_get(struct device *dev); int asahi_g15_pmp_ready(void); +int asahi_g15_pmp_report_t6031(bool enable); void asahi_g15_pmp_put(struct device *dev); bool asahi_g15_reserved_uat(u32 index, u64 start, u64 size); @@ -184,6 +186,12 @@ int asahi_g15_pmp_ready(void) return apple_pmp_t6030_gpu_ready(); } +/* T6031: the AGX request of the PMP report, after the firmware is running. */ +int asahi_g15_pmp_report_t6031(bool enable) +{ + return apple_pmp_report_t6031_gfx(enable); +} + /* * Give back the vote of asahi_g15_pmp_get() after a start that failed before * the firmware was touched. The full M3 stack's J514S bridge has no release: diff --git a/drivers/gpu/drm/asahi/g15/mod.rs b/drivers/gpu/drm/asahi/g15/mod.rs index 8bfa34a5892fde..f9ff5ec7f1cc54 100644 --- a/drivers/gpu/drm/asahi/g15/mod.rs +++ b/drivers/gpu/drm/asahi/g15/mod.rs @@ -566,6 +566,21 @@ impl Firmware { } = KBox::into_inner(firmware); return Err(orphan(session, objects, channels, init, events, e)); } + if cfg.platform == hw::Platform::DeviceTree { + // T6031: nothing else sets the PMP's AGX request, and without it memory stays at its + // idle floor. Set it after setup, as T6030 does; a failure keeps the GPU as before. + // SAFETY: The session is retained until reboot, and the request with it. + let ret = unsafe { boot::glue::asahi_g15_pmp_report_t6031(true) }; + if ret == 0 { + dev_info!(dev.as_ref(), "G15: PMP AGX request set (memory bandwidth scaling)\n"); + } else if ret != ENODEV.to_errno() { + dev_warn!( + dev.as_ref(), + "G15: PMP AGX request failed ({}); memory stays at its idle floor\n", + ret + ); + } + } Ok(firmware) } } diff --git a/drivers/pmdomain/apple/pmp-report.c b/drivers/pmdomain/apple/pmp-report.c index 98d96d08f8a434..0b639821843c02 100644 --- a/drivers/pmdomain/apple/pmp-report.c +++ b/drivers/pmdomain/apple/pmp-report.c @@ -1348,6 +1348,58 @@ int apple_pmp_report_set_t6030_gfx(struct device *supplier, bool enable) } EXPORT_SYMBOL_GPL(apple_pmp_report_set_t6030_gfx); +/* + * T6031 (M3 Max): the GPU is powered by its PMGR domain alone, with no RTKit + * vote, and nothing sets its PTD request, so PMP ignores the GPU's bandwidth + * demand and memory stays at its idle floor. AGX is the acknowledged + * soc-device ID 14 (record 13) of the J516C table. As on T6030, a request + * made before the firmware runs is acknowledged without enabling bandwidth + * scaling: enabling clears and sets it again. On a failed enable the request + * is cleared, which is how the GPU ran before. + */ +int apple_pmp_report_t6031_gfx(bool enable) +{ + struct apple_pmp_report *rep, *found = NULL; + struct device_node *pmp = NULL; + const u8 *table = NULL; + int len = 0, ret = -ENODEV; + + if (!IS_ENABLED(CONFIG_APPLE_T6031_GPU_PMP_REPORT) || + !of_machine_is_compatible("apple,t6031")) + return -ENODEV; + + rcu_read_lock(); + list_for_each_entry_rcu(rep, &apple_pmp_reports, list) { + if (rep->offsets != &apple_pmp_offsets_t6031) + continue; + if (found) + goto out; + found = rep; + } + if (!found) + goto out; + pmp = of_parse_phandle(found->dev->of_node, "apple,pmp", 0); + if (pmp && of_device_is_compatible(pmp, "apple,t6031-pmp-v2")) + table = of_get_property(pmp, "apple,tunable-soc-device", &len); + if (!apple_pmp_devices_valid(table, len > 0 ? len : 0, BIT_ULL(13), BIT_ULL(13), true) || + memcmp(table + 13 * PMP_SOC_DEVICE_SIZE + 116, "AGX\0\0\0\0\0", 8)) { + ret = -EINVAL; + } else if (readq(found->base + found->offsets->status) != PMP_REPORT_READY) { + ret = -EAGAIN; + } else { + ret = apple_pmp_report_set_state(found, 13, false, false); + if (!ret && enable) + ret = apple_pmp_report_set_state(found, 13, true, false); + if (ret && enable) + apple_pmp_report_set_state(found, 13, false, false); + } + of_node_put(pmp); +out: + rcu_read_unlock(); + return ret; +} +EXPORT_SYMBOL_GPL(apple_pmp_report_t6031_gfx); + static int apple_pmp_report_entry_set_state(struct generic_pm_domain *genpd, bool enable) { struct apple_pmp_report_entry *ent = genpd_to_apple_pmp_report_entry(genpd); diff --git a/drivers/soc/apple/Kconfig b/drivers/soc/apple/Kconfig index ba57db91f8860b..fd64eae1b7280c 100644 --- a/drivers/soc/apple/Kconfig +++ b/drivers/soc/apple/Kconfig @@ -257,6 +257,16 @@ config APPLE_T6030_GPU_PMP_LINK GPU power commands wait for that supplier's acknowledged display request through the existing PMP report startup and removal rules. +config APPLE_T6031_GPU_PMP_REPORT + bool + depends on APPLE_PMP_REPORT + default n + help + Set the AGX request of the T6031 (M3 Max) PMP report once the G15 + firmware runs. The T6031 GPU is powered by its PMGR domain alone, so + nothing else sets that request, and without it the PMP ignores the + GPU's bandwidth demand: memory stays at its idle floor. + config APPLE_DISPLAY_GATE_KUNIT_TEST bool "KUnit tests for the Apple display gate" if !KUNIT_ALL_TESTS depends on KUNIT=y && APPLE_T6030_DISPLAY_GATE diff --git a/include/linux/soc/apple/pmp-report.h b/include/linux/soc/apple/pmp-report.h index 1d0689f31c2039..53098fc67edb01 100644 --- a/include/linux/soc/apple/pmp-report.h +++ b/include/linux/soc/apple/pmp-report.h @@ -18,6 +18,8 @@ int apple_pmp_report_wait_supplier_ready(struct device *supplier, unsigned int t /* Caller holds the PMP owner's vote lock and keeps the supplier bound. */ int apple_pmp_report_check_t6030_gfx(struct device *supplier); int apple_pmp_report_set_t6030_gfx(struct device *supplier, bool enable); +/* After the T6031 G15 firmware is running; the request stays until reboot. */ +int apple_pmp_report_t6031_gfx(bool enable); int apple_pmp_report_wait_ready(struct device_node *entry, unsigned long timeout); int apple_pmp_report_fast_die_effort(const struct device_node *np, unsigned int lane, u8 *effort); @@ -38,6 +40,11 @@ static inline int apple_pmp_report_set_t6030_gfx(struct device *supplier, bool e return -ENODEV; } +static inline int apple_pmp_report_t6031_gfx(bool enable) +{ + return -ENODEV; +} + static inline int apple_pmp_report_wait_ready(struct device_node *entry, unsigned long timeout) { From 801ad8b1f2428165374927eba4b29fc8c02a73cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20Carre=C3=B1o?= Date: Mon, 26 Oct 2026 21:45:42 +0100 Subject: [PATCH 02/22] drm/asahi: g15: allow 8 prefixes in flight per VM (was 2), tunable with g15_max_inflight MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On the T6031 (J516C) a browser submits one render per job. With only two prefixes in flight and ~612 us of host/firmware turnaround per flight while the GPU work per render is ~88 us (asahi-g15/stats span vs busy), the engine held publication on the in-flight cap ~3 times per job (stats hold[0]) and the GPU idled ~85% of every flight: at 120 Hz Chrome's ~2.7k renders/s hit the ~3.3k/s ceiling and frames queued up as stutter. Power (17-24 W) and a resolution-scaling test (1322 -> 1276 fps for 4x pixels) showed the GPU was starved, not busy; pstate was already 8/8. Raise FLIGHTS and MAX_INFLIGHT to 8 and add g15_max_inflight (0: tuned value, 1..8) so it can be lowered without a rebuild. Later flights still have to be of the same VM; pass slots (16) and CT buffers bound the rest, and publish_prefix_into already backs off with EAGAIN when they are held. Generated-by: LLM Signed-off-by: Samuel Carreño --- drivers/gpu/drm/asahi/asahi_core.rs | 4 ++++ drivers/gpu/drm/asahi/g15/engine.rs | 8 ++++---- drivers/gpu/drm/asahi/g15/hw.rs | 11 +++++++---- drivers/gpu/drm/asahi/g15/params.rs | 8 ++++++++ 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/drivers/gpu/drm/asahi/asahi_core.rs b/drivers/gpu/drm/asahi/asahi_core.rs index 0fbe6e728773d3..7e95b7f1c909c8 100644 --- a/drivers/gpu/drm/asahi/asahi_core.rs +++ b/drivers/gpu/drm/asahi/asahi_core.rs @@ -123,6 +123,10 @@ module! { default: 100, description: "G15: completion repoll interval, us (25..1000)", }, + g15_max_inflight: u32 { + default: 0, + description: "G15: published prefixes in flight per VM at most (1..8; 0: the tuned value)", + }, g15_idle_standby_us: u32 { default: 0, description: "G15: firmware idle standby time override, us (Globals +0x7a8, 100..100000; 0: the device tree's, else the tuned value)", diff --git a/drivers/gpu/drm/asahi/g15/engine.rs b/drivers/gpu/drm/asahi/g15/engine.rs index 80e4a31a78706d..67f420b82a00e1 100644 --- a/drivers/gpu/drm/asahi/g15/engine.rs +++ b/drivers/gpu/drm/asahi/g15/engine.rs @@ -9,9 +9,9 @@ //! then thresholds/counters once per prefix, then one queue WRITE per queue, then channel //! records, then doorbells (compute, FS, TA). A compute-only job that ends its prefix pulls //! further ready compute-only jobs of the same queue and VM into a *horizon* (up to 16, posted -//! ahead at the observed depth). At most [`super::hw::sizes::FLIGHTS`] prefixes are in flight, -//! the second only of the same VM, posted behind the first's unconsumed records -//! (submit-ahead). Ordering is a total order, which satisfies the header barriers and +//! ahead at the observed depth). At most [`super::hw::sizes::FLIGHTS`] prefixes are in flight +//! (`g15_max_inflight`), after the first only of the same VM, each posted behind the previous +//! one's unconsumed records (submit-ahead). Ordering is a total order, which satisfies the header barriers and //! EXACT_PRIOR ordinals validated at submission. Priming: after a recovered abort and for the //! first render prefix after boot, an exclusive one-render prefix rebuilds the render //! allocators and blocks publication behind it until it retires. @@ -545,7 +545,7 @@ impl Firmware { /// Returns Err only when this call latched the engine failed (a publication error after a /// queue WRITE advanced, or a partly sent job): the caller latches the fail-stop. pub(crate) fn publish(&mut self, done: &mut Completions) -> Result { - let max_inflight = tuning::MAX_INFLIGHT.clamp(1, sizes::FLIGHTS as u32) as usize; + let max_inflight = params::max_inflight() as usize; let mut result = Ok(()); self.engine.chan_wait = false; diff --git a/drivers/gpu/drm/asahi/g15/hw.rs b/drivers/gpu/drm/asahi/g15/hw.rs index f231e573c40176..0a49c3c74ec723 100644 --- a/drivers/gpu/drm/asahi/g15/hw.rs +++ b/drivers/gpu/drm/asahi/g15/hw.rs @@ -506,8 +506,11 @@ pub(crate) mod sizes { pub(crate) const PB_BLOCKS: u32 = 4096; pub(crate) const PB_LIST_RECORDS: u32 = 4096; pub(crate) const PB_GROW_STEP: u32 = 10; - /// Flights (published prefixes) in flight at most; the second must be of the same VM. - pub(crate) const FLIGHTS: usize = 2; + /// Flights (published prefixes) in flight at most; after the first, only of the same VM. + /// Measured on the T6031 (J516C, 26-oct-2026): with 2, a browser's one-render jobs held + /// publication ~3 times per job (stats hold[0]) and the GPU spent ~75% of each flight idle + /// (span 88 us of a 612 us flight). 8 keeps the GPU fed; pass slots (16) bound the rest. + pub(crate) const FLIGHTS: usize = 8; /// Jobs per compute horizon, and scheduler credits per queue. pub(crate) const HORIZON_JOBS: u32 = 16; /// Unconsumed records per 256-entry channel ring a prefix may be posted behind. @@ -565,8 +568,8 @@ pub(crate) mod tuning { pub(crate) const PASS_FULL_RESET: bool = true; /// Rotating compute-slot cursor. pub(crate) const SLOT_ROTATE: bool = true; - /// Flights per VM. - pub(crate) const MAX_INFLIGHT: u32 = 2; + /// Flights per VM (tunable: `g15_max_inflight`, 0 = this value). + pub(crate) const MAX_INFLIGHT: u32 = 8; /// PB growth through event 7 / DEVCTL 7; the initial posting is `g15_pb_blocks` (64). pub(crate) const PB_GROW: bool = true; /// The most PB blocks that may be posted: one below the mapped heap's block count, so the diff --git a/drivers/gpu/drm/asahi/g15/params.rs b/drivers/gpu/drm/asahi/g15/params.rs index 48d21e45d0acd6..683b36c908e1d7 100644 --- a/drivers/gpu/drm/asahi/g15/params.rs +++ b/drivers/gpu/drm/asahi/g15/params.rs @@ -37,6 +37,14 @@ pub(crate) fn repoll_us() -> u32 { pub(crate) fn resnap_us() -> u32 { (*module_parameters::g15_resnap_us.value()).clamp(25, 1000) } +/// Published prefixes in flight per VM at most (0: the tuned value), bounded by the flight slots. +pub(crate) fn max_inflight() -> u32 { + match *module_parameters::g15_max_inflight.value() { + 0 => hw::tuning::MAX_INFLIGHT, + n => n, + } + .clamp(1, hw::sizes::FLIGHTS as u32) +} /// The explicit firmware idle standby time, us (0: not set). pub(crate) fn idle_standby_override_us() -> u32 { From 61fe14980ab8439f56dc8a182e8a7345996fc570 Mon Sep 17 00:00:00 2001 From: DB Date: Thu, 29 Oct 2026 19:21:38 +0000 Subject: [PATCH 03/22] arm64: dts: apple: t6031: describe dcpext0..3 and the ATC display crossbars Add the M3 Max's four external display processors, their mailboxes and firmware DARTs, all disabled, at the addresses, interrupts and power states of the J516C ADT (dcpextN, dart-dcpextN), translated through /arm-io ranges. The internal DCP's ADT values translate to exactly what t6031-dcp.dtsi already has, which checks the translation. dcpext0 0x3_06c00000 mbox IRQ 1068..1071 DART 0x3_0530c000 IRQ 1047 dcpext1 0x4_4ac00000 mbox IRQ 1104..1107 DART 0x4_4930c000 IRQ 1083 dcpext2 0x4_4ec00000 mbox IRQ 1140..1143 DART 0x4_4d30c000 IRQ 1119 dcpext3 0x2_cac00000 mbox IRQ 1176..1179 DART 0x2_c930c000 IRQ 1155 Each processor's ADT power gate is DISPEXTn-CPU-V, whose parents are DISPEXTn_CPU and DISPEXTn_FE, as DISP-CPU-V is the internal DCP's. So each sits on ps_dispextN_fe, as the internal DCP sits on ps_disp_fe. The node shape is T6030's dcpext: Type-C routes to ATC0..2, disabled until the display gate enables their processor. The compatible keeps apple,t6030-dcpext as a fallback so the display driver takes its T6030 external path. apple,typec-mux-indices is N for dcpextN because the ADT's display-crossbar0 lists its upstream ports as dispext0..7 in order; this is not yet confirmed on hardware. Add the three ATC display crossbars (ADT atcN-dpxbar, atc-dpxbar,t602x) at the T6030 addresses. With the t6020 compatible their probe writes no register. Give the PMP's pmp-dispext0..3 requests apple,always-on and apple,defer-startup-ack, as T6030's. They stay disabled, and the report driver seeds only available entries, so nothing changes until the gate enables one. Add the J514C/J516C Type-C port@3 endpoints for all four processors. Co-Authored-By: Claude Opus 5.5 Signed-off-by: DB --- arch/arm64/boot/dts/apple/t6031-dcp.dtsi | 412 +++++++++++++++++- arch/arm64/boot/dts/apple/t6031-die0.dtsi | 16 + arch/arm64/boot/dts/apple/t6031-dieX.dtsi | 27 ++ arch/arm64/boot/dts/apple/t6031-j514c.dts | 1 + arch/arm64/boot/dts/apple/t6031-j516c.dts | 1 + .../dts/apple/t603x-j514c-j516c-dcpext.dtsi | 95 ++++ 6 files changed, 551 insertions(+), 1 deletion(-) create mode 100644 arch/arm64/boot/dts/apple/t603x-j514c-j516c-dcpext.dtsi diff --git a/arch/arm64/boot/dts/apple/t6031-dcp.dtsi b/arch/arm64/boot/dts/apple/t6031-dcp.dtsi index f679c57b97709c..a4f70fb8dbb37c 100644 --- a/arch/arm64/boot/dts/apple/t6031-dcp.dtsi +++ b/arch/arm64/boot/dts/apple/t6031-dcp.dtsi @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0+ OR MIT /* - * T6031 internal display. + * T6031 internal display and the external display processors. * * The same description as t6030-dcp.dtsi: the DCP, its mailbox, the two * display DARTs, the display subsystem and its PIODMA stream. The T6031 @@ -24,6 +24,10 @@ aliases { /* The boot loader finds the display nodes through these. */ dcp = &dcp; + dcpext0 = &dcpext0; + dcpext1 = &dcpext1; + dcpext2 = &dcpext2; + dcpext3 = &dcpext3; disp0 = &display; disp0-piodma = &disp0_piodma; disp0_piodma = &disp0_piodma; @@ -115,6 +119,412 @@ }; }; + /* + * The external display processors dcpext0..3 (ADT dcpextN, + * dart-dcpextN): T6030's dcpext description at the J516C ADT's + * addresses. Each sits on its own dispextN_fe power state, the child + * of dispextN_cpu, as the internal DCP sits on disp_fe. Their display + * DARTs (dart-dispextN) and scanout children are added by the boot + * loader's dispext handoff. Every node stays disabled unless the display + * gate enables it: after the boot loader's T6030-style handoff markers + * and with apple_t6031_display.dcpext=1. + */ + + dcpext0_dart: iommu@30530c000 { + compatible = "apple,t6031-dart", "apple,t8110-dart"; + reg = <0x3 0x0530c000 0x0 0x4000>; + interrupt-parent = <&aic>; + interrupts = ; + #iommu-cells = <1>; + apple,dma-range = <0x100 0x0 0x10 0x0>; + power-domains = <&ps_dispext0_fe>; + status = "disabled"; + }; + + dcpext0_mbox: mbox@306c08000 { + compatible = "apple,t6031-asc-mailbox", "apple,asc-mailbox-v4"; + reg = <0x3 0x06c08000 0x0 0x4000>; + interrupt-parent = <&aic>; + interrupts = , + , + , + ; + interrupt-names = "send-empty", "send-not-empty", + "recv-empty", "recv-not-empty"; + #mbox-cells = <0>; + power-domains = <&ps_dispext0_fe>; + resets = <&ps_dispext0_cpu>; + status = "disabled"; + }; + + dcpext0: dcp@306c00000 { + compatible = "apple,t6031-dcpext", "apple,t6030-dcpext", "apple,dcpext"; + reg = <0x3 0x06c00000 0x0 0x4000>; + reg-names = "coproc"; + mboxes = <&dcpext0_mbox>; + mbox-names = "mbox"; + iommus = <&dcpext0_dart 5>; + power-domains = <&ps_dispext0_fe>; + resets = <&ps_dispext0_cpu>; + phys = <&atcphy0 PHY_TYPE_DP>, <&atcphy1 PHY_TYPE_DP>, + <&atcphy2 PHY_TYPE_DP>; + phy-names = "typec0", "typec1", "typec2"; + mux-controls = <&atcphy0_xbar 0>, <&atcphy1_xbar 0>, + <&atcphy2_xbar 0>, + <&atcphy0_xbar 1>, <&atcphy0_xbar 2>, + <&atcphy1_xbar 1>, <&atcphy1_xbar 2>, + <&atcphy2_xbar 1>, <&atcphy2_xbar 2>; + mux-control-names = "typec0", "typec1", "typec2", + "typec0-dpin0", "typec0-dpin1", + "typec1-dpin0", "typec1-dpin1", + "typec2-dpin0", "typec2-dpin1"; + apple,connector-type = "USB-C"; + apple,dcp-index = <1>; + apple,typec-dptx-phys = <0 1 2>; + /* The crossbar's upstream port N is dispextN (ADT display-crossbar0). */ + apple,typec-mux-indices = <0 0 0>; + #address-cells = <2>; + #size-cells = <2>; + ranges; + status = "disabled"; + + /* Enabled with this processor by the display gate, as on T6030. */ + typec-routes { + #address-cells = <1>; + #size-cells = <0>; + + route@0 { + reg = <0>; + mode-switch; + status = "disabled"; + + port { + dcpext0_typec0: endpoint { + remote-endpoint = <&typec0_dcpext0>; + }; + }; + }; + route@1 { + reg = <1>; + mode-switch; + status = "disabled"; + + port { + dcpext0_typec1: endpoint { + remote-endpoint = <&typec1_dcpext0>; + }; + }; + }; + route@2 { + reg = <2>; + mode-switch; + status = "disabled"; + + port { + dcpext0_typec2: endpoint { + remote-endpoint = <&typec2_dcpext0>; + }; + }; + }; + }; + }; + + dcpext1_dart: iommu@44930c000 { + compatible = "apple,t6031-dart", "apple,t8110-dart"; + reg = <0x4 0x4930c000 0x0 0x4000>; + interrupt-parent = <&aic>; + interrupts = ; + #iommu-cells = <1>; + apple,dma-range = <0x100 0x0 0x10 0x0>; + power-domains = <&ps_dispext1_fe>; + status = "disabled"; + }; + + dcpext1_mbox: mbox@44ac08000 { + compatible = "apple,t6031-asc-mailbox", "apple,asc-mailbox-v4"; + reg = <0x4 0x4ac08000 0x0 0x4000>; + interrupt-parent = <&aic>; + interrupts = , + , + , + ; + interrupt-names = "send-empty", "send-not-empty", + "recv-empty", "recv-not-empty"; + #mbox-cells = <0>; + power-domains = <&ps_dispext1_fe>; + resets = <&ps_dispext1_cpu>; + status = "disabled"; + }; + + dcpext1: dcp@44ac00000 { + compatible = "apple,t6031-dcpext", "apple,t6030-dcpext", "apple,dcpext"; + reg = <0x4 0x4ac00000 0x0 0x4000>; + reg-names = "coproc"; + mboxes = <&dcpext1_mbox>; + mbox-names = "mbox"; + iommus = <&dcpext1_dart 5>; + power-domains = <&ps_dispext1_fe>; + resets = <&ps_dispext1_cpu>; + phys = <&atcphy0 PHY_TYPE_DP>, <&atcphy1 PHY_TYPE_DP>, + <&atcphy2 PHY_TYPE_DP>; + phy-names = "typec0", "typec1", "typec2"; + mux-controls = <&atcphy0_xbar 0>, <&atcphy1_xbar 0>, + <&atcphy2_xbar 0>, + <&atcphy0_xbar 1>, <&atcphy0_xbar 2>, + <&atcphy1_xbar 1>, <&atcphy1_xbar 2>, + <&atcphy2_xbar 1>, <&atcphy2_xbar 2>; + mux-control-names = "typec0", "typec1", "typec2", + "typec0-dpin0", "typec0-dpin1", + "typec1-dpin0", "typec1-dpin1", + "typec2-dpin0", "typec2-dpin1"; + apple,connector-type = "USB-C"; + apple,dcp-index = <2>; + apple,typec-dptx-phys = <0 1 2>; + /* The crossbar's upstream port N is dispextN (ADT display-crossbar0). */ + apple,typec-mux-indices = <1 1 1>; + #address-cells = <2>; + #size-cells = <2>; + ranges; + status = "disabled"; + + /* Enabled with this processor by the display gate, as on T6030. */ + typec-routes { + #address-cells = <1>; + #size-cells = <0>; + + route@0 { + reg = <0>; + mode-switch; + status = "disabled"; + + port { + dcpext1_typec0: endpoint { + remote-endpoint = <&typec0_dcpext1>; + }; + }; + }; + route@1 { + reg = <1>; + mode-switch; + status = "disabled"; + + port { + dcpext1_typec1: endpoint { + remote-endpoint = <&typec1_dcpext1>; + }; + }; + }; + route@2 { + reg = <2>; + mode-switch; + status = "disabled"; + + port { + dcpext1_typec2: endpoint { + remote-endpoint = <&typec2_dcpext1>; + }; + }; + }; + }; + }; + + dcpext2_dart: iommu@44d30c000 { + compatible = "apple,t6031-dart", "apple,t8110-dart"; + reg = <0x4 0x4d30c000 0x0 0x4000>; + interrupt-parent = <&aic>; + interrupts = ; + #iommu-cells = <1>; + apple,dma-range = <0x100 0x0 0x10 0x0>; + power-domains = <&ps_dispext2_fe>; + status = "disabled"; + }; + + dcpext2_mbox: mbox@44ec08000 { + compatible = "apple,t6031-asc-mailbox", "apple,asc-mailbox-v4"; + reg = <0x4 0x4ec08000 0x0 0x4000>; + interrupt-parent = <&aic>; + interrupts = , + , + , + ; + interrupt-names = "send-empty", "send-not-empty", + "recv-empty", "recv-not-empty"; + #mbox-cells = <0>; + power-domains = <&ps_dispext2_fe>; + resets = <&ps_dispext2_cpu>; + status = "disabled"; + }; + + dcpext2: dcp@44ec00000 { + compatible = "apple,t6031-dcpext", "apple,t6030-dcpext", "apple,dcpext"; + reg = <0x4 0x4ec00000 0x0 0x4000>; + reg-names = "coproc"; + mboxes = <&dcpext2_mbox>; + mbox-names = "mbox"; + iommus = <&dcpext2_dart 5>; + power-domains = <&ps_dispext2_fe>; + resets = <&ps_dispext2_cpu>; + phys = <&atcphy0 PHY_TYPE_DP>, <&atcphy1 PHY_TYPE_DP>, + <&atcphy2 PHY_TYPE_DP>; + phy-names = "typec0", "typec1", "typec2"; + mux-controls = <&atcphy0_xbar 0>, <&atcphy1_xbar 0>, + <&atcphy2_xbar 0>, + <&atcphy0_xbar 1>, <&atcphy0_xbar 2>, + <&atcphy1_xbar 1>, <&atcphy1_xbar 2>, + <&atcphy2_xbar 1>, <&atcphy2_xbar 2>; + mux-control-names = "typec0", "typec1", "typec2", + "typec0-dpin0", "typec0-dpin1", + "typec1-dpin0", "typec1-dpin1", + "typec2-dpin0", "typec2-dpin1"; + apple,connector-type = "USB-C"; + apple,dcp-index = <3>; + apple,typec-dptx-phys = <0 1 2>; + /* The crossbar's upstream port N is dispextN (ADT display-crossbar0). */ + apple,typec-mux-indices = <2 2 2>; + #address-cells = <2>; + #size-cells = <2>; + ranges; + status = "disabled"; + + /* Enabled with this processor by the display gate, as on T6030. */ + typec-routes { + #address-cells = <1>; + #size-cells = <0>; + + route@0 { + reg = <0>; + mode-switch; + status = "disabled"; + + port { + dcpext2_typec0: endpoint { + remote-endpoint = <&typec0_dcpext2>; + }; + }; + }; + route@1 { + reg = <1>; + mode-switch; + status = "disabled"; + + port { + dcpext2_typec1: endpoint { + remote-endpoint = <&typec1_dcpext2>; + }; + }; + }; + route@2 { + reg = <2>; + mode-switch; + status = "disabled"; + + port { + dcpext2_typec2: endpoint { + remote-endpoint = <&typec2_dcpext2>; + }; + }; + }; + }; + }; + + dcpext3_dart: iommu@2c930c000 { + compatible = "apple,t6031-dart", "apple,t8110-dart"; + reg = <0x2 0xc930c000 0x0 0x4000>; + interrupt-parent = <&aic>; + interrupts = ; + #iommu-cells = <1>; + apple,dma-range = <0x100 0x0 0x10 0x0>; + power-domains = <&ps_dispext3_fe>; + status = "disabled"; + }; + + dcpext3_mbox: mbox@2cac08000 { + compatible = "apple,t6031-asc-mailbox", "apple,asc-mailbox-v4"; + reg = <0x2 0xcac08000 0x0 0x4000>; + interrupt-parent = <&aic>; + interrupts = , + , + , + ; + interrupt-names = "send-empty", "send-not-empty", + "recv-empty", "recv-not-empty"; + #mbox-cells = <0>; + power-domains = <&ps_dispext3_fe>; + resets = <&ps_dispext3_cpu>; + status = "disabled"; + }; + + dcpext3: dcp@2cac00000 { + compatible = "apple,t6031-dcpext", "apple,t6030-dcpext", "apple,dcpext"; + reg = <0x2 0xcac00000 0x0 0x4000>; + reg-names = "coproc"; + mboxes = <&dcpext3_mbox>; + mbox-names = "mbox"; + iommus = <&dcpext3_dart 5>; + power-domains = <&ps_dispext3_fe>; + resets = <&ps_dispext3_cpu>; + phys = <&atcphy0 PHY_TYPE_DP>, <&atcphy1 PHY_TYPE_DP>, + <&atcphy2 PHY_TYPE_DP>; + phy-names = "typec0", "typec1", "typec2"; + mux-controls = <&atcphy0_xbar 0>, <&atcphy1_xbar 0>, + <&atcphy2_xbar 0>, + <&atcphy0_xbar 1>, <&atcphy0_xbar 2>, + <&atcphy1_xbar 1>, <&atcphy1_xbar 2>, + <&atcphy2_xbar 1>, <&atcphy2_xbar 2>; + mux-control-names = "typec0", "typec1", "typec2", + "typec0-dpin0", "typec0-dpin1", + "typec1-dpin0", "typec1-dpin1", + "typec2-dpin0", "typec2-dpin1"; + apple,connector-type = "USB-C"; + apple,dcp-index = <4>; + apple,typec-dptx-phys = <0 1 2>; + /* The crossbar's upstream port N is dispextN (ADT display-crossbar0). */ + apple,typec-mux-indices = <3 3 3>; + #address-cells = <2>; + #size-cells = <2>; + ranges; + status = "disabled"; + + /* Enabled with this processor by the display gate, as on T6030. */ + typec-routes { + #address-cells = <1>; + #size-cells = <0>; + + route@0 { + reg = <0>; + mode-switch; + status = "disabled"; + + port { + dcpext3_typec0: endpoint { + remote-endpoint = <&typec0_dcpext3>; + }; + }; + }; + route@1 { + reg = <1>; + mode-switch; + status = "disabled"; + + port { + dcpext3_typec1: endpoint { + remote-endpoint = <&typec1_dcpext3>; + }; + }; + }; + route@2 { + reg = <2>; + mode-switch; + status = "disabled"; + + port { + dcpext3_typec2: endpoint { + remote-endpoint = <&typec2_dcpext3>; + }; + }; + }; + }; + }; }; /* diff --git a/arch/arm64/boot/dts/apple/t6031-die0.dtsi b/arch/arm64/boot/dts/apple/t6031-die0.dtsi index 47d868a323e8c9..5eb20297b084b7 100644 --- a/arch/arm64/boot/dts/apple/t6031-die0.dtsi +++ b/arch/arm64/boot/dts/apple/t6031-die0.dtsi @@ -78,6 +78,10 @@ label = "pmp-dispext0"; #power-domain-cells = <0>; power-domains = <&ps_dispext0_fe>; + /* As T6030's: enabled only by the dcpext gate, before the PMP starts. */ + apple,always-on; + /* A missing external acknowledgement must not block the internal panel. */ + apple,defer-startup-ack; status = "disabled"; }; @@ -88,6 +92,10 @@ label = "pmp-dispext1"; #power-domain-cells = <0>; power-domains = <&ps_dispext1_fe>; + /* As T6030's: enabled only by the dcpext gate, before the PMP starts. */ + apple,always-on; + /* A missing external acknowledgement must not block the internal panel. */ + apple,defer-startup-ack; status = "disabled"; }; @@ -98,6 +106,10 @@ label = "pmp-dispext2"; #power-domain-cells = <0>; power-domains = <&ps_dispext2_fe>; + /* As T6030's: enabled only by the dcpext gate, before the PMP starts. */ + apple,always-on; + /* A missing external acknowledgement must not block the internal panel. */ + apple,defer-startup-ack; status = "disabled"; }; @@ -108,6 +120,10 @@ label = "pmp-dispext3"; #power-domain-cells = <0>; power-domains = <&ps_dispext3_fe>; + /* As T6030's: enabled only by the dcpext gate, before the PMP starts. */ + apple,always-on; + /* A missing external acknowledgement must not block the internal panel. */ + apple,defer-startup-ack; status = "disabled"; }; diff --git a/arch/arm64/boot/dts/apple/t6031-dieX.dtsi b/arch/arm64/boot/dts/apple/t6031-dieX.dtsi index 37fc4d2001d601..cfea5f134cdbe0 100644 --- a/arch/arm64/boot/dts/apple/t6031-dieX.dtsi +++ b/arch/arm64/boot/dts/apple/t6031-dieX.dtsi @@ -354,6 +354,15 @@ power-domains = <&DIE_NODE(ps_atc0_usb)>; }; + /* At the ADT's atc0-dpxbar; routes the dispext pipes to ATC0's DP outputs. */ + DIE_NODE(atcphy0_xbar): mux@70304c000 { + compatible = "apple,t6031-display-crossbar", + "apple,t6020-display-crossbar"; + reg = <0x7 0x0304c000 0x0 0x4000>; + #mux-control-cells = <1>; + power-domains = <&DIE_NODE(ps_atc0_usb)>; + }; + DIE_NODE(usb4_1_mbox): mailbox@b01108000 { compatible = "apple,t6031-asc-mailbox", "apple,asc-mailbox-v4"; @@ -521,6 +530,15 @@ power-domains = <&DIE_NODE(ps_atc1_usb)>; }; + /* At the ADT's atc1-dpxbar; routes the dispext pipes to ATC1's DP outputs. */ + DIE_NODE(atcphy1_xbar): mux@b0304c000 { + compatible = "apple,t6031-display-crossbar", + "apple,t6020-display-crossbar"; + reg = <0xb 0x0304c000 0x0 0x4000>; + #mux-control-cells = <1>; + power-domains = <&DIE_NODE(ps_atc1_usb)>; + }; + DIE_NODE(usb4_2_mbox): mailbox@f01108000 { compatible = "apple,t6031-asc-mailbox", "apple,asc-mailbox-v4"; @@ -687,4 +705,13 @@ mode-switch; power-domains = <&DIE_NODE(ps_atc2_usb)>; }; + + /* At the ADT's atc2-dpxbar; routes the dispext pipes to ATC2's DP outputs. */ + DIE_NODE(atcphy2_xbar): mux@f0304c000 { + compatible = "apple,t6031-display-crossbar", + "apple,t6020-display-crossbar"; + reg = <0xf 0x0304c000 0x0 0x4000>; + #mux-control-cells = <1>; + power-domains = <&DIE_NODE(ps_atc2_usb)>; + }; }; diff --git a/arch/arm64/boot/dts/apple/t6031-j514c.dts b/arch/arm64/boot/dts/apple/t6031-j514c.dts index f4f282e9d32072..2a56d2338fa9a3 100644 --- a/arch/arm64/boot/dts/apple/t6031-j514c.dts +++ b/arch/arm64/boot/dts/apple/t6031-j514c.dts @@ -12,6 +12,7 @@ #include "t6031.dtsi" #include "t6031-dcp.dtsi" #include "t603x-j514-j516.dtsi" +#include "t603x-j514c-j516c-dcpext.dtsi" / { compatible = "apple,j514c", "apple,t6031", "apple,arm-platform"; diff --git a/arch/arm64/boot/dts/apple/t6031-j516c.dts b/arch/arm64/boot/dts/apple/t6031-j516c.dts index 562491755117f1..ca9b437c9a9c0f 100644 --- a/arch/arm64/boot/dts/apple/t6031-j516c.dts +++ b/arch/arm64/boot/dts/apple/t6031-j516c.dts @@ -12,6 +12,7 @@ #include "t6031.dtsi" #include "t6031-dcp.dtsi" #include "t603x-j514-j516.dtsi" +#include "t603x-j514c-j516c-dcpext.dtsi" / { compatible = "apple,j516c", "apple,t6031", "apple,arm-platform"; diff --git a/arch/arm64/boot/dts/apple/t603x-j514c-j516c-dcpext.dtsi b/arch/arm64/boot/dts/apple/t603x-j514c-j516c-dcpext.dtsi new file mode 100644 index 00000000000000..965aa91750a02d --- /dev/null +++ b/arch/arm64/boot/dts/apple/t603x-j514c-j516c-dcpext.dtsi @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-2.0+ OR MIT +/* + * The USB-C connectors' display endpoints on the M3 Max MacBook Pros (J514C, + * J516C): each Type-C port can be fed by any of dcpext0..3, as on the + * J514S/J516S (t603x-j514s-j516s-usb4.dtsi). The routes they point at stay + * disabled until the display gate enables their processor. + */ + +&typec0 { + ports { + port@3 { + reg = <3>; + #address-cells = <1>; + #size-cells = <0>; + + typec0_dcpext0: endpoint@0 { + reg = <0>; + remote-endpoint = <&dcpext0_typec0>; + }; + + typec0_dcpext1: endpoint@1 { + reg = <1>; + remote-endpoint = <&dcpext1_typec0>; + }; + + typec0_dcpext2: endpoint@2 { + reg = <2>; + remote-endpoint = <&dcpext2_typec0>; + }; + + typec0_dcpext3: endpoint@3 { + reg = <3>; + remote-endpoint = <&dcpext3_typec0>; + }; + }; + }; +}; +&typec1 { + ports { + port@3 { + reg = <3>; + #address-cells = <1>; + #size-cells = <0>; + + typec1_dcpext0: endpoint@0 { + reg = <0>; + remote-endpoint = <&dcpext0_typec1>; + }; + + typec1_dcpext1: endpoint@1 { + reg = <1>; + remote-endpoint = <&dcpext1_typec1>; + }; + + typec1_dcpext2: endpoint@2 { + reg = <2>; + remote-endpoint = <&dcpext2_typec1>; + }; + + typec1_dcpext3: endpoint@3 { + reg = <3>; + remote-endpoint = <&dcpext3_typec1>; + }; + }; + }; +}; +&typec2 { + ports { + port@3 { + reg = <3>; + #address-cells = <1>; + #size-cells = <0>; + + typec2_dcpext0: endpoint@0 { + reg = <0>; + remote-endpoint = <&dcpext0_typec2>; + }; + + typec2_dcpext1: endpoint@1 { + reg = <1>; + remote-endpoint = <&dcpext1_typec2>; + }; + + typec2_dcpext2: endpoint@2 { + reg = <2>; + remote-endpoint = <&dcpext2_typec2>; + }; + + typec2_dcpext3: endpoint@3 { + reg = <3>; + remote-endpoint = <&dcpext3_typec2>; + }; + }; + }; +}; From 2f4cbacb2087ce3054b82dae441116eb04a4d35b Mon Sep 17 00:00:00 2001 From: DB Date: Thu, 29 Oct 2026 19:21:38 +0000 Subject: [PATCH 04/22] soc: apple: display-gate: hand over T6031 external display processors Enable the dcpext gate on T6031 behind its own switch, apple_t6031_display.dcpext=1, which is off by default. Without it, or without the boot loader's handoff markers, every processor and its Type-C routes stay disabled. The gate's dcpext path assumed T6030's power order: the processor on dispextN_cpu, the child of dispextN_fe. On T6031 the processor is on dispextN_fe, the child of dispextN_cpu. Resolve the chain in the SoC's order, and check the mailbox, firmware DART and display DART against the processor's own power state, which also gets the active floor (its parents follow it). The boot loader's markers stay the T6030 ones (apple,t6030-dcpext-memory-ready, apple,t6030-dispext-handoff), so the display driver's checks need no change. Co-Authored-By: Claude Opus 5.5 Signed-off-by: DB --- drivers/soc/apple/t6030-display-gate.c | 83 ++++++++++++++++++++------ 1 file changed, 66 insertions(+), 17 deletions(-) diff --git a/drivers/soc/apple/t6030-display-gate.c b/drivers/soc/apple/t6030-display-gate.c index 7ee02087c969b8..fcdd94fc404c00 100644 --- a/drivers/soc/apple/t6030-display-gate.c +++ b/drivers/soc/apple/t6030-display-gate.c @@ -154,6 +154,15 @@ static const char *gate_t6031_board __initdata = "apple,j516c"; /* The resolved request, set by gate_run() on a T6031. */ static bool gate_t6031_requested __initdata; +/* apple_t6030_display.dcpext: T6030's external processors, on unless 0. */ +static bool gate_dcpext_requested __initdata = true; + +/* + * apple_t6031_display.dcpext: T6031's external processors, off unless 1. + * Their addresses come from one J516C ADT and nothing has driven them yet. + */ +static bool gate_t6031_dcpext_requested __initdata; + /* Whether the T6031 native display is requested: an explicit 0 or 1 wins. */ static bool __init gate_t6031_wanted(int sw, bool default_board) { @@ -197,6 +206,14 @@ struct gate_soc { const char *fw_uuids; /* /chosen strings naming each firmware image */ bool quiet_without_nodes; /* no display nodes in the device tree is normal */ bool dcpext; /* the external display processors and their DARTs */ + const char *dcpext_compat; /* their compatible */ + bool *dcpext_requested; /* their command line switch */ + /* + * Their own power state is dispextN_fe, the child of dispextN_cpu + * (T6031), rather than dispextN_cpu, the child of dispextN_fe (T6030). + */ + bool dcpext_fe_first; + const char *dcpext_opt_in; /* set: off unless this is on the command line */ }; static const struct gate_soc gate_t6030 __initconst = { @@ -220,6 +237,8 @@ static const struct gate_soc gate_t6030 __initconst = { .requested = &gate_requested, .ps_chain = { PMP_PS_DISP_CPU, PMP_PS_DISP_FE, PMP_PS_DISP_SYS }, .dcpext = true, + .dcpext_compat = "apple,t6030-dcpext", + .dcpext_requested = &gate_dcpext_requested, }; /* @@ -282,6 +301,15 @@ static const struct gate_soc gate_t6031 __initconst = { .pmp_ps_any_pmgr = true, .describe = true, .fw_uuids = "asahi,t6031-fw-uuids", + /* + * dcpext0..3 as on T6030, handed over by the boot loader with the T6030 + * markers, each on its own dispextN_fe power state. + */ + .dcpext = true, + .dcpext_compat = "apple,t6031-dcpext", + .dcpext_requested = &gate_t6031_dcpext_requested, + .dcpext_fe_first = true, + .dcpext_opt_in = "apple_t6031_display.dcpext=1", }; static const struct gate_soc *const gate_socs[] __initconst = { @@ -1047,14 +1075,18 @@ static int __init gate_pmp_apply(struct device_node *dcp, struct device_node **p * apple_t6030_display.scanout=0 refuses the display DART, which the native * path needs. */ -static bool gate_dcpext_requested __initdata = true; - static int __init gate_dcpext_setup(char *arg) { return kstrtobool(arg, &gate_dcpext_requested); } early_param("apple_t6030_display.dcpext", gate_dcpext_setup); +static int __init gate_t6031_dcpext_setup(char *arg) +{ + return kstrtobool(arg, &gate_t6031_dcpext_requested); +} +early_param("apple_t6031_display.dcpext", gate_t6031_dcpext_setup); + static bool gate_scanout_requested __initdata = true; static int __init gate_scanout_setup(char *arg) @@ -1077,6 +1109,7 @@ early_param("apple_t6030_display.dcpext_manual", gate_dcpext_manual_setup); struct gate_ext { struct device_node *dcp, *dart, *mbox; struct device_node *cpu, *fe, *sys; + struct device_node *own; /* cpu or fe: the processor's own power state, not counted */ struct device_node *entry; /* its PMP request, pmp-dispextN */ struct device_node *scanout; /* the boot loader's scanout child */ struct device_node *disp_dart; /* the display DART that child names */ @@ -1223,9 +1256,9 @@ static int __init gate_dispext_check(struct gate_ext *ext) state[0] != 0 || state[3] != 4 || of_address_to_resource(dart, 0, ®s) || resource_size(®s) != SZ_16K) goto out; - /* The DART belongs to this processor's pipe: it shares its CPU domain. */ + /* The DART belongs to this processor's pipe: it shares its power state. */ region = gate_ps_parent(dart); - if (region != ext->cpu) + if (region != ext->own) goto out; of_node_put(region); region = NULL; @@ -1296,19 +1329,31 @@ static int __init gate_ext_resolve(struct gate_ext *ext) return -EINVAL; /* Do not enable a CPU whose power ownership is not fully described. */ - ext->cpu = gate_ps_parent(ext->dcp); - if (!gate_ps_dispext(ext->cpu, "cpu", &ext->index)) - return -EINVAL; - ext->fe = gate_ps_parent(ext->cpu); - if (!gate_ps_dispext(ext->fe, "fe", &ext->index)) - return -EINVAL; - ext->sys = gate_ps_parent(ext->fe); + if (gate_soc->dcpext_fe_first) { + ext->fe = gate_ps_parent(ext->dcp); + if (!gate_ps_dispext(ext->fe, "fe", &ext->index)) + return -EINVAL; + ext->cpu = gate_ps_parent(ext->fe); + if (!gate_ps_dispext(ext->cpu, "cpu", &ext->index)) + return -EINVAL; + ext->sys = gate_ps_parent(ext->cpu); + ext->own = ext->fe; + } else { + ext->cpu = gate_ps_parent(ext->dcp); + if (!gate_ps_dispext(ext->cpu, "cpu", &ext->index)) + return -EINVAL; + ext->fe = gate_ps_parent(ext->cpu); + if (!gate_ps_dispext(ext->fe, "fe", &ext->index)) + return -EINVAL; + ext->sys = gate_ps_parent(ext->fe); + ext->own = ext->cpu; + } if (!gate_ps_dispext(ext->sys, "sys", &ext->index)) return -EINVAL; for (i = 0; i < 2; i++) { domain = gate_ps_parent(i ? ext->mbox : ext->dart); of_node_put(domain); - if (domain != ext->cpu) + if (domain != ext->own) return -EINVAL; } @@ -1456,8 +1501,11 @@ static int __init gate_ext_apply(struct gate_ext *ext, bool native) if (!cs) return -ENOMEM; - /* PMGR applies the floor at probe, before the PMP can manage this CPU. */ - ret = gate_set_u32(cs, ext->cpu, "apple,min-state", PMGR_PS_ACTIVE); + /* + * PMGR applies the floor at probe, before the PMP can manage this CPU. + * On the processor's own power state, which keeps its parents on. + */ + ret = gate_set_u32(cs, ext->own, "apple,min-state", PMGR_PS_ACTIVE); /* The driver must wait for this request's acknowledgement before RUN. */ if (!ret) ret = gate_set_u32(cs, ext->dcp, "apple,pmp-report", ext->entry->phandle); @@ -1583,9 +1631,10 @@ static void __init gate_dcpext(const char *refuse) { struct device_node *np; - if (!refuse && !gate_dcpext_requested) - refuse = "disabled on the command line"; - for_each_compatible_node(np, NULL, "apple,t6030-dcpext") + if (!refuse && !*gate_soc->dcpext_requested) + refuse = gate_soc->dcpext_opt_in ? + "not requested on the command line" : "disabled on the command line"; + for_each_compatible_node(np, NULL, gate_soc->dcpext_compat) gate_dcpext_one(np, refuse); } From e21ed7f8a05b56d831b86f8ae6f31f608f5a7100 Mon Sep 17 00:00:00 2001 From: DB Date: Thu, 29 Oct 2026 19:21:38 +0000 Subject: [PATCH 05/22] drm: apple: run T6031 external processors on the J516C 14.7 image A T6031 dcpext loads the same DCP image as its internal DCP (F493F03E-CC40-3D89-9A08-5F259BA2B246, per the J516C ADT's iop nubs). Accept that image on apple,t6031-dcpext nodes only; every other external processor still needs the T6030 image. Also give T6031 the T6030 display-crossbar programming. Both use the same dispext pipes and the same t602x crossbar, and the T6030 path handles dispext bits 0..3. Co-Authored-By: Claude Opus 5.5 Signed-off-by: DB --- drivers/gpu/drm/apple/iomfb_v14_7.c | 9 +++++++-- drivers/mux/apple-display-crossbar.c | 4 +++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/drivers/gpu/drm/apple/iomfb_v14_7.c b/drivers/gpu/drm/apple/iomfb_v14_7.c index d55f96b2ba123f..12d2c3b7bb561e 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7.c +++ b/drivers/gpu/drm/apple/iomfb_v14_7.c @@ -2021,9 +2021,14 @@ int iomfb_v14_7_external_prepare(struct apple_dcp *dcp) dcp->coproc_reg = NULL; goto refuse; } - /* The callbacks here are those of one firmware image. */ + /* + * The callbacks here are those of one firmware image: T6030's, or on a + * T6031 external processor the J516C image its internal DCP runs. + */ if (of_property_read_string(dev->of_node, "apple,firmware-uuid", &uuid) || - strcmp(uuid, DCP_V14_FIRMWARE_UUID)) { + (strcmp(uuid, DCP_V14_FIRMWARE_UUID) && + (!of_device_is_compatible(dev->of_node, "apple,t6031-dcpext") || + strcmp(uuid, DCP_V14_J516C_FIRMWARE_UUID)))) { dev_err(dev, "external display processor firmware %s is not supported\n", uuid ?: "(unknown)"); ret = -ENODEV; diff --git a/drivers/mux/apple-display-crossbar.c b/drivers/mux/apple-display-crossbar.c index a02b8d9fe8a304..ff5803453abee4 100644 --- a/drivers/mux/apple-display-crossbar.c +++ b/drivers/mux/apple-display-crossbar.c @@ -1003,7 +1003,9 @@ static int apple_dpxbar_probe(struct platform_device *pdev) spin_lock_init(&dpxbar->lock); dpxbar->dev = dev; - dpxbar->t6030_dpin = of_machine_is_compatible("apple,t6030"); + /* T6031 (M3 Max) has T6030's dispext pipes and the same crossbar. */ + dpxbar->t6030_dpin = of_machine_is_compatible("apple,t6030") || + of_machine_is_compatible("apple,t6031"); dpxbar->regs = devm_platform_ioremap_resource(pdev, 0); if (IS_ERR(dpxbar->regs)) return PTR_ERR(dpxbar->regs); From 783a8e1273d5316d413826a5ec134a65b28197bf Mon Sep 17 00:00:00 2001 From: DB Date: Fri, 30 Oct 2026 05:38:54 +0000 Subject: [PATCH 06/22] arm64: dts: apple: t6031: link each dcpext to its PMP DISPEXT request The display gate needs the pmp-dispextN report entry to have a phandle, and dtc emits one only for referenced nodes. Name the entry in apple,pmp-report, as T8122 does; the gate sets the same value. Co-Authored-By: Claude Opus 5.5 Signed-off-by: DB --- arch/arm64/boot/dts/apple/t6031-dcp.dtsi | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/arch/arm64/boot/dts/apple/t6031-dcp.dtsi b/arch/arm64/boot/dts/apple/t6031-dcp.dtsi index a4f70fb8dbb37c..330836e0a52b8d 100644 --- a/arch/arm64/boot/dts/apple/t6031-dcp.dtsi +++ b/arch/arm64/boot/dts/apple/t6031-dcp.dtsi @@ -166,6 +166,8 @@ iommus = <&dcpext0_dart 5>; power-domains = <&ps_dispext0_fe>; resets = <&ps_dispext0_cpu>; + /* Linked here so dtc gives the PMP request entry a phandle. */ + apple,pmp-report = <&pmp_report_dispext0>; phys = <&atcphy0 PHY_TYPE_DP>, <&atcphy1 PHY_TYPE_DP>, <&atcphy2 PHY_TYPE_DP>; phy-names = "typec0", "typec1", "typec2"; @@ -265,6 +267,8 @@ iommus = <&dcpext1_dart 5>; power-domains = <&ps_dispext1_fe>; resets = <&ps_dispext1_cpu>; + /* Linked here so dtc gives the PMP request entry a phandle. */ + apple,pmp-report = <&pmp_report_dispext1>; phys = <&atcphy0 PHY_TYPE_DP>, <&atcphy1 PHY_TYPE_DP>, <&atcphy2 PHY_TYPE_DP>; phy-names = "typec0", "typec1", "typec2"; @@ -364,6 +368,8 @@ iommus = <&dcpext2_dart 5>; power-domains = <&ps_dispext2_fe>; resets = <&ps_dispext2_cpu>; + /* Linked here so dtc gives the PMP request entry a phandle. */ + apple,pmp-report = <&pmp_report_dispext2>; phys = <&atcphy0 PHY_TYPE_DP>, <&atcphy1 PHY_TYPE_DP>, <&atcphy2 PHY_TYPE_DP>; phy-names = "typec0", "typec1", "typec2"; @@ -463,6 +469,8 @@ iommus = <&dcpext3_dart 5>; power-domains = <&ps_dispext3_fe>; resets = <&ps_dispext3_cpu>; + /* Linked here so dtc gives the PMP request entry a phandle. */ + apple,pmp-report = <&pmp_report_dispext3>; phys = <&atcphy0 PHY_TYPE_DP>, <&atcphy1 PHY_TYPE_DP>, <&atcphy2 PHY_TYPE_DP>; phy-names = "typec0", "typec1", "typec2"; From 356aa3a98de1c3bd895ee37a968b48e132085ecf Mon Sep 17 00:00:00 2001 From: DB Date: Fri, 30 Oct 2026 05:45:29 +0000 Subject: [PATCH 07/22] drm: apple: find the T6031 display subsystem for a native dcpext dcp_t6030_ext_native() looked only for apple,t6030-display-subsystem, so a T6031 dcpext the gate had handed its display DART fell back to the manual diagnostic path and never joined the DRM device. Look up the subsystem of the processor's own SoC. Co-Authored-By: Claude Opus 5.5 Signed-off-by: DB --- drivers/gpu/drm/apple/dcp.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 14d3bdf6403357..b3fcd23b4d4bd9 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -1403,7 +1403,11 @@ bool dcp_t6030_ext_native(const struct device_node *np) of_node_put(piodma); if (dart.args_count != 1 || dart.args[0] != 4 || !of_device_is_available(dart.np)) goto out; - display = of_find_compatible_node(NULL, NULL, "apple,t6030-display-subsystem"); + /* The display subsystem of the processor's own SoC. */ + display = of_find_compatible_node(NULL, NULL, + of_device_is_compatible(np, "apple,t6031-dcpext") ? + "apple,t6031-display-subsystem" : + "apple,t6030-display-subsystem"); n = display && of_device_is_available(display) ? of_count_phandle_with_args(display, "iommus", "#iommu-cells") : 0; for (i = 0; i < n && !native; i++) { From ba344e7c2e50884de23e03d01cad08d5e3fe2a97 Mon Sep 17 00:00:00 2001 From: DB Date: Fri, 30 Oct 2026 05:54:56 +0000 Subject: [PATCH 08/22] drm: apple: check the T6031 dcpext floor on its own dispext_fe dcpext_check_power() required the processor's power domain to be dispext_cpu, as on T6030. On T6031 the dcpext sits on dispext_fe, whose parent is the CPU domain, and the display gate puts the active floor there, so startup was refused with -EINVAL on attach. Co-Authored-By: Claude Opus 5.5 Signed-off-by: DB --- drivers/gpu/drm/apple/iomfb_v14_7.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/drivers/gpu/drm/apple/iomfb_v14_7.c b/drivers/gpu/drm/apple/iomfb_v14_7.c index 12d2c3b7bb561e..2d68cd8743583e 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7.c +++ b/drivers/gpu/drm/apple/iomfb_v14_7.c @@ -2121,17 +2121,21 @@ static int dcpext_check_power(struct apple_dcp *dcp) struct device *dev = dcp->dev; struct device_node *ps, *entry; struct regmap *map; - const char *label; + const char *label, *suffix; u32 offset, value; int ret; ps = of_parse_phandle(dev->of_node, "power-domains", 0); if (!ps) return -ENODEV; - /* The CPU domain of this pipe: dispext_cpu. */ + /* + * The pipe's own domain, which carries the floor: dispext_cpu, or on + * T6031 dispext_fe, whose parent is the CPU domain. + */ + suffix = of_device_is_compatible(dev->of_node, "apple,t6031-dcpext") ? "_fe" : "_cpu"; ret = of_property_read_string(ps, "label", &label); - if (ret || !strstarts(label, "dispext") || strlen(label) < 4 || - strcmp(label + strlen(label) - 4, "_cpu")) { + if (ret || !strstarts(label, "dispext") || strlen(label) < strlen(suffix) || + strcmp(label + strlen(label) - strlen(suffix), suffix)) { of_node_put(ps); return -EINVAL; } From 703eecc6d06f6c5a6be1a44f22d2e5c2d970345c Mon Sep 17 00:00:00 2001 From: DB Date: Fri, 30 Oct 2026 06:01:00 +0000 Subject: [PATCH 09/22] apple: put the T6031 dcpext power floor on its CPU domain A T6031 dispext_fe power state does not hold the PMGR minimum-state field: with apple,min-state written it still reads 0xff, as the internal display's FE state does, while the CPU states keep it. Floor the CPU domain in the display gate (the same node as before on T6030), and have the driver check the floor there, reached through the FE domain the T6031 dcpext sits on. Co-Authored-By: Claude Opus 5.5 Signed-off-by: DB --- drivers/gpu/drm/apple/iomfb_v14_7.c | 27 ++++++++++++++++++-------- drivers/soc/apple/t6030-display-gate.c | 4 ++-- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/drivers/gpu/drm/apple/iomfb_v14_7.c b/drivers/gpu/drm/apple/iomfb_v14_7.c index 2d68cd8743583e..6f44cb699093bd 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7.c +++ b/drivers/gpu/drm/apple/iomfb_v14_7.c @@ -2115,13 +2115,22 @@ bool iomfb_v14_7_external_failed(struct apple_dcp *dcp) (dcp->v14 && READ_ONCE(dcp->v14->failed)); } +/* True if @ps is a power state labelled dispext. */ +static bool dcpext_ps_is(struct device_node *ps, const char *suffix) +{ + const char *label; + + return ps && !of_property_read_string(ps, "label", &label) && + strstarts(label, "dispext") && strlen(label) >= strlen(suffix) && + !strcmp(label + strlen(label) - strlen(suffix), suffix); +} + /* Refuse startup unless both the hardware floor and PMP vote are in place. */ static int dcpext_check_power(struct apple_dcp *dcp) { struct device *dev = dcp->dev; - struct device_node *ps, *entry; + struct device_node *ps, *fe, *entry; struct regmap *map; - const char *label, *suffix; u32 offset, value; int ret; @@ -2129,13 +2138,15 @@ static int dcpext_check_power(struct apple_dcp *dcp) if (!ps) return -ENODEV; /* - * The pipe's own domain, which carries the floor: dispext_cpu, or on - * T6031 dispext_fe, whose parent is the CPU domain. + * The floor is on the pipe's CPU domain, dispext_cpu. On T6031 the + * pipe sits on dispext_fe, whose parent is the CPU domain. */ - suffix = of_device_is_compatible(dev->of_node, "apple,t6031-dcpext") ? "_fe" : "_cpu"; - ret = of_property_read_string(ps, "label", &label); - if (ret || !strstarts(label, "dispext") || strlen(label) < strlen(suffix) || - strcmp(label + strlen(label) - strlen(suffix), suffix)) { + if (of_device_is_compatible(dev->of_node, "apple,t6031-dcpext")) { + fe = ps; + ps = dcpext_ps_is(fe, "_fe") ? of_parse_phandle(fe, "power-domains", 0) : NULL; + of_node_put(fe); + } + if (!dcpext_ps_is(ps, "_cpu")) { of_node_put(ps); return -EINVAL; } diff --git a/drivers/soc/apple/t6030-display-gate.c b/drivers/soc/apple/t6030-display-gate.c index fcdd94fc404c00..01c11de95e892c 100644 --- a/drivers/soc/apple/t6030-display-gate.c +++ b/drivers/soc/apple/t6030-display-gate.c @@ -1503,9 +1503,9 @@ static int __init gate_ext_apply(struct gate_ext *ext, bool native) return -ENOMEM; /* * PMGR applies the floor at probe, before the PMP can manage this CPU. - * On the processor's own power state, which keeps its parents on. + * On the CPU's power state: a T6031 FE state does not hold a floor. */ - ret = gate_set_u32(cs, ext->own, "apple,min-state", PMGR_PS_ACTIVE); + ret = gate_set_u32(cs, ext->cpu, "apple,min-state", PMGR_PS_ACTIVE); /* The driver must wait for this request's acknowledgement before RUN. */ if (!ret) ret = gate_set_u32(cs, ext->dcp, "apple,pmp-report", ext->entry->phandle); From 40abf9b4000bfff5ebdd20338917fab2e98952d7 Mon Sep 17 00:00:00 2001 From: DB Date: Fri, 30 Oct 2026 06:12:40 +0000 Subject: [PATCH 10/22] drm: apple: take a T6031 dcpext timing's colors from ColorElements The J516C 14.7 firmware on a T6031 external processor publishes every timing with an empty ColorModes array; the display's color modes are only in ColorElements, and each timing names the ones it excludes in UnsafeColorElementIDs and DSCRequiredColorElementIDs. Every timing so had no color mode, parse_mode() rejected all of them, and the connector stayed disconnected with HPD asserted. When an external timing has no color mode of its own, select one from the published ColorElements, less its excluded IDs, with the external SDR ranking (full-range SDR RGB, 8 or 10 bits). Co-Authored-By: Claude Opus 5.5 Signed-off-by: DB --- drivers/gpu/drm/apple/iomfb_v14_7.c | 8 ++++++++ drivers/gpu/drm/apple/parser.c | 32 +++++++++++++++++++++++++++++ drivers/gpu/drm/apple/parser.h | 6 ++++++ 3 files changed, 46 insertions(+) diff --git a/drivers/gpu/drm/apple/iomfb_v14_7.c b/drivers/gpu/drm/apple/iomfb_v14_7.c index 6f44cb699093bd..4683df3b3ac0b4 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7.c +++ b/drivers/gpu/drm/apple/iomfb_v14_7.c @@ -930,6 +930,14 @@ static void dcp_v14_external_published(struct apple_dcp_v14 *v14, const char *ke ret = parse(raw->data, raw->size, &ctx); if (!ret) { ctx.dcp = dcp; + /* Timings that list no ColorModes use these; see parse_mode(). */ + for (i = 0; i < v14->raw_count; i++) + if (!v14->raw[i].service && + !strcmp(v14->raw[i].key, "ColorElements") && + v14->raw[i].size) { + ctx.shared_colors = v14->raw[i].data; + ctx.shared_colors_len = v14->raw[i].size; + } ret = dcp_modes_replace(dcp, &ctx, generation); } if (ret) { diff --git a/drivers/gpu/drm/apple/parser.c b/drivers/gpu/drm/apple/parser.c index 1d0638360251c6..d6e01c1e724e56 100644 --- a/drivers/gpu/drm/apple/parser.c +++ b/drivers/gpu/drm/apple/parser.c @@ -684,6 +684,30 @@ static int parse_color_modes(struct dcp_parse_ctx *handle, return 0; } +/* + * A T6031 external processor (firmware 14.7) publishes every timing with an + * empty ColorModes: the color modes are the display's ColorElements, less the + * IDs the timing lists as unsafe or DSC-only. @timing is the timing's + * dictionary. Only full-range SDR RGB is taken, as for H17P external SDR. + */ +static int parse_shared_colors(const struct dcp_parse_ctx *handle, + const struct dcp_parse_ctx *timing, + struct dcp_display_mode *out) +{ + struct timing_color_restrictions limits = {}; + struct dcp_parse_ctx scan = *timing, colors; + int ret; + + ret = parse_timing_color_restrictions(&scan, &limits); + if (ret) + return ret; + ret = parse(handle->shared_colors, handle->shared_colors_len, &colors); + if (ret) + return ret; + colors.dcp = handle->dcp; + return parse_color_modes(&colors, out, true, &limits, NULL); +} + /* * Calculate the pixel clock for a mode given the 16:16 fixed-point refresh * rate. The pixel clock is the refresh rate times the pixel count. DRM @@ -722,6 +746,7 @@ static int parse_mode(struct dcp_parse_ctx *handle, bool have_preferred = false, have_interlaced = false; bool is_preferred = false; struct drm_display_mode *mode = &out->mode; + const struct dcp_parse_ctx timing = *handle; *out = (struct dcp_display_mode) { .sdr_rgb.score = -1, @@ -830,6 +855,13 @@ static int parse_mode(struct dcp_parse_ctx *handle, out->automatic_rgb8_quirk = handle->automatic_1080p180_rgb8; } + if (!internal && handle->shared_colors && out->best.score < 0 && + out->sdr.score < 0 && out->sdr_rgb.score < 0 && out->sdr_444.score < 0) { + ret = parse_shared_colors(handle, &timing, out); + if (ret) + return ret; + } + if (out->sdr_rgb.score >= 0) best_color_mode = out->sdr_rgb.id; else if (out->sdr_444.score >= 0) diff --git a/drivers/gpu/drm/apple/parser.h b/drivers/gpu/drm/apple/parser.h index 2ba2c22a070266..1633bbf4c33c39 100644 --- a/drivers/gpu/drm/apple/parser.h +++ b/drivers/gpu/drm/apple/parser.h @@ -21,6 +21,12 @@ struct dcp_parse_ctx { /* External H17P candidates require separate publication/lookup proof. */ bool automatic_1080p180_rgb8; bool ambiguous_180hz; + /* + * An external display's ColorElements, serialized, for timings that + * list no ColorModes of their own (T6031 dcpext, firmware 14.7). + */ + const void *shared_colors; + u32 shared_colors_len; #if IS_ENABLED(CONFIG_DRM_APPLE_DCP_T6030_AUDIO) /* * T6030 external processor audio elements (DCP firmware 14.7, external From d6a91ffe3ff9d1fbc94550ab36aedb09436788c5 Mon Sep 17 00:00:00 2001 From: DB Date: Fri, 30 Oct 2026 06:24:42 +0000 Subject: [PATCH 11/22] drm: apple: clock the T6031 crossbar's direct DP output on link config The display crossbar runs T6031 in its T6030 mode (t6030_dpin): a route is selected on activate, and its clocks come up only from the DCP's DidChangeLinkConfiguration call, through dcp_direct_crossbar_link(). That function returned early on anything but apple,t6030, so on T6031 the direct DP PHY output stayed unclocked ('T602X route 0, clocks disabled'), and every mode set failed in the firmware with 'IOAVVideoInterface prepareLink() failed'. Co-Authored-By: Claude Opus 5.5 Signed-off-by: DB --- drivers/gpu/drm/apple/dcp-fabric.c | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp-fabric.c b/drivers/gpu/drm/apple/dcp-fabric.c index 29321f8032a490..e98a73b89eadf6 100644 --- a/drivers/gpu/drm/apple/dcp-fabric.c +++ b/drivers/gpu/drm/apple/dcp-fabric.c @@ -1750,10 +1750,11 @@ int dcp_tunnel_crossbar_down(struct apple_dcp *dcp, u32 unit) } /* - * T6030 routes a direct DP PHY output of its crossbar on select and clocks - * it only while a link runs, as it does its DP IN outputs. From DCP's link - * configuration calls: clocks up once a link is configured at a nonzero - * rate, down before the rate changes and when DCP deactivates the port. + * T6030 and T6031 route a direct DP PHY output of their crossbar on select + * and clock it only while a link runs, as they do their DP IN outputs. From + * DCP's link configuration calls: clocks up once a link is configured at a + * nonzero rate, down before the rate changes and when DCP deactivates the + * port. * The output is the borrowed Type-C route's, or the fixed route's. Other * SoCs clock it at select. Runs inside a DCP apcall: only tb_lock. */ @@ -1763,7 +1764,9 @@ int dcp_direct_crossbar_link(struct apple_dcp *dcp, bool up) struct mux_control *xbar = NULL; int ret; - if (!of_machine_is_compatible("apple,t6030")) + /* The crossbar driver runs both in its T6030 mode (t6030_dpin). */ + if (!of_machine_is_compatible("apple,t6030") && + !of_machine_is_compatible("apple,t6031")) return 0; guard(mutex)(&dcp->tb_lock); route = dcp->active_typec_route; From abbc8264bc0ba537ea9220433f3b48e3db401c93 Mon Sep 17 00:00:00 2001 From: DB Date: Fri, 30 Oct 2026 06:40:24 +0000 Subject: [PATCH 12/22] apple: run Thunderbolt DP tunnels on T6031 T6031 (M3 Max) has the T6030 (M3 Pro) display fabric: T8122-generation ATC PHYs, a crossbar in t6030_dpin mode and dcpext processors bound as apple,t6030-dcpext. Its DP IN tunnel path was still disabled, though: - the Thunderbolt driver leaves DP IN off on SoCs missing from apple_dpin_qualified_soc, and the T6031 ACIO matched only the t8103 fallback (M1 DP IN policy); - the DCP fabric and the ATC PHY pick the T6030 DP IN pixel clock path and its source map on of_machine_is_compatible("apple,t6030") alone. Qualify T6031, give its ACIO the T6030 DP IN policy while it keeps the t8103 boot sequence (its DT node has no MxWrap region), and take the T6030 clock path on T6031 too. The T6030 source map (crossbar sources 0 and 2 feed DP IN0/1) is assumed to hold on T6031; untested. Co-Authored-By: Claude Opus 5.5 Signed-off-by: DB --- drivers/gpu/drm/apple/dcp-fabric.c | 20 +++++++++++++++----- drivers/phy/apple/atc.c | 11 +++++++---- drivers/thunderbolt/apple.c | 13 +++++++++++++ 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp-fabric.c b/drivers/gpu/drm/apple/dcp-fabric.c index e98a73b89eadf6..e85ce95a6bdc10 100644 --- a/drivers/gpu/drm/apple/dcp-fabric.c +++ b/drivers/gpu/drm/apple/dcp-fabric.c @@ -110,6 +110,16 @@ bool dcp_is_usb4_output(struct apple_dcp *dcp) return dcp->active_typec_route && dcp->active_typec_route->tunnel; } +/* + * T6031 (M3 Max) has the T6030 (M3 Pro) DP IN clock path: the same + * T8122-generation ATC PHYs and T6030 external processors. + */ +static bool dcp_fabric_t603x(void) +{ + return of_machine_is_compatible("apple,t6030") || + of_machine_is_compatible("apple,t6031"); +} + /* SoC flow support and board wiring are separate qualifications. */ bool dcp_uses_t6020_tunnel_flow(struct apple_dcp *dcp) { @@ -211,7 +221,7 @@ bool dcp_typec_dual_stream(void) bool dcp_typec_ordered_dual_stream(void) { return dcp_fabric_ordered_dual_stream(dcp_typec_dual_stream(), - of_machine_is_compatible("apple,t6030")); + dcp_fabric_t603x()); } /* @@ -257,7 +267,7 @@ static int dcp_route_clock_rate(struct apple_dcp_typec_route *route, { int ret; - if (of_machine_is_compatible("apple,t6030")) { + if (dcp_fabric_t603x()) { typeof(apple_atc_dp_tunnel_rate_source) *fn; if (dcp_fabric_t6030_pclk(route->mux_index, dpin) < 0) @@ -902,7 +912,7 @@ static void dcp_fabric_snapshot_port(struct apple_dcp_typec_port *port, route->core.tunnel = route->tunnel; route->core.dpin = route->tunnel_dpin; route->core.tunnel_clock_blocked = dcp_fabric_tunnel_clock_blocked( - of_machine_is_compatible("apple,t6030"), route->mux_index); + dcp_fabric_t603x(), route->mux_index); *tail = &route->core; tail = &route->core.next; } @@ -1808,7 +1818,7 @@ int dcp_tunnel_set_rate(struct apple_dcp *dcp, struct phy *phy, u32 unit, return -ENODEV; if (!dcp_fabric_callback_valid(route->tunnel_generation, dcp->tb_generation)) return -ESTALE; - if (of_machine_is_compatible("apple,t6030") && + if (dcp_fabric_t603x() && (unit || dcp->dptx_dfp_port != 1 + route->tunnel_dpin || apple_dp_tunnel_t6030_pclk(route->mux_index, route->tunnel_dpin) < 0)) @@ -1906,7 +1916,7 @@ static struct mux_control * dcp_typec_tunnel_ctl(struct apple_dcp_typec_route *route, unsigned int dpin) { if (dpin > 1 || - (dcp_fabric_tunnel_clock_blocked(of_machine_is_compatible("apple,t6030"), + (dcp_fabric_tunnel_clock_blocked(dcp_fabric_t603x(), route->mux_index) & BIT(dpin))) return NULL; if (route->dpin[dpin]) diff --git a/drivers/phy/apple/atc.c b/drivers/phy/apple/atc.c index 4ce4ad5062504f..eefc4f1c539e00 100644 --- a/drivers/phy/apple/atc.c +++ b/drivers/phy/apple/atc.c @@ -3219,10 +3219,14 @@ static int atc_tunnel_set_t602x(struct apple_atcphy *atcphy, unsigned int dpin, return 0; } -/* T6030 (M3 Pro) uses T8122-generation PHYs for both DP IN clock outputs. */ +/* + * T6030 (M3 Pro) and T6031 (M3 Max) use T8122-generation PHYs for both DP IN + * clock outputs. + */ static bool apple_atc_tunnel_is_t6030(struct apple_atcphy *atcphy) { - return of_machine_is_compatible("apple,t6030") && + return (of_machine_is_compatible("apple,t6030") || + of_machine_is_compatible("apple,t6031")) && atcphy->hw->gen == ATCPHY_GENERATION_T8122; } @@ -3487,8 +3491,7 @@ static int atcphy_configure(struct apple_atcphy *atcphy, enum atcphy_mode mode) /* Keep failed transitions retryable even when the requested mode is unchanged. */ atcphy->mode_dirty = true; /* the mode change below may power the PHY down: stop the tunnel clock first */ - if (of_machine_is_compatible("apple,t6030") && - atcphy->hw->gen == ATCPHY_GENERATION_T8122) { + if (apple_atc_tunnel_is_t6030(atcphy)) { ret = atc_t8122_tunnel_stop(atcphy); if (ret) return ret; diff --git a/drivers/thunderbolt/apple.c b/drivers/thunderbolt/apple.c index c6625c5cbb5d3f..6bdad9c5d5d99e 100644 --- a/drivers/thunderbolt/apple.c +++ b/drivers/thunderbolt/apple.c @@ -151,6 +151,14 @@ static const struct apple_cio_hw apple_cio_t6030_hw = { .dp = &apple_dpin_m3, }; +/* + * The T6031 (M3 Max) ACIO boots like t8103 (no MxWrap region in its DT + * node) and drives its DP IN adapters with the T6030 (M3 Pro) policy. + */ +static const struct apple_cio_hw apple_cio_t6031_hw = { + .dp = &apple_dpin_m3, +}; + /* Type 7 uses its existing ready-state check and has no qualified DP route. */ static const struct apple_cio_hw apple_cio_t8142_hw = { .dp = &apple_dpin_disabled, @@ -376,6 +384,7 @@ static const struct of_device_id apple_dpin_qualified_soc[] = { { .compatible = "apple,t6020" }, { .compatible = "apple,t6021" }, { .compatible = "apple,t6030" }, + { .compatible = "apple,t6031" }, {}, }; @@ -3335,6 +3344,10 @@ static const struct of_device_id apple_acio_match[] = { .compatible = "apple,t6030-usb4-acio", .data = &apple_cio_t6030_hw, }, + { + .compatible = "apple,t6031-usb4-acio", + .data = &apple_cio_t6031_hw, + }, { .compatible = "apple,t6020-usb4-acio", .data = &apple_cio_t6020_hw, From 06bc062400b52d30305cafc6729cb01ed2b42d7c Mon Sep 17 00:00:00 2001 From: DB Date: Fri, 30 Oct 2026 07:02:20 +0000 Subject: [PATCH 13/22] thunderbolt: keep T6031 DP tunnels out of bandwidth allocation mode The T6031 (M3 Max) DCP DPTX firmware drives a DP tunnel like a plain DP link: it never requests bandwidth through the DP IN adapter's allocation registers. With allocation mode enabled the tunnel reported "consumed bandwidth 0/12960 Mb/s", the mode set completed and the sink (a DisplayPort monitor behind a USB4 dock, DP OUT x2 HBR3) dropped the stream about 1.5 s later (DPTX INACTIVE_SINK_DETECTED). The same setup works with thunderbolt.bw_alloc_mode=0. Add a TB_HOST_DP_NO_BW_ALLOC host policy that takes the bw_alloc_mode=0 path for that host's tunnels only, and give T6031 its own policy: the T6030 flow plus the new flag. T6030 keeps its current behaviour. Co-Authored-By: Claude Opus 5.5 Signed-off-by: DB --- drivers/thunderbolt/apple-dpin-state.c | 13 +++++++++++++ drivers/thunderbolt/apple-dpin-state.h | 1 + drivers/thunderbolt/apple.c | 4 ++-- drivers/thunderbolt/nhi.h | 2 ++ drivers/thunderbolt/tunnel.c | 3 ++- 5 files changed, 20 insertions(+), 3 deletions(-) diff --git a/drivers/thunderbolt/apple-dpin-state.c b/drivers/thunderbolt/apple-dpin-state.c index 18817e784822c0..eab144d3e293ed 100644 --- a/drivers/thunderbolt/apple-dpin-state.c +++ b/drivers/thunderbolt/apple-dpin-state.c @@ -33,6 +33,19 @@ const struct apple_dpin_policy apple_dpin_m3 = { .host_policy = TB_HOST_DP_NOTIFY, }; +/* + * T6031 (M3 Max): the T6030 flow, without bandwidth allocation mode. Its DCP + * DPTX firmware never requests bandwidth through the DP IN adapter, so an + * allocation-mode tunnel carries none and the sink drops the stream right + * after the mode set (seen on an iVANKY USB4 dock, DP OUT x2 HBR3). + */ +const struct apple_dpin_policy apple_dpin_m3_max = { + .flow = APPLE_DPIN_CHANGED, + .capacity_retry = true, + .defer_new_bringup = true, + .host_policy = TB_HOST_DP_NOTIFY | TB_HOST_DP_NO_BW_ALLOC, +}; + enum apple_dpin_flow apple_dpin_hooks(const struct apple_dpin_policy *policy, bool queue_present, bool display_enabled) { diff --git a/drivers/thunderbolt/apple-dpin-state.h b/drivers/thunderbolt/apple-dpin-state.h index 5ccfef1c45e545..ebaa9467546550 100644 --- a/drivers/thunderbolt/apple-dpin-state.h +++ b/drivers/thunderbolt/apple-dpin-state.h @@ -29,6 +29,7 @@ extern const struct apple_dpin_policy apple_dpin_disabled; extern const struct apple_dpin_policy apple_dpin_m1; extern const struct apple_dpin_policy apple_dpin_m2; extern const struct apple_dpin_policy apple_dpin_m3; +extern const struct apple_dpin_policy apple_dpin_m3_max; enum apple_dpin_phase { APPLE_DPIN_IDLE, diff --git a/drivers/thunderbolt/apple.c b/drivers/thunderbolt/apple.c index 6bdad9c5d5d99e..16de7b1114b643 100644 --- a/drivers/thunderbolt/apple.c +++ b/drivers/thunderbolt/apple.c @@ -153,10 +153,10 @@ static const struct apple_cio_hw apple_cio_t6030_hw = { /* * The T6031 (M3 Max) ACIO boots like t8103 (no MxWrap region in its DT - * node) and drives its DP IN adapters with the T6030 (M3 Pro) policy. + * node) and drives its DP IN adapters with the T6030 (M3 Pro) flow. */ static const struct apple_cio_hw apple_cio_t6031_hw = { - .dp = &apple_dpin_m3, + .dp = &apple_dpin_m3_max, }; /* Type 7 uses its existing ready-state check and has no qualified DP route. */ diff --git a/drivers/thunderbolt/nhi.h b/drivers/thunderbolt/nhi.h index 2707adf6215f90..e1ce2a24ed50d5 100644 --- a/drivers/thunderbolt/nhi.h +++ b/drivers/thunderbolt/nhi.h @@ -222,6 +222,8 @@ struct tb_nhi_ops { #define TB_HOST_DP_KEEP_DPRX_TIMEOUT BIT(3) #define TB_HOST_DP_ADAPTER_QUIRKS BIT(4) #define TB_HOST_DP_INITIAL_BW_GRANT BIT(5) +/* The host's DPTX never requests bandwidth: keep DP tunnels out of allocation mode. */ +#define TB_HOST_DP_NO_BW_ALLOC BIT(6) /* * Minimal number of vectors when we use MSI-X. Two for control channel diff --git a/drivers/thunderbolt/tunnel.c b/drivers/thunderbolt/tunnel.c index 3c99c25df6f4ef..d27cc006368b8e 100644 --- a/drivers/thunderbolt/tunnel.c +++ b/drivers/thunderbolt/tunnel.c @@ -1039,7 +1039,8 @@ static int tb_dp_bandwidth_alloc_mode_enable(struct tb_tunnel *tunnel) u32 in_dp_cap, in_rate, in_lanes; u32 rate, lanes; - if (!bw_alloc_mode) + if (!bw_alloc_mode || + tb_host_dp_policy(tunnel->tb->nhi, TB_HOST_DP_NO_BW_ALLOC)) return 0; ret = usb4_dp_port_set_cm_bandwidth_mode_supported(in, true); From d95180b53ce8973516b9cfa1453b268884842a24 Mon Sep 17 00:00:00 2001 From: DB Date: Fri, 30 Oct 2026 16:37:59 +0000 Subject: [PATCH 14/22] arm64: dts: apple: j516c: describe the HDMI port The J516C's HDMI port is ATC3 used as a DisplayPort-only PHY behind display-crossbar0, feeding an on-board DP-to-HDMI converter, as on the J514S/J516S (t603x-j514s-j516s-hdmi.dtsi). The T6031 tree described only ATC0..2, so the port had no connector. From the J516C ADT: - atc-phy3 and atc3-dpxbar sit where the T6030's do (0x1303000000, 0x130304c000); - dp2hdmi-gpio0: hdmi_pwr_en and dp2hdmi_pwr_en are SMC gP0f and gP1a, the J514S/J516S pins; - atc3-hdmi: function-hdmi_hpd is AOP GPIO 42 (47 on the J514S/J516S). dcpext0 takes the fixed route and stays a hybrid pipeline, lent to a USB-C port while no HDMI display is attached. The atcphy3 alias lets m1n1 copy the ADT's atc-phy3 calibration; without it the PHY reports "Calibration tunables missing, DisplayPort disabled". HDMI audio (DPA1 over the SIO) is not described yet. Signed-off-by: DB Co-Authored-By: Claude Opus 5.5 --- arch/arm64/boot/dts/apple/t6031-j516c.dts | 1 + .../dts/apple/t603x-j514c-j516c-hdmi.dtsi | 111 ++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 arch/arm64/boot/dts/apple/t603x-j514c-j516c-hdmi.dtsi diff --git a/arch/arm64/boot/dts/apple/t6031-j516c.dts b/arch/arm64/boot/dts/apple/t6031-j516c.dts index ca9b437c9a9c0f..2b3fc4b9bf52fe 100644 --- a/arch/arm64/boot/dts/apple/t6031-j516c.dts +++ b/arch/arm64/boot/dts/apple/t6031-j516c.dts @@ -13,6 +13,7 @@ #include "t6031-dcp.dtsi" #include "t603x-j514-j516.dtsi" #include "t603x-j514c-j516c-dcpext.dtsi" +#include "t603x-j514c-j516c-hdmi.dtsi" / { compatible = "apple,j516c", "apple,t6031", "apple,arm-platform"; diff --git a/arch/arm64/boot/dts/apple/t603x-j514c-j516c-hdmi.dtsi b/arch/arm64/boot/dts/apple/t603x-j514c-j516c-hdmi.dtsi new file mode 100644 index 00000000000000..777779250e91ae --- /dev/null +++ b/arch/arm64/boot/dts/apple/t603x-j514c-j516c-hdmi.dtsi @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: GPL-2.0+ OR MIT +/* + * HDMI port of the M3 Max MacBook Pros (J514C, J516C), as on the J514S/J516S + * (t603x-j514s-j516s-hdmi.dtsi). + * + * The ADT's atc3-dpphy is a DisplayPort-only port (port-type 6, no USB) + * behind display-crossbar0's atc3 endpoint, feeding the on-board DP-to-HDMI + * converter (dp2hdmi-gpio0). Its power enables are the J514S/J516S SMC pins + * (gP0f hdmi_pwr_en, gP1a dp2hdmi_pwr_en); the hotplug signal is AOP GPIO 42 + * (atc3-hdmi function-hdmi_hpd), not 47 as on the J514S/J516S. + * + * Any dispext can reach ATC3 through the crossbar; dcpext0 takes the fixed + * route here, as the first external processor does on T6030, and stays a + * hybrid pipeline that is lent to a USB-C port while no HDMI display is + * attached. (On the J516C dcpext3 times out on the fixed route just as + * dcpext1..3 do on USB-C.) + * + * The port's audio (DPA1 over the SIO) is not described yet. + */ + +/ { + aliases { + /* m1n1 copies the ADT's atc-phy3 calibration to this alias. */ + atcphy3 = &atcphy3; + }; +}; + +&soc { + /* + * Only the DisplayPort block of ATC3 is described, so the PHY driver + * runs it DisplayPort-only and never touches the USB2 or PIPE blocks. + * The ADT's atc-phy3 and atc3-dpxbar sit where the T6030's do. + */ + atcphy3: phy@1303000000 { + compatible = "apple,t6031-atcphy", "apple,t8122-atcphy"; + reg = <0x13 0x03000000 0x0 0x4c000>; + reg-names = "core"; + + #phy-cells = <1>; + #reset-cells = <0>; + + orientation-switch; + mode-switch; + power-domains = <&ps_atc3_common>; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + port@2 { + reg = <2>; + atcphy3_dp: endpoint { + remote-endpoint = <&dcpext0_dpout>; + }; + }; + }; + }; + + atcphy3_xbar: mux@130304c000 { + compatible = "apple,t6031-display-crossbar", + "apple,t6020-display-crossbar"; + reg = <0x13 0x0304c000 0x0 0x4000>; + #mux-control-cells = <1>; + power-domains = <&ps_atc3_usb>; + }; +}; + +&dcpext0 { + apple,connector-type = "HDMI-A"; + + /* HDMI HPD, also used as the hotplug interrupt */ + hdmi-hpd-gpios = <&pinctrl_aop 42 GPIO_ACTIVE_HIGH>; + hdmi-pwren-gpios = <&smc_gpio 15 GPIO_ACTIVE_HIGH>; + dp2hdmi-pwren-gpios = <&smc_gpio 26 GPIO_ACTIVE_HIGH>; + + phys = <&atcphy3 PHY_TYPE_DP>, <&atcphy0 PHY_TYPE_DP>, + <&atcphy1 PHY_TYPE_DP>, <&atcphy2 PHY_TYPE_DP>; + phy-names = "dp-phy", "typec0", "typec1", "typec2"; + mux-controls = <&atcphy3_xbar 0>, <&atcphy0_xbar 0>, + <&atcphy1_xbar 0>, <&atcphy2_xbar 0>, + <&atcphy0_xbar 1>, <&atcphy0_xbar 2>, + <&atcphy1_xbar 1>, <&atcphy1_xbar 2>, + <&atcphy2_xbar 1>, <&atcphy2_xbar 2>; + mux-control-names = "dp-xbar", "typec0", "typec1", "typec2", + "typec0-dpin0", "typec0-dpin1", + "typec1-dpin0", "typec1-dpin1", + "typec2-dpin0", "typec2-dpin1"; + /* The crossbar's upstream port N is dispextN (ADT display-crossbar0). */ + mux-index = <0>; + apple,dptx-phy = <3>; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + port@1 { + reg = <1>; + dcpext0_dpout: endpoint { + remote-endpoint = <&atcphy3_dp>; + }; + }; + }; +}; + +/* + * ATC3 has no USB consumer that would hold its common block on, so across + * system sleep it would power down and lose the HDMI PHY state. + */ +&ps_atc3_common { + apple,always-on; +}; From 26ea9ca83c9cf6050ee9990b08cb662f6b753471 Mon Sep 17 00:00:00 2001 From: DB Date: Fri, 30 Oct 2026 16:37:59 +0000 Subject: [PATCH 15/22] arm64: dts: apple: j516c: keep dcpext1..3 from the display gate On the J516C only dcpext0 brings up an external display. dcpext1..3 time out in the DPTX remote port on USB-C (HPD assert, link configuration) and dcpext3 does the same on the HDMI route: AppleDCPDPTXRemotePort (chan:1) command type 0xc0 tag 0x0300 timed out after 1000 ms dcp_dptx_connect: failed to assert Type-C HPD: -110 DPTXPort: firmware reports link fault 22 on target 0:3 With the HDMI route on dcpext0, the allocator offers a hybrid pipeline to USB-C last, so a USB-C display always landed on a broken processor. Mark dcpext1..3 "fail": the display gate enables only processors left "disabled", and the DRM device waits only for available ones, so dcpext0 serves USB-C and HDMI alike. This is a stopgap that limits the J516C to one external display until dcpext1..3 are fixed; drop it then. Signed-off-by: DB Co-Authored-By: Claude Opus 5.5 --- arch/arm64/boot/dts/apple/t6031-j516c.dts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/arch/arm64/boot/dts/apple/t6031-j516c.dts b/arch/arm64/boot/dts/apple/t6031-j516c.dts index 2b3fc4b9bf52fe..1a31b84f915b18 100644 --- a/arch/arm64/boot/dts/apple/t6031-j516c.dts +++ b/arch/arm64/boot/dts/apple/t6031-j516c.dts @@ -55,3 +55,22 @@ compatible = "apple,j516-macaudio", "apple,j316-macaudio", "apple,macaudio"; model = "MacBook Pro J516"; }; + +/* + * On this J516C only dcpext0 brings up an external display: dcpext1..3 time + * out in the DPTX remote port (HPD assert, link configuration) on USB-C and + * on the HDMI route alike. Keep them away from the display gate, which only + * enables processors left "disabled", so the allocator always picks dcpext0. + * The DRM device waits only for available processors. + */ +&dcpext1 { + status = "fail"; +}; + +&dcpext2 { + status = "fail"; +}; + +&dcpext3 { + status = "fail"; +}; From f40169053c62a9cad811d11d90e6b01b60f22221 Mon Sep 17 00:00:00 2001 From: DB Date: Fri, 6 Nov 2026 13:02:18 +0000 Subject: [PATCH 16/22] drm: apple: rebuild a T6031 dcpext catalog when its colors change A T6031 external processor's timings take their colors from ColorElements, but dcp_v14_external_published() built the mode catalog only on a TimingElements publication. Timings published before their colors so gave no mode until republished, replaced colors (ID 7 by 11) left the catalog on the old ID, and withdrawn colors left an admitted catalog naming color IDs that were gone. The fallback also applied to every native v14.7 external processor, beyond the T6031 opt-in. Opt the T6031 dcpext in explicitly (shared_colors), and on it rebuild the catalog from the published timings and the colors published now whenever either is published, replaced or withdrawn, in either order. A color change that leaves the timings no mode withdraws the catalog (dcp_modes_withdraw(): no longer admitted, kept for snapshot readers) and the timings' part of the description, and reports the display withdrawn once: the firmware withdraws ColorElements before TimingElements on an unplug, and a second report would release a connector kept connected through the withdrawal. A removal has no transfer and applies to the current attachment; a transfer begun before it is still refused as stale. The dcp_v14_shared_colors KUnit suite runs the production callback through both publication orders, replacement, unsafe-ID exclusion, withdrawal in both orders (one report each), republication, stale generations, and a processor without the opt-in. With the rebuild on a color change disabled, six of its eight cases fail. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WHu9VBxYcBYR8voXKiSV65 Signed-off-by: DB --- drivers/gpu/drm/apple/iomfb.c | 17 + drivers/gpu/drm/apple/iomfb_v14_7.c | 105 ++++- .../apple/iomfb_v14_7_shared_colors_test.c | 386 ++++++++++++++++++ drivers/gpu/drm/apple/parser.h | 5 +- 4 files changed, 496 insertions(+), 17 deletions(-) create mode 100644 drivers/gpu/drm/apple/iomfb_v14_7_shared_colors_test.c diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index 6e80eca48ad9ac..c450120113d928 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -1665,6 +1665,23 @@ int dcp_modes_replace(struct apple_dcp *dcp, struct dcp_parse_ctx *ctx, return ret; } +/* + * The catalog no longer describes the display, though no replacement + * parsed: its mode ids refer to a description since changed or withdrawn. + * It stays allocated for readers holding a snapshot, but is no longer + * offered or matched. A transfer started before the current attachment + * changes nothing (-ESTALE). + */ +int dcp_modes_withdraw(struct apple_dcp *dcp, u64 generation) +{ + guard(mutex)(&dcp->modes_lock); + if (generation != dcp->modes_generation) + return -ESTALE; + dcp->modes_admitted = false; + dcp->modes_provisional = false; + return 0; +} + /* 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) diff --git a/drivers/gpu/drm/apple/iomfb_v14_7.c b/drivers/gpu/drm/apple/iomfb_v14_7.c index 4683df3b3ac0b4..7f79989ecc24e9 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7.c +++ b/drivers/gpu/drm/apple/iomfb_v14_7.c @@ -246,6 +246,18 @@ struct apple_dcp_v14 { * and the kernel sets the mode (A411) and the power state (A472). */ bool external; + /* + * External, T6031 (J516C 14.7 image): timings list no ColorModes, and + * take theirs from the published ColorElements. The mode catalog so + * depends on both, and is rebuilt when either changes. + */ + bool shared_colors; + /* + * A ColorElements change left the timings without a mode, and the + * display was reported withdrawn then: the TimingElements removal + * that follows on an unplug does not report it again. + */ + bool colors_withdrew; /* External: start signal and first client open done. */ bool opened; /* External: the display is powered (A472) in the mode last set. */ @@ -834,7 +846,10 @@ static bool dcp_v14_external_hold(struct apple_dcp *dcp, bool hdmi_hpd) */ void iomfb_v14_7_external_withdrawn(struct apple_dcp *dcp) { - bool hdmi_hpd = dcp_hdmi_port_attached(dcp); + bool hdmi_hpd; + + KUNIT_STATIC_STUB_REDIRECT(iomfb_v14_7_external_withdrawn, dcp); + hdmi_hpd = dcp_hdmi_port_attached(dcp); if ((READ_ONCE(dcp->typec_cable_connected) && !READ_ONCE(dcp->typec_crtc_off)) || hdmi_hpd) @@ -861,6 +876,44 @@ bool iomfb_v14_7_external_release(struct apple_dcp *dcp) return true; } +/* The published raw property @key of service 0, or NULL. */ +static struct dcp_v14_raw *dcp_v14_raw_find(struct apple_dcp_v14 *v14, const char *key) +{ + u32 i; + + for (i = 0; i < v14->raw_count; i++) + if (!v14->raw[i].service && !strcmp(v14->raw[i].key, key)) + return &v14->raw[i]; + return NULL; +} + +/* + * Builds the mode catalog from the published TimingElements and, where the + * timings take their colors from there (shared_colors), the ColorElements + * published now. -ENODATA without timings. + */ +static int dcp_v14_external_catalog(struct apple_dcp_v14 *v14, struct apple_dcp *dcp, + u64 generation) +{ + struct dcp_v14_raw *timings = dcp_v14_raw_find(v14, "TimingElements"); + struct dcp_v14_raw *colors; + struct dcp_parse_ctx ctx; + int ret; + + if (!timings) + return -ENODATA; + ret = parse(timings->data, timings->size, &ctx); + if (ret) + return ret; + ctx.dcp = dcp; + colors = v14->shared_colors ? dcp_v14_raw_find(v14, "ColorElements") : NULL; + if (colors && colors->size) { + ctx.shared_colors = colors->data; + ctx.shared_colors_len = colors->size; + } + return dcp_modes_replace(dcp, &ctx, generation); +} + /* * A display description the firmware published (or removed, @removed) on an * external processor: the timings and color modes of the attached display @@ -922,30 +975,47 @@ static void dcp_v14_external_published(struct apple_dcp_v14 *v14, const char *ke dcp_connector_update_dict(connector, key, &chunks); } - if (!strcmp(key, "TimingElements")) { - if (removed) { + if (!strcmp(key, "TimingElements") && removed) { + if (!v14->colors_withdrew) iomfb_v14_7_external_withdrawn(dcp); + v14->colors_withdrew = false; + } else if (!strcmp(key, "TimingElements") || + (part == DCP_V14_DESC_COLOR && v14->shared_colors && + dcp_v14_raw_find(v14, "TimingElements"))) { + /* + * With shared colors the catalog depends on the timings and the + * colors both, published in either order: rebuild it on either. + * A removal has no transfer; it applies to the current attachment. + */ + if (removed) + generation = dcp_modes_transfer_begin(dcp); + ret = dcp_v14_external_catalog(v14, dcp, generation); + if (ret == -ESTALE) + return; + if (ret && part == DCP_V14_DESC_COLOR) { + /* + * The colors the catalog's modes were chosen from changed, + * and the timings no longer give a mode: no mode is left. + */ + dev_warn(dcp->dev, "display colors leave no usable timing: %d\n", ret); + if (dcp_modes_withdraw(dcp, generation)) + return; + v14->described &= ~DCP_V14_DESC_TIMING; + wake_up_all(&v14->described_wait); + if (!v14->colors_withdrew) + iomfb_v14_7_external_withdrawn(dcp); + v14->colors_withdrew = true; return; - } - ret = parse(raw->data, raw->size, &ctx); - if (!ret) { - ctx.dcp = dcp; - /* Timings that list no ColorModes use these; see parse_mode(). */ - for (i = 0; i < v14->raw_count; i++) - if (!v14->raw[i].service && - !strcmp(v14->raw[i].key, "ColorElements") && - v14->raw[i].size) { - ctx.shared_colors = v14->raw[i].data; - ctx.shared_colors_len = v14->raw[i].size; - } - ret = dcp_modes_replace(dcp, &ctx, generation); } if (ret) { dev_warn(dcp->dev, "display timings not used: %d\n", ret); return; } + v14->colors_withdrew = false; if (READ_ONCE(dcp->nr_modes)) v14->described |= DCP_V14_DESC_TIMING; + else if (v14->shared_colors) + v14->described &= ~DCP_V14_DESC_TIMING; wake_up_all(&v14->described_wait); dev_info(dcp->dev, "display has %u usable modes\n", READ_ONCE(dcp->nr_modes)); dcp_v14_external_hotplug(dcp, READ_ONCE(dcp->nr_modes) > 0); @@ -2229,6 +2299,8 @@ static int dcpext_boot(struct apple_dcp *dcp) v14->dev = dev; v14->dcp = dcp; v14->external = dcp->external_native; + v14->shared_colors = v14->external && + of_device_is_compatible(dev->of_node, "apple,t6031-dcpext"); if (v14->external) { struct clk *clk = clk_get_optional(dev, NULL); @@ -3404,6 +3476,7 @@ void iomfb_v14_7_poweroff(struct apple_dcp *dcp) #if IS_ENABLED(CONFIG_DRM_APPLE_DISPLAY_KUNIT_TEST) #include "iomfb_v14_7_external_audio_test.c" +#include "iomfb_v14_7_shared_colors_test.c" #include "iomfb_v14_7_backlight_test.c" #include "iomfb_v14_7_backlight_lifetime_test.c" #endif diff --git a/drivers/gpu/drm/apple/iomfb_v14_7_shared_colors_test.c b/drivers/gpu/drm/apple/iomfb_v14_7_shared_colors_test.c new file mode 100644 index 00000000000000..aac68bd6bd65fd --- /dev/null +++ b/drivers/gpu/drm/apple/iomfb_v14_7_shared_colors_test.c @@ -0,0 +1,386 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +/* + * A T6031 external processor's mode catalog, whose timings list no + * ColorModes and take theirs from ColorElements: included at the end of + * iomfb_v14_7.c so that the production publication callback runs. Each + * case publishes and withdraws TimingElements and ColorElements in one + * serialized order and checks the catalog, the description and the + * generation after every step. + */ +#include +#include +#include +#include + +struct v14_sc { + struct apple_dcp *dcp; + struct apple_dcp_v14 *v14; + unsigned int withdrawals; +}; + +static struct v14_sc *v14_sc_current; + +struct v14_sc_blob { + u8 data[2048]; + u32 size; +}; + +static void v14_sc_tag(struct v14_sc_blob *b, u32 type, u32 count) +{ + b->size = ALIGN(b->size, 4); + put_unaligned_le32(count | type << 24, b->data + b->size); + b->size += 4; +} + +static void v14_sc_key(struct v14_sc_blob *b, const char *key) +{ + size_t length = strlen(key); + + v14_sc_tag(b, 9, length); + memcpy(b->data + b->size, key, length); + b->size += length; +} + +static void v14_sc_value(struct v14_sc_blob *b, s64 value) +{ + v14_sc_tag(b, 4, 0); + put_unaligned_le64(value, b->data + b->size); + b->size += 8; +} + +static void v14_sc_int(struct v14_sc_blob *b, const char *key, s64 value) +{ + v14_sc_key(b, key); + v14_sc_value(b, value); +} + +static void v14_sc_bool(struct v14_sc_blob *b, const char *key, bool value) +{ + v14_sc_key(b, key); + v14_sc_tag(b, 11, value); +} + +static void v14_sc_dimension(struct v14_sc_blob *b, const char *key, s64 active, + s64 total, s64 porch, s64 sync, s64 rate) +{ + v14_sc_key(b, key); + v14_sc_tag(b, 1, 5); + v14_sc_int(b, "Active", active); + v14_sc_int(b, "Total", total); + v14_sc_int(b, "FrontPorch", porch); + v14_sc_int(b, "SyncWidth", sync); + v14_sc_int(b, "PreciseSyncRate", rate); +} + +static struct v14_sc_blob *v14_sc_blob(struct kunit *test, u32 count) +{ + struct v14_sc_blob *b = kunit_kzalloc(test, sizeof(*b), GFP_KERNEL); + + KUNIT_ASSERT_NOT_NULL(test, b); + put_unaligned_le32(0xd3, b->data); + b->size = 4; + v14_sc_tag(b, 2, count); + return b; +} + +/* + * One 1920x1080 60 Hz timing, as the J516C 14.7 image publishes it: an + * empty ColorModes, and @unsafe (if not negative) as its one unsafe ID. + */ +static struct v14_sc_blob *v14_sc_timings(struct kunit *test, s64 id, s64 unsafe) +{ + struct v14_sc_blob *b = v14_sc_blob(test, 1); + + v14_sc_tag(b, 1, unsafe < 0 ? 6 : 7); + v14_sc_bool(b, "IsVirtual", false); + v14_sc_int(b, "ID", id); + v14_sc_int(b, "Score", 100); + v14_sc_dimension(b, "HorizontalAttributes", 1920, 2200, 88, 44, (675U << 16) / 10); + v14_sc_dimension(b, "VerticalAttributes", 1080, 1125, 4, 5, 60 << 16); + v14_sc_key(b, "ColorModes"); + v14_sc_tag(b, 2, 0); + if (unsafe >= 0) { + v14_sc_key(b, "UnsafeColorElementIDs"); + v14_sc_tag(b, 2, 1); + v14_sc_value(b, unsafe); + } + return b; +} + +/* ColorElements: full-range SDR RGB8 color modes with @ids. */ +static struct v14_sc_blob *v14_sc_colors(struct kunit *test, const s64 *ids, u32 count) +{ + struct v14_sc_blob *b = v14_sc_blob(test, count); + u32 i; + + for (i = 0; i < count; i++) { + v14_sc_tag(b, 1, 8); + v14_sc_bool(b, "IsVirtual", false); + v14_sc_int(b, "ID", ids[i]); + v14_sc_int(b, "Score", 10); + v14_sc_int(b, "Depth", 8); + v14_sc_int(b, "Colorimetry", DCP_COLORIMETRY_RGB); + v14_sc_int(b, "DynamicRange", DCP_COLOR_YCBCR_RANGE_FULL); + v14_sc_int(b, "EOTF", DCP_EOTF_SDR_GAMMA); + v14_sc_int(b, "PixelEncoding", DCP_COLOR_FORMAT_RGB); + } + return b; +} + +static void v14_sc_withdrawn(struct apple_dcp *dcp) +{ + v14_sc_current->withdrawals++; +} + +static void v14_sc_retry(struct work_struct *work) +{ +} + +static void v14_sc_stop(void *data) +{ + struct v14_sc *s = data; + u32 i; + + cancel_delayed_work_sync(&s->dcp->external_retry_wq); + for (i = 0; i < s->v14->raw_count; i++) + kvfree(s->v14->raw[i].data); + kfree(s->dcp->modes); +} + +/* A started native external session, before its display is described. */ +static struct v14_sc *v14_sc_setup(struct kunit *test, bool shared_colors) +{ + struct v14_sc *s = kunit_kzalloc(test, sizeof(*s), GFP_KERNEL); + struct device *dev; + + KUNIT_ASSERT_NOT_NULL(test, s); + dev = kunit_device_register(test, "dcp-v14-shared-colors"); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, dev); + s->dcp = devm_kzalloc(dev, sizeof(*s->dcp), GFP_KERNEL); + s->v14 = devm_kzalloc(dev, sizeof(*s->v14), GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, s->dcp); + KUNIT_ASSERT_NOT_NULL(test, s->v14); + s->dcp->dev = dev; + s->dcp->fw_compat = DCP_FIRMWARE_V_14_7; + s->dcp->external = true; + s->dcp->external_native = true; + dcp_modes_init(s->dcp); + spin_lock_init(&s->dcp->metadata.lock); + INIT_DELAYED_WORK(&s->dcp->external_retry_wq, v14_sc_retry); + s->dcp->v14 = s->v14; + s->v14->dev = dev; + s->v14->dcp = s->dcp; + s->v14->external = true; + s->v14->shared_colors = shared_colors; + s->v14->started = true; + s->v14->opened = true; + mutex_init(&s->v14->lock); + init_waitqueue_head(&s->v14->described_wait); + dcp_modes_begin_attachment(s->dcp); + KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test, v14_sc_stop, s), 0); + v14_sc_current = s; + kunit_activate_static_stub(test, iomfb_v14_7_external_withdrawn, v14_sc_withdrawn); + return s; +} + +/* The firmware publishes @key, a transfer begun at @generation. */ +static void v14_sc_publish_at(struct kunit *test, struct v14_sc *s, const char *key, + const struct v14_sc_blob *b, u64 generation) +{ + char name[64] = {}; + + strscpy(name, key, sizeof(name)); + KUNIT_ASSERT_EQ(test, dcp_v14_raw_property(s->v14, 0, (const u8 *)name, b->data, + b->size), 0); + dcp_v14_external_published(s->v14, name, generation, false); +} + +static void v14_sc_publish(struct kunit *test, struct v14_sc *s, const char *key, + const struct v14_sc_blob *b) +{ + v14_sc_publish_at(test, s, key, b, dcp_modes_transfer_begin(s->dcp)); +} + +static void v14_sc_withdraw(struct v14_sc *s, const char *key) +{ + dcp_v14_raw_remove(s->v14, key); + dcp_v14_external_published(s->v14, key, 0, true); +} + +/* The catalog offers one mode, of timing @timing in color @color. */ +static void v14_sc_expect_mode(struct kunit *test, struct v14_sc *s, u32 timing, u32 color) +{ + KUNIT_EXPECT_TRUE(test, s->dcp->modes_admitted); + KUNIT_ASSERT_EQ(test, s->dcp->nr_modes, 1U); + KUNIT_EXPECT_EQ(test, s->dcp->modes[0].timing_mode_id, timing); + KUNIT_EXPECT_EQ(test, s->dcp->modes[0].color_mode_id, color); + KUNIT_EXPECT_TRUE(test, s->v14->described & DCP_V14_DESC_TIMING); +} + +static void v14_sc_expect_none(struct kunit *test, struct v14_sc *s) +{ + KUNIT_EXPECT_FALSE(test, s->dcp->modes_admitted); + KUNIT_EXPECT_FALSE(test, s->v14->described & DCP_V14_DESC_TIMING); +} + +static const s64 v14_sc_id7[] = { 7 }; +static const s64 v14_sc_id11[] = { 11 }; +static const s64 v14_sc_id7_11[] = { 7, 11 }; + +/* Colors first, as the J516C image publishes them: the timings use them. */ +static void v14_sc_colors_then_timings(struct kunit *test) +{ + struct v14_sc *s = v14_sc_setup(test, true); + + v14_sc_publish(test, s, "ColorElements", v14_sc_colors(test, v14_sc_id7, 1)); + v14_sc_expect_none(test, s); + v14_sc_publish(test, s, "TimingElements", v14_sc_timings(test, 3, -1)); + v14_sc_expect_mode(test, s, 3, 7); +} + +/* Timings first give no mode; the colors published after them do. */ +static void v14_sc_timings_then_colors(struct kunit *test) +{ + struct v14_sc *s = v14_sc_setup(test, true); + u64 description; + + v14_sc_publish(test, s, "TimingElements", v14_sc_timings(test, 3, -1)); + v14_sc_expect_none(test, s); + KUNIT_EXPECT_EQ(test, s->dcp->nr_modes, 0U); + description = s->v14->description_generation; + v14_sc_publish(test, s, "ColorElements", v14_sc_colors(test, v14_sc_id7, 1)); + KUNIT_EXPECT_GT(test, s->v14->description_generation, description); + v14_sc_expect_mode(test, s, 3, 7); +} + +/* Replaced colors are those the catalog then selects, with no new timings. */ +static void v14_sc_colors_replaced(struct kunit *test) +{ + struct v14_sc *s = v14_sc_setup(test, true); + u64 description; + + v14_sc_publish(test, s, "ColorElements", v14_sc_colors(test, v14_sc_id7, 1)); + v14_sc_publish(test, s, "TimingElements", v14_sc_timings(test, 3, -1)); + v14_sc_expect_mode(test, s, 3, 7); + s->v14->mode_live = true; + description = s->v14->description_generation; + v14_sc_publish(test, s, "ColorElements", v14_sc_colors(test, v14_sc_id11, 1)); + KUNIT_EXPECT_FALSE(test, s->v14->mode_live); + KUNIT_EXPECT_GT(test, s->v14->description_generation, description); + v14_sc_expect_mode(test, s, 3, 11); +} + +/* A timing's unsafe ID is excluded from the shared colors, in either order. */ +static void v14_sc_unsafe_excluded(struct kunit *test) +{ + struct v14_sc *s = v14_sc_setup(test, true); + + v14_sc_publish(test, s, "TimingElements", v14_sc_timings(test, 3, 11)); + v14_sc_publish(test, s, "ColorElements", v14_sc_colors(test, v14_sc_id7_11, 2)); + v14_sc_expect_mode(test, s, 3, 7); + /* Only the unsafe color is left: no mode. */ + v14_sc_publish(test, s, "ColorElements", v14_sc_colors(test, v14_sc_id11, 1)); + v14_sc_expect_none(test, s); + KUNIT_EXPECT_EQ(test, s->withdrawals, 1U); +} + +/* + * Withdrawn colors leave the timings no mode: the catalog is withdrawn and + * the display reported withdrawn once, also across the TimingElements + * removal that follows on an unplug. A later description is used again. + */ +static void v14_sc_colors_withdrawn(struct kunit *test) +{ + struct v14_sc *s = v14_sc_setup(test, true); + + v14_sc_publish(test, s, "ColorElements", v14_sc_colors(test, v14_sc_id7, 1)); + v14_sc_publish(test, s, "TimingElements", v14_sc_timings(test, 3, -1)); + v14_sc_expect_mode(test, s, 3, 7); + v14_sc_withdraw(s, "ColorElements"); + v14_sc_expect_none(test, s); + KUNIT_EXPECT_FALSE(test, s->v14->described & DCP_V14_DESC_COLOR); + KUNIT_EXPECT_EQ(test, s->withdrawals, 1U); + v14_sc_withdraw(s, "TimingElements"); + KUNIT_EXPECT_EQ(test, s->withdrawals, 1U); + KUNIT_EXPECT_FALSE(test, s->v14->colors_withdrew); + + v14_sc_publish(test, s, "ColorElements", v14_sc_colors(test, v14_sc_id11, 1)); + v14_sc_publish(test, s, "TimingElements", v14_sc_timings(test, 4, -1)); + v14_sc_expect_mode(test, s, 4, 11); + /* Timings withdrawn first: reported once, by their removal. */ + v14_sc_withdraw(s, "TimingElements"); + KUNIT_EXPECT_EQ(test, s->withdrawals, 2U); + v14_sc_withdraw(s, "ColorElements"); + KUNIT_EXPECT_EQ(test, s->withdrawals, 2U); +} + +/* Colors withdrawn and published again: the catalog comes back. */ +static void v14_sc_colors_republished(struct kunit *test) +{ + struct v14_sc *s = v14_sc_setup(test, true); + + v14_sc_publish(test, s, "ColorElements", v14_sc_colors(test, v14_sc_id7, 1)); + v14_sc_publish(test, s, "TimingElements", v14_sc_timings(test, 3, -1)); + v14_sc_withdraw(s, "ColorElements"); + v14_sc_expect_none(test, s); + v14_sc_publish(test, s, "ColorElements", v14_sc_colors(test, v14_sc_id11, 1)); + v14_sc_expect_mode(test, s, 3, 11); + KUNIT_EXPECT_FALSE(test, s->v14->colors_withdrew); +} + +/* + * A ColorElements transfer begun before the current attachment changes + * neither catalog; one begun after it rebuilds the current timings. + */ +static void v14_sc_stale_colors(struct kunit *test) +{ + struct v14_sc *s = v14_sc_setup(test, true); + u64 old; + + v14_sc_publish(test, s, "ColorElements", v14_sc_colors(test, v14_sc_id7, 1)); + v14_sc_publish(test, s, "TimingElements", v14_sc_timings(test, 3, -1)); + v14_sc_expect_mode(test, s, 3, 7); + old = dcp_modes_transfer_begin(s->dcp); + dcp_modes_begin_attachment(s->dcp); + v14_sc_publish_at(test, s, "ColorElements", v14_sc_colors(test, v14_sc_id11, 1), old); + KUNIT_EXPECT_FALSE(test, s->dcp->modes_admitted); + KUNIT_EXPECT_EQ(test, s->dcp->modes[0].color_mode_id, 7U); + KUNIT_EXPECT_EQ(test, s->withdrawals, 0U); + /* No usable colors, stale: not withdrawn either. */ + v14_sc_publish_at(test, s, "ColorElements", v14_sc_colors(test, v14_sc_id7, 0), old); + KUNIT_EXPECT_EQ(test, s->withdrawals, 0U); + v14_sc_publish(test, s, "ColorElements", v14_sc_colors(test, v14_sc_id11, 1)); + v14_sc_expect_mode(test, s, 3, 11); +} + +/* Without the T6031 opt-in, ColorElements are not a timing's colors. */ +static void v14_sc_not_shared(struct kunit *test) +{ + struct v14_sc *s = v14_sc_setup(test, false); + + v14_sc_publish(test, s, "ColorElements", v14_sc_colors(test, v14_sc_id7, 1)); + v14_sc_publish(test, s, "TimingElements", v14_sc_timings(test, 3, -1)); + v14_sc_expect_none(test, s); + KUNIT_EXPECT_EQ(test, s->dcp->nr_modes, 0U); + v14_sc_withdraw(s, "ColorElements"); + KUNIT_EXPECT_EQ(test, s->withdrawals, 0U); +} + +static struct kunit_case v14_sc_cases[] = { + KUNIT_CASE(v14_sc_colors_then_timings), + KUNIT_CASE(v14_sc_timings_then_colors), + KUNIT_CASE(v14_sc_colors_replaced), + KUNIT_CASE(v14_sc_unsafe_excluded), + KUNIT_CASE(v14_sc_colors_withdrawn), + KUNIT_CASE(v14_sc_colors_republished), + KUNIT_CASE(v14_sc_stale_colors), + KUNIT_CASE(v14_sc_not_shared), + {} +}; + +static struct kunit_suite v14_sc_suite = { + .name = "dcp_v14_shared_colors", + .test_cases = v14_sc_cases, +}; + +kunit_test_suite(v14_sc_suite); diff --git a/drivers/gpu/drm/apple/parser.h b/drivers/gpu/drm/apple/parser.h index 1633bbf4c33c39..6e58edb2174387 100644 --- a/drivers/gpu/drm/apple/parser.h +++ b/drivers/gpu/drm/apple/parser.h @@ -23,7 +23,9 @@ struct dcp_parse_ctx { bool ambiguous_180hz; /* * An external display's ColorElements, serialized, for timings that - * list no ColorModes of their own (T6031 dcpext, firmware 14.7). + * list no ColorModes of their own. Set only by a processor that opts + * in (T6031 dcpext, firmware 14.7: shared_colors in iomfb_v14_7.c), + * which rebuilds its catalog when either is published or withdrawn. */ const void *shared_colors; u32 shared_colors_len; @@ -183,6 +185,7 @@ int dcp_attributes_replace(struct apple_dcp *dcp, struct dcp_parse_ctx *handle, u64 dcp_modes_transfer_begin(struct apple_dcp *dcp); int dcp_modes_replace(struct apple_dcp *dcp, struct dcp_parse_ctx *handle, u64 generation); +int dcp_modes_withdraw(struct apple_dcp *dcp, u64 generation); int parse_array_count(const struct dcp_parse_ctx *handle, u32 *count); From 8f6ea2ed39806579dae3b355b9fe7ed2d42a0b62 Mon Sep 17 00:00:00 2001 From: DB Date: Fri, 6 Nov 2026 14:11:50 +0000 Subject: [PATCH 17/22] drm: apple: wait for a dcpext display's timings before its first swap A T6031 external session whose first mode was fast hung on its first swap: with 3840x2560@60 (DSC) on the J516C 14.7 image, the firmware dropped swap ID 1 and its A407 never completed, ending the session until reboot: IOMFB: swap_submit_dcp: swallowed swap ID 1 as timinsg are not enabled for External Display DCP call 0x41343037 failed: -110; recovery requires a reboot The firmware starts a display's timings in a deferred run mode change after A472(1) and then sets the boolean property NormalModeActive (D104) to 1; A472(1) sets it to 0 first. With a slow mode (2560x1600@60) the change ends within A472, NormalModeActive already 1 when it replies. With the DSC mode it ends about 20 ms after A472 replied, and the swap sent meanwhile is dropped. Later swaps sent early are held instead, so only a session's first mode failed. Logged on a J516C: 8.597 A472(1) 8.599 D104 NormalModeActive = 0 8.613 A472 replied; A406, A407 sent 8.614 swallowed swap ID 1 as timings are not enabled 8.633 switch to normal mode succeeded; D104 NormalModeActive = 1 After A472(1), wait under the RPC lock for NormalModeActive (service 0) to be 1, handling the queued callbacks as dcp_v14_idle() does, for up to 500 ms. A timeout fails the mode set into the retry path; a failed callback stops the session; a firmware that never reported the property is not waited for. Every power-on is waited for, about 20 ms for the first DSC mode and 10 ms for later ones. On the J516C with a 3840x2560 display (DP over USB-C), six boots (reboot, power cycle, replug) set 3840x2560@60 first and showed its first swap in 18 ms; 14 later mode sets, 3840x2560@120 included, all succeeded. Without the wait, 5 of 5 boots lost the session. The dcp_v14_normal_mode KUnit suite runs the wait and the production D104 handler with a scripted link pump. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WHu9VCTrUXYgBxj2gKTGrp Signed-off-by: DB --- drivers/gpu/drm/apple/iomfb_v14_7.c | 68 ++++++ .../drm/apple/iomfb_v14_7_normal_mode_test.c | 224 ++++++++++++++++++ 2 files changed, 292 insertions(+) create mode 100644 drivers/gpu/drm/apple/iomfb_v14_7_normal_mode_test.c diff --git a/drivers/gpu/drm/apple/iomfb_v14_7.c b/drivers/gpu/drm/apple/iomfb_v14_7.c index 7f79989ecc24e9..9ff665a7e2acc1 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7.c +++ b/drivers/gpu/drm/apple/iomfb_v14_7.c @@ -710,6 +710,8 @@ static int dcp_v14_map_piodma(struct apple_dcp_v14 *v14, u32 id) #define DCP_V14_DESC_TIMEOUT_MS 3000 /* A withdrawn description that is not back by then gets its link redone. */ #define DCP_V14_DESC_RELINK_MS 2000 +/* An external display's timings start this soon after it is powered on. */ +#define DCP_V14_NORMAL_MODE_MS 500 /* An external processor's default stride; no framebuffer is allocated for it. */ #define DCP_V14_EXT_STRIDE (1920 * 4) @@ -3208,6 +3210,68 @@ static int dcp_v14_set_power(struct apple_dcp_v14 *v14, bool on) return ret; } +/* Handles one queued firmware callback, waiting up to @timeout for it. */ +static int dcp_v14_pump(struct apple_dcp_v14 *v14, unsigned long timeout) +{ + KUNIT_STATIC_STUB_REDIRECT(dcp_v14_pump, v14, timeout); + return dcp_v14_link_pump(&v14->link, timeout, dcp_v14_callback, v14); +} + +/* + * Waits after A472(1) until the firmware runs the display's timings. It + * starts them in a deferred run mode change and then sets NormalModeActive + * (D104) to 1; with a DSC mode (3840x2560@60 on the J516C 14.7 image) that + * change ends only after A472 replied, NormalModeActive still 0. The first + * swap of a session sent before then is dropped ("swallowed swap ID 1 as + * timings are not enabled for External Display"), its A407 never completes + * and the session is lost. A slower mode's change ends within A472, which is + * why only a fast first mode failed. Later swaps sent early are held, not + * dropped, but every power-on is waited for alike: it costs one run mode + * change, about 20 ms. + * + * Firmware that has not reported NormalModeActive is not waited for. + * Called with the RPC lock held; the callbacks are handled as by + * dcp_v14_idle(). + */ +static int dcp_v14_external_wait_normal(struct apple_dcp_v14 *v14) +{ + static const u8 key[64] = "NormalModeActive"; + unsigned long deadline = jiffies + msecs_to_jiffies(DCP_V14_NORMAL_MODE_MS); + ktime_t start = ktime_get(); + struct dcp_v14_property *p; + unsigned int pumped = 0; + int ret; + + lockdep_assert_held(&v14->lock); + for (;;) { + p = dcp_v14_property(v14, 0, key, false); + if (IS_ERR_OR_NULL(p) || p->value) { + if (pumped) + dev_dbg(v14->dev, "display timings running %lld us after power-on\n", + ktime_us_delta(ktime_get(), start)); + return 0; + } + if (v14->failed) + return -EIO; + if (!time_before(jiffies, deadline)) + break; + ret = dcp_v14_pump(v14, deadline - jiffies); + pumped++; + if (ret == -ETIMEDOUT) + break; + if (ret && ret != -EAGAIN) { + v14->failed = true; + dev_err(v14->dev, "DCP notification failed: %d; recovery requires a reboot\n", + ret); + dcp_v14_external_stopped(v14); + return ret; + } + } + dev_warn(v14->dev, "display timings not running %u ms after power-on\n", + DCP_V14_NORMAL_MODE_MS); + return -ETIMEDOUT; +} + /* The display is fully described, and no description is in transfer. */ static bool dcp_v14_external_described(struct apple_dcp_v14 *v14) { @@ -3364,6 +3428,9 @@ static int dcp_v14_external_modeset(struct apple_dcp *dcp, struct drm_crtc_state ret = dcp_v14_set_mode(v14, mode.color_mode_id, mode.timing_mode_id); if (!ret) ret = dcp_v14_set_power(v14, true); + /* No swap before the timings run; see dcp_v14_external_wait_normal(). */ + if (!ret) + ret = dcp_v14_external_wait_normal(v14); if (!ret) { v14->panel_width = mode.mode.hdisplay; v14->panel_height = mode.mode.vdisplay; @@ -3477,6 +3544,7 @@ void iomfb_v14_7_poweroff(struct apple_dcp *dcp) #if IS_ENABLED(CONFIG_DRM_APPLE_DISPLAY_KUNIT_TEST) #include "iomfb_v14_7_external_audio_test.c" #include "iomfb_v14_7_shared_colors_test.c" +#include "iomfb_v14_7_normal_mode_test.c" #include "iomfb_v14_7_backlight_test.c" #include "iomfb_v14_7_backlight_lifetime_test.c" #endif diff --git a/drivers/gpu/drm/apple/iomfb_v14_7_normal_mode_test.c b/drivers/gpu/drm/apple/iomfb_v14_7_normal_mode_test.c new file mode 100644 index 00000000000000..7b3924f5c73ca8 --- /dev/null +++ b/drivers/gpu/drm/apple/iomfb_v14_7_normal_mode_test.c @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +/* + * The wait after an external display's power-on for its timings to run, + * included at the end of iomfb_v14_7.c so that the production + * dcp_v14_external_wait_normal() and D104 handler run. Only the link pump + * is stubbed: it delivers the callbacks a case scripts, as the firmware + * queues them after A472 replied. + */ +#include +#include +#include + +/* What the stubbed pump does on each call, in order. */ +enum v14_nm_step { + V14_NM_OTHER, /* an unrelated callback */ + V14_NM_NORMAL_0, /* D104 NormalModeActive = 0 */ + V14_NM_NORMAL_1, /* D104 NormalModeActive = 1 */ + V14_NM_TIMEOUT, /* nothing arrived in time */ + V14_NM_ERROR, /* a callback that failed */ +}; + +struct v14_nm { + struct apple_dcp_v14 *v14; + const enum v14_nm_step *steps; + unsigned int count, pumps; +}; + +static struct v14_nm *v14_nm_current; + +/* D104: a 64-byte key, then the boolean. */ +static int v14_nm_d104(struct apple_dcp_v14 *v14, bool value) +{ + u8 in[68] = {}; + + strscpy((char *)in, "NormalModeActive", 64); + in[64] = value; + return dcp_v14_callback(v14, D(104), in, sizeof(in), NULL, 0); +} + +static int v14_nm_pump(struct apple_dcp_v14 *v14, unsigned long timeout) +{ + struct v14_nm *s = v14_nm_current; + + if (s->pumps == s->count) + return -ETIMEDOUT; + switch (s->steps[s->pumps++]) { + case V14_NM_OTHER: + return 0; + case V14_NM_NORMAL_0: + return v14_nm_d104(v14, false); + case V14_NM_NORMAL_1: + return v14_nm_d104(v14, true); + case V14_NM_TIMEOUT: + return -ETIMEDOUT; + case V14_NM_ERROR: + return -EPROTO; + } + return -EINVAL; +} + +static void v14_nm_stopped(struct work_struct *work) +{ +} + +static void v14_nm_stop(void *data) +{ + struct apple_dcp_v14 *v14 = data; + + cancel_work_sync(&v14->stopped_work); +} + +/* A powered-on external session; the pump plays @steps. */ +static struct v14_nm *v14_nm_setup(struct kunit *test, const enum v14_nm_step *steps, + unsigned int count) +{ + struct v14_nm *s = kunit_kzalloc(test, sizeof(*s), GFP_KERNEL); + struct device *dev; + + KUNIT_ASSERT_NOT_NULL(test, s); + dev = kunit_device_register(test, "dcp-v14-normal-mode"); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, dev); + s->v14 = devm_kzalloc(dev, sizeof(*s->v14), GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, s->v14); + s->v14->dev = dev; + s->v14->external = true; + s->v14->started = true; + s->v14->opened = true; + s->v14->powered = true; + mutex_init(&s->v14->lock); + INIT_WORK(&s->v14->stopped_work, v14_nm_stopped); + KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test, v14_nm_stop, s->v14), 0); + s->steps = steps; + s->count = count; + v14_nm_current = s; + kunit_activate_static_stub(test, dcp_v14_pump, v14_nm_pump); + return s; +} + +static int v14_nm_wait(struct v14_nm *s) +{ + int ret; + + mutex_lock(&s->v14->lock); + ret = dcp_v14_external_wait_normal(s->v14); + mutex_unlock(&s->v14->lock); + return ret; +} + +/* Firmware that never reported NormalModeActive: no wait. */ +static void v14_nm_not_reported(struct kunit *test) +{ + struct v14_nm *s = v14_nm_setup(test, NULL, 0); + + KUNIT_EXPECT_EQ(test, v14_nm_wait(s), 0); + KUNIT_EXPECT_EQ(test, s->pumps, 0U); +} + +/* The run mode change ended within A472 (a slow mode): no wait. */ +static void v14_nm_already_running(struct kunit *test) +{ + struct v14_nm *s = v14_nm_setup(test, NULL, 0); + + KUNIT_ASSERT_EQ(test, v14_nm_d104(s->v14, false), 0); + KUNIT_ASSERT_EQ(test, v14_nm_d104(s->v14, true), 0); + KUNIT_EXPECT_EQ(test, v14_nm_wait(s), 0); + KUNIT_EXPECT_EQ(test, s->pumps, 0U); +} + +/* + * The J516C 3840x2560 DSC sequence: A472 replied with NormalModeActive 0, + * other callbacks follow, then 1. The wait ends on it, not before. + */ +static void v14_nm_deferred(struct kunit *test) +{ + static const enum v14_nm_step steps[] = { + V14_NM_OTHER, V14_NM_OTHER, V14_NM_NORMAL_1, V14_NM_OTHER, + }; + struct v14_nm *s = v14_nm_setup(test, steps, ARRAY_SIZE(steps)); + + KUNIT_ASSERT_EQ(test, v14_nm_d104(s->v14, false), 0); + KUNIT_EXPECT_EQ(test, v14_nm_wait(s), 0); + KUNIT_EXPECT_EQ(test, s->pumps, 3U); + KUNIT_EXPECT_FALSE(test, s->v14->failed); +} + +/* A power-on after the first sets 0 again: it is waited for alike. */ +static void v14_nm_every_power_on(struct kunit *test) +{ + static const enum v14_nm_step steps[] = { V14_NM_NORMAL_1, V14_NM_NORMAL_1 }; + struct v14_nm *s = v14_nm_setup(test, steps, ARRAY_SIZE(steps)); + + KUNIT_ASSERT_EQ(test, v14_nm_d104(s->v14, false), 0); + KUNIT_EXPECT_EQ(test, v14_nm_wait(s), 0); + KUNIT_ASSERT_EQ(test, v14_nm_d104(s->v14, false), 0); + KUNIT_EXPECT_EQ(test, v14_nm_wait(s), 0); + KUNIT_EXPECT_EQ(test, s->pumps, 2U); +} + +/* Only service 0 counts: the IOMF service's NormalModeActive (D415) does not. */ +static void v14_nm_other_service(struct kunit *test) +{ + struct v14_nm *s = v14_nm_setup(test, NULL, 0); + struct dcp_v14_property *p; + static const u8 key[64] = "NormalModeActive"; + + p = dcp_v14_property(s->v14, 1229933894, key, true); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, p); + p->value = 0; + KUNIT_EXPECT_EQ(test, v14_nm_wait(s), 0); + KUNIT_EXPECT_EQ(test, s->pumps, 0U); +} + +/* Timings that never start fail the mode set; the session goes on. */ +static void v14_nm_timeout(struct kunit *test) +{ + static const enum v14_nm_step steps[] = { V14_NM_OTHER, V14_NM_TIMEOUT }; + struct v14_nm *s = v14_nm_setup(test, steps, ARRAY_SIZE(steps)); + + KUNIT_ASSERT_EQ(test, v14_nm_d104(s->v14, false), 0); + KUNIT_EXPECT_EQ(test, v14_nm_wait(s), -ETIMEDOUT); + KUNIT_EXPECT_FALSE(test, s->v14->failed); +} + +/* A callback that fails stops the session, as in dcp_v14_idle(). */ +static void v14_nm_callback_failed(struct kunit *test) +{ + static const enum v14_nm_step steps[] = { V14_NM_ERROR, V14_NM_NORMAL_1 }; + struct v14_nm *s = v14_nm_setup(test, steps, ARRAY_SIZE(steps)); + + KUNIT_ASSERT_EQ(test, v14_nm_d104(s->v14, false), 0); + KUNIT_EXPECT_EQ(test, v14_nm_wait(s), -EPROTO); + KUNIT_EXPECT_TRUE(test, s->v14->failed); + KUNIT_EXPECT_EQ(test, s->pumps, 1U); +} + +/* A session that failed meanwhile is not waited for. */ +static void v14_nm_session_failed(struct kunit *test) +{ + struct v14_nm *s = v14_nm_setup(test, NULL, 0); + + KUNIT_ASSERT_EQ(test, v14_nm_d104(s->v14, false), 0); + s->v14->failed = true; + KUNIT_EXPECT_EQ(test, v14_nm_wait(s), -EIO); + KUNIT_EXPECT_EQ(test, s->pumps, 0U); +} + +static struct kunit_case v14_nm_cases[] = { + KUNIT_CASE(v14_nm_not_reported), + KUNIT_CASE(v14_nm_already_running), + KUNIT_CASE(v14_nm_deferred), + KUNIT_CASE(v14_nm_every_power_on), + KUNIT_CASE(v14_nm_other_service), + KUNIT_CASE(v14_nm_timeout), + KUNIT_CASE(v14_nm_callback_failed), + KUNIT_CASE(v14_nm_session_failed), + {} +}; + +static struct kunit_suite v14_nm_suite = { + .name = "dcp_v14_normal_mode", + .test_cases = v14_nm_cases, +}; + +kunit_test_suite(v14_nm_suite); From 6d3d1c3d35355c08f42a051a0211c8202d618e4f Mon Sep 17 00:00:00 2001 From: Daniel Lemky Date: Fri, 6 Nov 2026 23:30:53 -0500 Subject: [PATCH 18/22] arm64: dts: apple: complete J514M display route endpoint definitions The in-flight T6034 display profile includes t6031-dcp.dtsi, whose external routes reference the Type-C endpoints. Include the same endpoint definitions already used by J514C to resolve the twelve undefined phandles when combining PR372 with the current display base. This supplies graph endpoints without enabling a processor. Keep this integration correction separate from the J516M board work. Assisted-by: Codex:GPT-6 Signed-off-by: Daniel Lemky --- arch/arm64/boot/dts/apple/t6034-j514m.dts | 1 + 1 file changed, 1 insertion(+) diff --git a/arch/arm64/boot/dts/apple/t6034-j514m.dts b/arch/arm64/boot/dts/apple/t6034-j514m.dts index 790319c2db3f21..030b9c1dcdf5d9 100644 --- a/arch/arm64/boot/dts/apple/t6034-j514m.dts +++ b/arch/arm64/boot/dts/apple/t6034-j514m.dts @@ -12,6 +12,7 @@ #include "t6034.dtsi" #include "t6034-dcp.dtsi" #include "t603x-j514-j516.dtsi" +#include "t603x-j514c-j516c-dcpext.dtsi" / { compatible = "apple,j514m", "apple,t6034", "apple,arm-platform"; From 727efc7f628c9b1b406fce5338bb7b9498437cf3 Mon Sep 17 00:00:00 2001 From: Daniel Lemky Date: Fri, 6 Nov 2026 23:28:03 -0500 Subject: [PATCH 19/22] drm/apple: add opt-in J516M native panel and HDMI admission Reuse the ADT-confirmed T6031 display layout on the J516M/T6034, with its exact board/SoC and pinned 14.7 DCP image. Retain the working T6031 handoff namespace separately from J514M and require explicit display and dcpext owner choices. Only dcpext0 is qualified. Enable the existing swap timestamp behavior for the 120 Hz internal panel and carry the J516M panel admission cases into the existing host fixture. Other boards and their default policy are unchanged. Assisted-by: Codex:GPT-6 Signed-off-by: Daniel Lemky --- .../admin-guide/kernel-parameters.txt | 5 ++ arch/arm64/boot/dts/apple/t6034-j516m.dts | 30 ++++++++++++ drivers/gpu/drm/apple/dcp-fabric.c | 6 ++- drivers/gpu/drm/apple/iomfb_v14_7.c | 19 ++++++++ drivers/mux/apple-display-crossbar.c | 3 +- drivers/phy/apple/atc.c | 3 +- drivers/soc/apple/t6030-display-gate.c | 47 +++++++++++++++++++ tools/asahi/dcp14-panel-tests.py | 25 ++++++++-- 8 files changed, 131 insertions(+), 7 deletions(-) diff --git a/Documentation/admin-guide/kernel-parameters.txt b/Documentation/admin-guide/kernel-parameters.txt index c5e4ab0ba915f0..ad55132f6c4ef2 100644 --- a/Documentation/admin-guide/kernel-parameters.txt +++ b/Documentation/admin-guide/kernel-parameters.txt @@ -549,6 +549,11 @@ Kernel parameters apm= [APM] Advanced Power Management See header of arch/x86/kernel/apm_32.c. + apple_t6034_display.dcpext= [ARM64] + Enable the opt-in J516M/T6034 external display handoff. + Requires apple_t6034_display.enable=1 and matching + boot-loader handoff metadata. Default: disabled. + apparmor= [APPARMOR] Disable or enable AppArmor at boot time Format: { "0" | "1" } See security/apparmor/Kconfig help text diff --git a/arch/arm64/boot/dts/apple/t6034-j516m.dts b/arch/arm64/boot/dts/apple/t6034-j516m.dts index 22ad5251eaecef..4b3c46a2162fc2 100644 --- a/arch/arm64/boot/dts/apple/t6034-j516m.dts +++ b/arch/arm64/boot/dts/apple/t6034-j516m.dts @@ -10,7 +10,10 @@ /dts-v1/; #include "t6034.dtsi" +#include "t6031-dcp.dtsi" #include "t603x-j514-j516.dtsi" +#include "t603x-j514c-j516c-dcpext.dtsi" +#include "t603x-j514c-j516c-hdmi.dtsi" / { compatible = "apple,j516m", "apple,t6034", "apple,arm-platform"; @@ -38,3 +41,30 @@ compatible = "apple,j516-macaudio", "apple,j316-macaudio", "apple,macaudio"; model = "MacBook Pro J516"; }; + +/* + * J516M ADT confirms the J516C display register and interrupt layout. + * Only dcpext0 has been qualified, using direct HDMI. + */ +&clk_disp0 { + /* + * Preserve the captured boot-clock metadata, as the J514M path does. + * Firmware warns that this is below its video clock; + * that warning is not proof of the programmed clock or a startup veto. + * The metadata does not qualify sustained power or thermal behavior. + */ + clock-frequency = <400000000>; +}; + +&dcp { + panel { + compatible = "apple,panel-j516", "apple,panel-mini-led", "apple,panel"; + width-mm = <346>; + height-mm = <223>; + adj-height-mm = <216>; + }; +}; + +&dcpext1 { status = "fail"; }; +&dcpext2 { status = "fail"; }; +&dcpext3 { status = "fail"; }; diff --git a/drivers/gpu/drm/apple/dcp-fabric.c b/drivers/gpu/drm/apple/dcp-fabric.c index e85ce95a6bdc10..66d696a73a786d 100644 --- a/drivers/gpu/drm/apple/dcp-fabric.c +++ b/drivers/gpu/drm/apple/dcp-fabric.c @@ -117,7 +117,8 @@ bool dcp_is_usb4_output(struct apple_dcp *dcp) static bool dcp_fabric_t603x(void) { return of_machine_is_compatible("apple,t6030") || - of_machine_is_compatible("apple,t6031"); + of_machine_is_compatible("apple,t6031") || + of_machine_is_compatible("apple,j516m"); } /* SoC flow support and board wiring are separate qualifications. */ @@ -1776,7 +1777,8 @@ int dcp_direct_crossbar_link(struct apple_dcp *dcp, bool up) /* The crossbar driver runs both in its T6030 mode (t6030_dpin). */ if (!of_machine_is_compatible("apple,t6030") && - !of_machine_is_compatible("apple,t6031")) + !of_machine_is_compatible("apple,t6031") && + !of_machine_is_compatible("apple,j516m")) return 0; guard(mutex)(&dcp->tb_lock); route = dcp->active_typec_route; diff --git a/drivers/gpu/drm/apple/iomfb_v14_7.c b/drivers/gpu/drm/apple/iomfb_v14_7.c index 89c7ce84bff4b7..0bb5b58c6cdefd 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7.c +++ b/drivers/gpu/drm/apple/iomfb_v14_7.c @@ -197,6 +197,24 @@ static const struct dcp_v14_board dcp_v14_board_j516c = { .panel_nits = true, }; +/* J516M uses the J516C image; its first boot offered only a 120 Hz panel timing. */ +static const struct dcp_v14_board dcp_v14_board_j516m = { + .name = "J516M", + .debugfs = "dcp-j516m", + .dcp_compatible = "apple,t6031-dcp", + .machine = "apple,j516m", + .soc = "apple,t6034", + .firmware_uuid = DCP_V14_J516C_FIRMWARE_UUID, + .handoff = "apple,t6031-handoff", + .panel_width = 3456, + .panel_height = 2234, + .promotion = true, + .log_bringup = true, + .base_edid = true, + /* Same pinned DCP image as J516C: zero stamps cap this panel at 60 fps. */ + .swap_timestamps = true, +}; + /* * J514C (MacBook Pro 14", M3 Max): the T6031 DCP running the J516C image * (the DCP image is the SoC's; its UUID is checked at probe), in the J514S @@ -236,6 +254,7 @@ static const struct dcp_v14_board *const dcp_v14_boards[] = { &dcp_v14_board_j516c, &dcp_v14_board_j514c, &dcp_v14_board_j514m, + &dcp_v14_board_j516m, }; struct dcp_v14_property { diff --git a/drivers/mux/apple-display-crossbar.c b/drivers/mux/apple-display-crossbar.c index ff5803453abee4..ff5d48c321beee 100644 --- a/drivers/mux/apple-display-crossbar.c +++ b/drivers/mux/apple-display-crossbar.c @@ -1005,7 +1005,8 @@ static int apple_dpxbar_probe(struct platform_device *pdev) dpxbar->dev = dev; /* T6031 (M3 Max) has T6030's dispext pipes and the same crossbar. */ dpxbar->t6030_dpin = of_machine_is_compatible("apple,t6030") || - of_machine_is_compatible("apple,t6031"); + of_machine_is_compatible("apple,t6031") || + of_machine_is_compatible("apple,j516m"); dpxbar->regs = devm_platform_ioremap_resource(pdev, 0); if (IS_ERR(dpxbar->regs)) return PTR_ERR(dpxbar->regs); diff --git a/drivers/phy/apple/atc.c b/drivers/phy/apple/atc.c index eefc4f1c539e00..b576b7e59b97e8 100644 --- a/drivers/phy/apple/atc.c +++ b/drivers/phy/apple/atc.c @@ -3226,7 +3226,8 @@ static int atc_tunnel_set_t602x(struct apple_atcphy *atcphy, unsigned int dpin, static bool apple_atc_tunnel_is_t6030(struct apple_atcphy *atcphy) { return (of_machine_is_compatible("apple,t6030") || - of_machine_is_compatible("apple,t6031")) && + of_machine_is_compatible("apple,t6031") || + of_machine_is_compatible("apple,j516m")) && atcphy->hw->gen == ATCPHY_GENERATION_T8122; } diff --git a/drivers/soc/apple/t6030-display-gate.c b/drivers/soc/apple/t6030-display-gate.c index 47e349a60b1286..85408f34f97689 100644 --- a/drivers/soc/apple/t6030-display-gate.c +++ b/drivers/soc/apple/t6030-display-gate.c @@ -374,10 +374,48 @@ static const struct gate_soc gate_t6034 __initconst = { .fw_uuids = "asahi,t6034-fw-uuids", }; +static bool gate_j516m_requested __initdata; +static bool gate_j516m_dcpext_requested __initdata; + +/* ADT-confirmed T6031 display layout, admitted only on the J516M. */ +static const struct gate_soc gate_j516m __initconst = { + .machine = "apple,j516m", + .prefix = "apple-t6034-display: ", + .name = "T6034", + .dcp_compat = "apple,t6031-dcp", + .display_compat = "apple,t6031-display-subsystem", + .marker = "apple,t6031-handoff", + .pwrstate_compat = "apple,t6031-pmgr-pwrstate", + .dcp_full_name = "dcp@386c00000", + .pmp_values = "/chosen/asahi,t6031-pmp", + .pmp_uuid = gate_t6031_pmp_uuid, + .report_compat = "apple,t6031-pmp-v2-report", + .disp_entry = "report@10", + .reuse_pmp = true, + .pmp_compat = "apple,t6031-pmp-v2", + .ans_entry = "report@21", + .requested = &gate_j516m_requested, + .opt_in = "apple_t6034_display.enable=1", + .ps_chain = { PMP_PS_DISP_FE, PMP_PS_DISP_CPU, PMP_PS_DISP_SYS }, + .pmp_ps_any_pmgr = true, + .describe = true, + .fw_uuids = "asahi,t6031-fw-uuids", + /* + * dcpext0..3 as on T6030, handed over by the boot loader with the T6030 + * markers, each on its own dispextN_fe power state. + */ + .dcpext = true, + .dcpext_compat = "apple,t6031-dcpext", + .dcpext_requested = &gate_j516m_dcpext_requested, + .dcpext_fe_first = true, + .dcpext_opt_in = "apple_t6034_display.dcpext=1", +}; + static const struct gate_soc *const gate_socs[] __initconst = { &gate_t6030, &gate_t8122, &gate_t6031, + &gate_j516m, &gate_t6034, }; @@ -462,6 +500,12 @@ static int __init gate_setup_t6034(char *arg) } early_param("apple_t6034_display.enable", gate_setup_t6034); +static int __init gate_setup_j516m_dcpext(char *arg) +{ + return kstrtobool(arg, &gate_j516m_dcpext_requested); +} +early_param("apple_t6034_display.dcpext", gate_setup_j516m_dcpext); + /* Returns the only node compatible with @compat, or NULL if there are none or several. */ static struct device_node *__init gate_find_one(const char *compat) { @@ -2102,6 +2146,9 @@ static void __init gate_run(void) board); } + if (gate_soc == &gate_j516m) + gate_j516m_requested = gate_t6034_switch > 0; + if (gate_soc == &gate_t6034) { if (!gate_t6034_default_board()) { gate_info("unsupported board; native display stays disabled\n"); diff --git a/tools/asahi/dcp14-panel-tests.py b/tools/asahi/dcp14-panel-tests.py index 19c02cafd82010..8efbbcea6419ab 100755 --- a/tools/asahi/dcp14-panel-tests.py +++ b/tools/asahi/dcp14-panel-tests.py @@ -36,7 +36,10 @@ struct device {struct device_node *of_node;}; static const char *machine; static bool of_device_is_compatible(const struct device_node *n,const char *c){return !strcmp(n->compatible,c);} - static bool of_machine_is_compatible(const char*c){return !strcmp(machine,c);} + static bool of_machine_is_compatible(const char*c){ + if (!strcmp(machine,c)) return true; + return (!strcmp(machine,"apple,j514m") || !strcmp(machine,"apple,j516m")) && !strcmp(c,"apple,t6034"); + } static struct device_node fb={"simple-framebuffer"},dcp={"apple,t8122-dcp"}; static u32 width,height,stride,notch,marker_value=1; static const char *image_uuid; @@ -65,6 +68,9 @@ defines='\n'.join(l for l in src.splitlines() if l.startswith('#define DCP_V14_') and 'FIRMWARE_UUID' in l)+'\n' sel_start='const struct dcp_v14_board *dcp_v14_board_select(' if 'const struct dcp_v14_board *dcp_v14_board_select(' in src else 'const struct dcp_v14_board *iomfb_v14_7_board(' sel=src[src.index(sel_start):src.index('const char *iomfb_v14_7_board_name(')] + if 'bool dcp_v14_board_stamps_swaps(' in src: + stamp_start=src.index('bool dcp_v14_board_stamps_swaps(') + sel+=src[stamp_start:src.index('\n}',stamp_start)+2]+'\n' geo=src[src.index('static int dcp_v14_geometry('):src.index('/* An external processor\'s firmware:')] driver='''struct apple_dcp_v14 {struct device *dev;const struct dcp_v14_board *board;u32 fb_width,fb_height,stride,notch_rows,panel_width,panel_height;};\n''' probe=src[src.index('int iomfb_v14_7_probe('):] @@ -155,9 +161,22 @@ dcp.compatible="apple,t8122-dcp";assert(IS_ERR(iomfb_v14_7_board(&dev))); dcp.compatible="apple,t6031-dcp";machine="apple,j514s";assert(IS_ERR(iomfb_v14_7_board(&dev))); machine="apple,j613";assert(IS_ERR(iomfb_v14_7_board(&dev))); - machine="apple,j516m";assert(IS_ERR(iomfb_v14_7_board(&dev))); + machine="apple,j516m"; + const struct dcp_v14_board*m16=iomfb_v14_7_board(&dev); + assert(!IS_ERR(m16)&&m16&&!strcmp(m16->name,"J516M")); + assert(m16->promotion&&m16->swap_timestamps&&!m16->ctm_set); + assert(dcp_v14_board_stamps_swaps(m16,false)); + assert(!dcp_v14_board_stamps_swaps(m16,true)); + assert(!dcp_v14_board_stamps_swaps(NULL,false)); + image_uuid="F493F03E-CC40-3D89-9A08-5F259BA2B246"; + assert(probe_gate(&dev,m16)==0); + image_uuid="DDF38191-93B3-324A-BC8F-643006F5AC82"; + assert(probe_gate(&dev,m16)==-ENODEV); + marker_value=0;image_uuid=m16->firmware_uuid; + assert(probe_gate(&dev,m16)==-ENODEV);marker_value=1; + dcp.compatible="apple,t6030-dcp";machine="apple,j516s";assert(iomfb_v14_7_board(&dev)==&dcp_v14_board_t6030); - puts("current J615/J613/J504/T6030/J516C/J514C admission + hidden/full notch, wrong geometry controls: PASS"); + puts("current J615/J613/J504/T6030/J516C/J514C/J516M admission + hidden/full notch, wrong geometry controls: PASS"); #endif } ''' From 272cd802c034268f3f002151258ee8d1a054f3ef Mon Sep 17 00:00:00 2001 From: Daniel Lemky Date: Fri, 6 Nov 2026 23:28:24 -0500 Subject: [PATCH 20/22] drm/asahi: admit the measured J516M T6034 GPU profile Share the existing six-channel T6034 G15C hardware layout while keeping J516M machine and full firmware admission independent of J514M. Require the measured 30-core fused identity 0x7b5ed7f5fd and retain strict refusal of the generic firmware-prefix override. Link J516M GPU startup to its T6031 display supplier; keep J514M on its separate T6034 supplier. Extend the existing profile and display order fixtures with the prior J516M admission cases. Inherited J516C power/reference values remain thermally unqualified. Assisted-by: Codex:GPT-6 Signed-off-by: Daniel Lemky --- drivers/gpu/drm/apple/apple_drv.c | 3 +++ drivers/gpu/drm/asahi/g15/boot.rs | 15 +++++++++++-- drivers/gpu/drm/asahi/g15/glue.c | 3 ++- drivers/gpu/drm/asahi/g15/hw.rs | 2 ++ drivers/gpu/drm/asahi/g15/hw/j516m.rs | 30 ++++++++++++++++++++++++++ drivers/gpu/drm/asahi/g15/mod.rs | 5 +++++ tools/asahi/g15-display-order-tests.py | 10 ++++++--- tools/asahi/g15-t6034-profile-tests.py | 14 +++++++++--- tools/aurora-ci/host-tests.json | 2 +- 9 files changed, 74 insertions(+), 10 deletions(-) create mode 100644 drivers/gpu/drm/asahi/g15/hw/j516m.rs diff --git a/drivers/gpu/drm/apple/apple_drv.c b/drivers/gpu/drm/apple/apple_drv.c index b6a5be040ef9c8..9028388b33915b 100644 --- a/drivers/gpu/drm/apple/apple_drv.c +++ b/drivers/gpu/drm/apple/apple_drv.c @@ -139,6 +139,9 @@ EXPORT_SYMBOL_GPL(apple_drm_t6031_link_device); int apple_drm_t6034_link_device(struct device *consumer) { + if (of_machine_is_compatible("apple,j516m")) + return apple_g15_link_device(consumer, "apple,t6034", "apple,agx-t6034", + &apple_t6031_display); if (!of_machine_is_compatible("apple,j514m")) return -ENODEV; return apple_g15_link_device(consumer, "apple,t6034", "apple,agx-t6034", diff --git a/drivers/gpu/drm/asahi/g15/boot.rs b/drivers/gpu/drm/asahi/g15/boot.rs index ed42af3f7d3d63..09b314a77007ae 100644 --- a/drivers/gpu/drm/asahi/g15/boot.rs +++ b/drivers/gpu/drm/asahi/g15/boot.rs @@ -481,6 +481,10 @@ pub(crate) fn admit( && cfg.platform == hw::Platform::DeviceTree && glue::machine(kernel::c_str!("apple,j514m")) && glue::machine(kernel::c_str!("apple,t6034")); + let j516m_board = cfg.chip_id == 0x6034 + && cfg.platform == hw::Platform::DeviceTree + && glue::machine(kernel::c_str!("apple,j516m")) + && glue::machine(kernel::c_str!("apple,t6034")); if pmp { if !glue::board() { return Err(ENODEV); @@ -495,7 +499,7 @@ pub(crate) fn admit( } return Err(err); } - } else if (cfg.chip_id == 0x6034 && !j514m_board) + } else if (cfg.chip_id == 0x6034 && !j514m_board && !j516m_board) || (!glue::machine(cfg.machine) && !j514c_board) { dev_err!(dev, "G15: not a {} machine\n", cfg.name); @@ -512,7 +516,7 @@ pub(crate) fn admit( return Err(err); } } - if j514c_board || j514m_board { + if j514c_board || j514m_board || j516m_board { if let hw::Board::DeviceTree(reference) = cfg.board { dev_warn!( dev, @@ -682,6 +686,13 @@ pub(crate) fn start( ); return Err(eproto()); } + if cfg.chip_id == 0x6034 + && glue::machine(kernel::c_str!("apple,j516m")) + && !hw::j516m::identity_matches(id.version, id.counts, id.cores) + { + dev_err!(d, "J516M powered identity differs from the measured bin\n"); + return Err(eproto()); + } // Binning: any non-empty enabled set in each cluster, nothing above the core slots. let clusters = cfg.num_clusters as usize; let slots = cfg.core_slots(); diff --git a/drivers/gpu/drm/asahi/g15/glue.c b/drivers/gpu/drm/asahi/g15/glue.c index 72d74a7218fa3f..ef32a03eae557c 100644 --- a/drivers/gpu/drm/asahi/g15/glue.c +++ b/drivers/gpu/drm/asahi/g15/glue.c @@ -167,7 +167,8 @@ int asahi_g15_display_prepare(struct device *dev) name = "T6031"; ret = apple_drm_t6031_link_device(dev); } else if (of_device_is_compatible(dev->of_node, "apple,agx-t6034")) { - compatible = "apple,t6034-display-subsystem"; + compatible = of_machine_is_compatible("apple,j516m") ? + "apple,t6031-display-subsystem" : "apple,t6034-display-subsystem"; name = "T6034"; ret = apple_drm_t6034_link_device(dev); } else { diff --git a/drivers/gpu/drm/asahi/g15/hw.rs b/drivers/gpu/drm/asahi/g15/hw.rs index f7f00f729660a1..de4be2e5c2b647 100644 --- a/drivers/gpu/drm/asahi/g15/hw.rs +++ b/drivers/gpu/drm/asahi/g15/hw.rs @@ -11,6 +11,8 @@ use core::ops::Range; pub(crate) mod t6031; #[cfg(CONFIG_DRM_ASAHI_G15)] pub(crate) mod t6034; +#[cfg(CONFIG_DRM_ASAHI_G15)] +pub(crate) mod j516m; /// Identification register values of a qualified G15S. pub(crate) struct HwId { diff --git a/drivers/gpu/drm/asahi/g15/hw/j516m.rs b/drivers/gpu/drm/asahi/g15/hw/j516m.rs new file mode 100644 index 00000000000000..642abbd0999c3d --- /dev/null +++ b/drivers/gpu/drm/asahi/g15/hw/j516m.rs @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT + +//! Measured J516M/T6034 30-core profile. Share the six-channel G15C layout +//! with J514M, but retain independent machine, firmware and fused-core admission. +//! Native rendering is verified; inherited J516C power/reference values remain +//! unqualified for sustained power and thermal behavior on this board. + +use super::super::Config; +use super::FirmwareBuild; + +pub(crate) const CONFIG: Config = Config { + machine: kernel::c_str!("apple,j516m"), + firmware: &[FirmwareBuild { + name: "J516M RTKit 2419.140.12 (power qualification pending)", + machine: Some(kernel::c_str!("apple,j516m")), + text_size: 0x64000, + data_size: 0x140000, + marker: 0x5cf28, + text_sha256: [ + 0x4c, 0xa2, 0x4b, 0x65, 0x8f, 0x8e, 0xbe, 0x7c, 0x27, 0x37, 0x5f, 0xdf, 0x38, 0xe4, + 0x1e, 0x5d, 0xaf, 0x86, 0x32, 0x51, 0x50, 0x77, 0xb5, 0x37, 0xca, 0x19, 0xe6, 0xd0, + 0xdc, 0x77, 0x04, 0xb7, + ], + }], + ..super::t6034::CONFIG +}; + +pub(crate) fn identity_matches(version: u32, counts: u32, cores: u64) -> bool { + version == 0x07041100 && counts == 0x0021040a && cores == 0x7b5ed7f5fd +} diff --git a/drivers/gpu/drm/asahi/g15/mod.rs b/drivers/gpu/drm/asahi/g15/mod.rs index d3600c1f0f4174..53c28957cdfc8e 100644 --- a/drivers/gpu/drm/asahi/g15/mod.rs +++ b/drivers/gpu/drm/asahi/g15/mod.rs @@ -203,6 +203,11 @@ impl platform::Driver for Driver<'_> { return Err(EPROBE_DEFER); } let cfg = *info.ok_or(ENODEV)?; + let cfg = if cfg.chip_id == 0x6034 && boot::glue::machine(c_str!("apple,j516m")) { + &hw::j516m::CONFIG + } else { + cfg + }; dev_info!(pdev.as_ref(), "G15: probing ({})\n", cfg.name); match crate::driver::AsahiPlatformDriver::probe_g15(pdev, cfg) { Ok(driver) => Some(driver), diff --git a/tools/asahi/g15-display-order-tests.py b/tools/asahi/g15-display-order-tests.py index 6ff26493965c60..8d18710802b587 100644 --- a/tools/asahi/g15-display-order-tests.py +++ b/tools/asahi/g15-display-order-tests.py @@ -221,9 +221,13 @@ def c_source(root): CHECK(publish(&d34) == 0 && d34.refs == 1); CHECK(prepare(&g34) == 0 && links_made == 1 && last_link.supplier == &d34); machine("apple,t6034", "apple,j516m"); - CHECK(prepare(&g34) == -ENODEV && links_made == 1); + CHECK(prepare(&g34) == 0 && links_made == 2 && last_link.supplier == &d31); + withdraw(&d31); + CHECK(prepare(&g34) == -EPROBE_DEFER && links_made == 2); + machine("apple,t6034", "apple,j999"); + CHECK(prepare(&g34) == -ENODEV && links_made == 2); machine("apple,t6031", "apple,j514m"); - CHECK(prepare(&g34) == -ENODEV && links_made == 1); + CHECK(prepare(&g34) == -ENODEV && links_made == 2); machine("apple,t6034", "apple,j514m"); withdraw(&d34); CHECK(!d34.refs && prepare(&g34) == -EPROBE_DEFER); @@ -328,7 +332,7 @@ def c_source(root): mod glue { use super::{call, STATE}; pub fn board() -> bool { call("board"); STATE.with(|s| s.borrow().board) } - pub fn machine(name: &str) -> bool { if !["apple,j514c", "apple,t6031", "apple,j514m", "apple,t6034"].contains(&name) { call("machine"); } STATE.with(|s| { let s=s.borrow(); match name { "apple,j514c" => s.j514c, "apple,t6031" => s.t6031, "apple,j514m" => s.j514m, "apple,t6034" => s.t6034, _ => s.machine } }) } + pub fn machine(name: &str) -> bool { if !["apple,j514c", "apple,t6031", "apple,j514m", "apple,j516m", "apple,t6034"].contains(&name) { call("machine"); } STATE.with(|s| { let s=s.borrow(); match name { "apple,j514c" => s.j514c, "apple,t6031" => s.t6031, "apple,j514m" => s.j514m, "apple,j516m" => false, "apple,t6034" => s.t6034, _ => s.machine } }) } pub unsafe fn asahi_g15_pmp_prepare(_: *mut core::ffi::c_void) -> core::ffi::c_int { call("pmp"); STATE.with(|s| s.borrow().pmp) } pub unsafe fn asahi_g15_display_prepare(_: *mut core::ffi::c_void) -> core::ffi::c_int { call("display"); STATE.with(|s| s.borrow().display) } pub fn dma_coherent(_: &super::device::Device) -> bool { call("coherent"); false } diff --git a/tools/asahi/g15-t6034-profile-tests.py b/tools/asahi/g15-t6034-profile-tests.py index 4f73b5172aee76..90605f84d1240b 100644 --- a/tools/asahi/g15-t6034-profile-tests.py +++ b/tools/asahi/g15-t6034-profile-tests.py @@ -3,7 +3,7 @@ """Compile actual G15C profiles and test T6034 shape and exact firmware admission. The diagnostic profile has no firmware row. Once a measured row is present, -exercise its complete public identity and J514M binding without reading an image. +exercise the complete public identities and J514M/J516M bindings without reading an image. """ from pathlib import Path import subprocess @@ -47,6 +47,7 @@ def item(text, marker): @TYPES@ #[path="@T6031@"] pub(crate) mod t6031; #[path="@T6034@"] pub(crate) mod t6034; + #[path="@J516M@"] pub(crate) mod j516m; } } fn main() { @@ -107,7 +108,7 @@ def item(text, marker): assert_eq!(t6031::IOMAPS.iter().find(|m|m.slot==11).unwrap().map_bytes,8*0xd8000); } ''' -for token,replacement in {'@CONFIG@':config,'@REFERENCE@':reference,'@VALUE@':value,'@TYPES@':types,'@T6031@':str(G15/'hw/t6031.rs'),'@T6034@':str(G15/'hw/t6034.rs'),'@MATCHING@':str(G15/'identity/matching.rs'),'@LOOKUP@':str(G15/'identity/lookup.rs')}.items():source=source.replace(token,replacement) +for token,replacement in {'@CONFIG@':config,'@REFERENCE@':reference,'@VALUE@':value,'@TYPES@':types,'@T6031@':str(G15/'hw/t6031.rs'),'@T6034@':str(G15/'hw/t6034.rs'),'@J516M@':str(G15/'hw/j516m.rs'),'@MATCHING@':str(G15/'identity/matching.rs'),'@LOOKUP@':str(G15/'identity/lookup.rs')}.items():source=source.replace(token,replacement) identity=(G15/'identity.rs').read_text() # Run the production firmware admission against synthetic public identity bytes. firmware=item(identity,'pub(crate) fn firmware(') @@ -156,8 +157,15 @@ def item(text, marker): } ''' for token,replacement in {'FIRMWARE':firmware,'MATCHING':str(G15/'identity/matching.rs'),'LOOKUP':str(G15/'identity/lookup.rs')}.items():identity_source=identity_source.replace(token,replacement) +j516m_source = source.replace('let c=&t6034::CONFIG;', 'let c=&j516m::CONFIG;') +j516m_source = j516m_source.replace('use g15::hw::{t6031,t6034};', 'use g15::hw::{t6031,j516m};') +j516m_source = j516m_source.replace('apple,j514m', 'apple,j516m') +j516m_source = j516m_source.replace('b"apple,j516m",b"apple,j514s"', 'b"apple,j514m",b"apple,j514s"') +j516m_source = j516m_source.replace('0x7b5fc7f5fd', '0x7b5ed7f5fd') +j516m_source = j516m_source.replace('[0x1fd,0x1fd,0x1fc,0x1ed]', '[0x1fd,0x1fd,0x1ed,0x1ed]') +j516m_source = j516m_source.replace(' assert_eq!((c.hw_ids', ' assert!(j516m::identity_matches(0x07041100,0x0021040a,0x7b5ed7f5fd));\n assert!(!j516m::identity_matches(0x07041200,0x0021040a,0x7b5ed7f5fd));\n assert!(!j516m::identity_matches(0x07041100,0x0001040a,0x7b5ed7f5fd));\n assert!(!j516m::identity_matches(0x07041100,0x0021040a,0x7b5fc7f5fd));\n' + ' assert_eq!((c.hw_ids', 1) with tempfile.TemporaryDirectory(prefix='t6034-profile-') as temp: - for name,code in [('profile',source),('measurement-refusal',identity_source)]: + for name,code in [('profile-j514m',source),('profile-j516m',j516m_source),('measurement-refusal',identity_source)]: p=Path(temp)/name;p.with_suffix('.rs').write_text(code) subprocess.run(['rustc','--edition=2021','--cfg','CONFIG_DRM_ASAHI_G15','-o',str(p),str(p.with_suffix('.rs'))],check=True) subprocess.run([str(p)],check=True) diff --git a/tools/aurora-ci/host-tests.json b/tools/aurora-ci/host-tests.json index fb9cdbdd66fc69..1a48d610093964 100644 --- a/tools/aurora-ci/host-tests.json +++ b/tools/aurora-ci/host-tests.json @@ -2226,7 +2226,7 @@ "tools/asahi/g15-t6034-profile-tests.py" ], "rust": "1.93.1", - "note": "Production G15C profiles, exact T6034 topology/six memory channels, full J514M firmware tuple and wrong-board/field refusal; strict nonempty and empty profiles never consult prefix overrides, while older policy remains unchanged. Public identity inputs only." + "note": "Production G15C profiles, exact T6034 topology/six memory channels, full J514M/J516M firmware tuples and wrong-board/field refusal; strict nonempty and empty profiles never consult prefix overrides, while older policy remains unchanged. Public identity inputs only." }, { "name": "tools/asahi/t6034-display-tests.py", From 86db3c959abe96eb0f6c71368e9ecfa9fc5ef9ea Mon Sep 17 00:00:00 2001 From: Daniel Lemky Date: Fri, 6 Nov 2026 23:28:24 -0500 Subject: [PATCH 21/22] pmdomain: apple: allow the J516M T6034 PMP bandwidth request Extend the T6031 GFX bandwidth helper to the exact J516M/T6034 root pair. This board uses the same report layout and the existing GFX request/ack protocol. Keep other T6034 boards outside this guard and retain the existing supplier readiness and timeout behavior. Assisted-by: Codex:GPT-6 Signed-off-by: Daniel Lemky --- drivers/pmdomain/apple/pmp-report.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/drivers/pmdomain/apple/pmp-report.c b/drivers/pmdomain/apple/pmp-report.c index caf5b25dacfe48..0d0192a803005f 100644 --- a/drivers/pmdomain/apple/pmp-report.c +++ b/drivers/pmdomain/apple/pmp-report.c @@ -1388,7 +1388,9 @@ int apple_pmp_report_t6031_gfx(bool enable) int len = 0, ret = -ENODEV; if (!IS_ENABLED(CONFIG_APPLE_T6031_GPU_PMP_REPORT) || - !of_machine_is_compatible("apple,t6031")) + (!of_machine_is_compatible("apple,t6031") && + !(of_machine_is_compatible("apple,t6034") && + of_machine_is_compatible("apple,j516m")))) return -ENODEV; rcu_read_lock(); From 721a41ccb47c1754de23529bb47d87c3f7be83ae Mon Sep 17 00:00:00 2001 From: Daniel Lemky Date: Sat, 7 Nov 2026 20:42:40 -0500 Subject: [PATCH 22/22] pmdomain: apple: select J516M's display recovery switch J516M uses the T6031 PMP report layout, whose fallback tells users to boot with apple_t6031_display.enable=0 after startup fails. Its display gate only honors apple_t6034_display.enable, so that advice can leave a retained T6034 enable active and retry the failed startup. Select apple_t6034_display.enable=0 for the exact J516M/T6034 machine pair. Keep other report profiles' recovery switches and the nomodeset alternative unchanged. No PMP startup or report protocol changes. Fixes: 727efc7f628c9b1b406fce5338bb7b9498437cf3 ("drm/apple: add opt-in J516M native panel and HDMI admission") Assisted-by: Codex:GPT-6 Signed-off-by: Daniel Lemky --- drivers/pmdomain/apple/pmp-report.c | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/drivers/pmdomain/apple/pmp-report.c b/drivers/pmdomain/apple/pmp-report.c index 0d0192a803005f..cc111ab6c46927 100644 --- a/drivers/pmdomain/apple/pmp-report.c +++ b/drivers/pmdomain/apple/pmp-report.c @@ -699,10 +699,18 @@ static void apple_pmp_start_finish(struct apple_pmp_start *st, int result) st->result = result; complete_all(&st->started); } - if (result && READ_ONCE(st->enabled)) + if (result && READ_ONCE(st->enabled)) { + const char *fallback = st->rep->offsets->fallback; + + /* J516M shares the T6031 report layout, but has its own display gate. */ + if (of_machine_is_compatible("apple,t6034") && + of_machine_is_compatible("apple,j516m")) + fallback = "apple_t6034_display.enable=0"; + dev_err(st->rep->dev, "%s PMP start failed (%d) after the PMP was enabled: it may keep running until the next reboot. Reboot to stop it; to boot without starting it: %s, or nomodeset\n", - st->rep->offsets->name, result, st->rep->offsets->fallback); + st->rep->offsets->name, result, fallback); + } } /* Teardown has begun: a start still in progress gives up early. */