From 3f611d5813fced61f726ad3a7249f236fd033ab7 Mon Sep 17 00:00:00 2001 From: Dj Date: Sat, 10 Oct 2026 23:51:19 -0400 Subject: [PATCH 01/12] phy: apple: T6050 fixed DisplayPort PHY register sequences The register sequence tables of the T6050 fixed DisplayPort PHY, taken alone from 440f79f96363 ("phy: apple: Add T6050 fixed DP and four-clock crossbar support"). The T6041 DisplayPort mode of the T6040 PHY uses the same sequences. The driver and crossbar parts of that commit build on the APIF series and are not included here. Signed-off-by: Dj (cherry picked from commit 440f79f963632f702f4e7f1b1d72f5fd2b9ec3cc) Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/phy/apple/atc-t6050-dp-data.h | 513 ++++++++++++++++++++++++++ 1 file changed, 513 insertions(+) create mode 100644 drivers/phy/apple/atc-t6050-dp-data.h diff --git a/drivers/phy/apple/atc-t6050-dp-data.h b/drivers/phy/apple/atc-t6050-dp-data.h new file mode 100644 index 0000000000000..e4a09dae6c3f5 --- /dev/null +++ b/drivers/phy/apple/atc-t6050-dp-data.h @@ -0,0 +1,513 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* T6050 fixed DisplayPort PHY register sequences. */ +static const struct atc_op dp_init[] = { + { ATC_MASK, 39, 0x0, 0x1, 0x1, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_POLL, 39, 0x4, 0x1, 0x1, 1000 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 39, 0x0, 0x2, 0x2, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_POLL, 39, 0x4, 0x2, 0x2, 1000 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 39, 0x0, 0x4, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_POWER, 2, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 39, 0x2c, 0x1, 0x0, 0 }, + { ATC_POLL, 39, 0x4, 0x100, 0x0, 1000 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 39, 0x0, 0x8, 0x8, 0 }, + { ATC_TUNABLE, 41, 0x0, 0x0, 0x0, 0 }, + { ATC_TUNABLE, 40, 0x0, 0x0, 0x1, 0 }, + { ATC_TUNABLE, 43, 0x0, 0x0, 0x2, 0 }, + { ATC_TUNABLE, 42, 0x0, 0x0, 0x3, 0 }, + { ATC_TUNABLE, 20, 0x0, 0x0, 0x4, 0 }, + { ATC_TUNABLE, 29, 0x0, 0x0, 0x5, 0 }, + { ATC_TUNABLE, 18, 0x0, 0x0, 0x6, 0 }, + { ATC_TUNABLE, 27, 0x0, 0x0, 0x7, 0 }, + { ATC_MASK, 4, 0x0, 0x3f0, 0x1c0, 0 }, + { ATC_MASK, 4, 0x18, 0xf000, 0x3000, 0 }, + { ATC_MASK, 9, 0x88, 0xff, 0x27, 0 }, + { ATC_MASK, 33, 0x4, 0x38, 0x8, 0 }, + { ATC_MASK, 10, 0xdc, 0x3ff0, 0x1760, 0 }, + { ATC_MASK, 21, 0xcc, 0x1f80000, 0x380000, 0 }, + { ATC_MASK, 30, 0xcc, 0x1f80000, 0x380000, 0 }, + { ATC_MASK, 20, 0x90, 0x1f80000, 0x380000, 0 }, + { ATC_MASK, 29, 0x90, 0x1f80000, 0x380000, 0 }, + { ATC_TUNABLE, 8, 0x0, 0x0, 0x8, 0 }, + { ATC_MASK, 2, 0x6c, 0x3c000fc, 0x48, 0 }, + { ATC_MASK, 2, 0x78, 0x1f, 0x14, 0 }, + { ATC_MASK, 2, 0x78, 0x3ffe0, 0x22000, 0 }, + { ATC_MASK, 2, 0x6c, 0x3, 0x2, 0 }, + { ATC_MASK, 2, 0xc, 0x2a, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0xa, 0 }, + { ATC_MASK, 39, 0x0, 0x10, 0x10, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x64, 0 }, +}; + +static const struct atc_op aux_on[] = { + { ATC_MASK, 34, 0x0, 0x3, 0x3, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 34, 0x0, 0xc, 0xc, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 34, 0x0, 0x300, 0x100, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 33, 0x0, 0x1, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, +}; + +static const struct atc_op aux_off[] = { + { ATC_MASK, 33, 0x0, 0x1, 0x1, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 34, 0x0, 0x300, 0x300, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 34, 0x0, 0x8, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 34, 0x0, 0x2, 0x0, 0 }, +}; + +static const struct atc_op dp_poweroff[] = { + { ATC_MASK, 2, 0x120, 0x100, 0x100, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0x120, 0x1, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0x120, 0x10, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0x120, 0x200, 0x200, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0x120, 0x2, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0x120, 0x20, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 5, 0x0, 0x2, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0xc, 0x10, 0x10, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0xc, 0x1, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0xc, 0x4, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0xc8, 0 }, + { ATC_POWER, 2, 0x0, 0x0, 0x0, 0 }, + { ATC_MASK, 39, 0x2c, 0x1, 0x1, 0 }, + { ATC_POLL, 39, 0x4, 0x100, 0x100, 1000 }, + { ATC_MASK, 39, 0x0, 0x10, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 39, 0x0, 0x8, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 39, 0x0, 0x4, 0x4, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 39, 0x0, 0x2, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_POLL, 39, 0x4, 0x2, 0x0, 1000 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 39, 0x0, 0x1, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_POLL, 39, 0x4, 0x1, 0x0, 1000 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 39, 0x8, 0x1, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, +}; + +static const struct atc_op dp_link_off[] = { + { ATC_MASK, 39, 0x14, 0x1007, 0x7, 0 }, + { ATC_MASK, 39, 0x18, 0x1, 0x0, 0 }, + { ATC_POLL, 39, 0x1c, 0x1, 0x0, 5000 }, + { ATC_MASK, 14, 0x0, 0x1, 0x1, 0 }, + { ATC_MASK, 22, 0x1c, 0xc00000, 0xc00000, 0 }, + { ATC_MASK, 22, 0x10, 0x600, 0x400, 0 }, + { ATC_MASK, 22, 0x4, 0x30000000, 0x20000000, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 22, 0x44, 0x100000, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 22, 0x44, 0x400000, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 22, 0x44, 0x4000000, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 22, 0x44, 0x1000000, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 22, 0x44, 0x1000, 0x0, 0 }, + { ATC_MASK, 2, 0x120, 0x100, 0x100, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0x120, 0x1, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0x120, 0x10, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 31, 0x1c, 0xc00000, 0xc00000, 0 }, + { ATC_MASK, 31, 0x10, 0x600, 0x400, 0 }, + { ATC_MASK, 31, 0x4, 0x30000000, 0x20000000, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 31, 0x44, 0x100000, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 31, 0x44, 0x400000, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 31, 0x44, 0x4000000, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 31, 0x44, 0x1000000, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 31, 0x44, 0x1000, 0x0, 0 }, + { ATC_MASK, 2, 0x120, 0x200, 0x200, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0x120, 0x2, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0x120, 0x20, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 19, 0x44, 0x6000000, 0x4000000, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 19, 0x44, 0x18000000, 0x10000000, 0 }, + { ATC_MASK, 19, 0x24, 0xc0000000, 0x80000000, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 19, 0x24, 0x30000000, 0x20000000, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 19, 0x28, 0x10, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 19, 0x40, 0xc000, 0xc000, 0 }, + { ATC_MASK, 16, 0x1d0, 0x3, 0x1, 0 }, + { ATC_MASK, 19, 0x40, 0x10000, 0x0, 0 }, + { ATC_MASK, 2, 0xc, 0x4000, 0x4000, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0xc, 0x40, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0xc, 0x400, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0x10, 0x1, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0x10, 0x10, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 28, 0x44, 0x6000000, 0x4000000, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 28, 0x44, 0x18000000, 0x10000000, 0 }, + { ATC_MASK, 28, 0x24, 0xc0000000, 0x80000000, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 28, 0x24, 0x30000000, 0x20000000, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 28, 0x28, 0x10, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 28, 0x40, 0xc000, 0xc000, 0 }, + { ATC_MASK, 25, 0x1d0, 0x3, 0x1, 0 }, + { ATC_MASK, 28, 0x40, 0x10000, 0x0, 0 }, + { ATC_MASK, 2, 0xc, 0x8000, 0x8000, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0xc, 0x80, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0xc, 0x800, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0x10, 0x2, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0x10, 0x20, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 14, 0x0, 0xc, 0x8, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0x160, 0x440000, 0x440000, 0 }, + { ATC_MASK, 3, 0x8, 0x5, 0x5, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0x160, 0x880000, 0x0, 0 }, + { ATC_MASK, 3, 0x8, 0xa, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0x160, 0x880000, 0x880000, 0 }, + { ATC_MASK, 3, 0x8, 0xa, 0xa, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0x160, 0x440000, 0x0, 0 }, + { ATC_MASK, 3, 0x8, 0x5, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0x160, 0x400000, 0x400000, 0 }, + { ATC_MASK, 3, 0x8, 0x4, 0x4, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0x160, 0x800000, 0x0, 0 }, + { ATC_MASK, 3, 0x8, 0x8, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0x160, 0x800000, 0x800000, 0 }, + { ATC_MASK, 3, 0x8, 0x8, 0x8, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0x160, 0x400000, 0x0, 0 }, + { ATC_MASK, 3, 0x8, 0x4, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0x160, 0x40000, 0x40000, 0 }, + { ATC_MASK, 3, 0x8, 0x1, 0x1, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0x160, 0x80000, 0x0, 0 }, + { ATC_MASK, 3, 0x8, 0x2, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0x160, 0x80000, 0x80000, 0 }, + { ATC_MASK, 3, 0x8, 0x2, 0x2, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0x160, 0x40000, 0x0, 0 }, + { ATC_MASK, 3, 0x8, 0x1, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 6, 0x1c, 0x7, 0x1, 0 }, + { ATC_MASK, 6, 0x1c, 0x8, 0x8, 0 }, + { ATC_POLL, 6, 0x2c, 0xff00, 0x100, 500000 }, + { ATC_MASK, 6, 0x1c, 0x8, 0x0, 0 }, + { ATC_POLL, 6, 0x2c, 0xff00, 0x0, 500000 }, + { ATC_POLL, 14, 0x34, 0x8, 0x0, 500000 }, + { ATC_DELAY, 0, 0x0, 0x0, 0xa, 0 }, + { ATC_MASK, 6, 0x34, 0xf, 0x1, 0 }, + { ATC_MASK, 6, 0x1c, 0x7, 0x0, 0 }, + { ATC_MASK, 6, 0x1c, 0x8, 0x8, 0 }, + { ATC_POLL, 6, 0x2c, 0xff00, 0x100, 500000 }, + { ATC_MASK, 6, 0x1c, 0x8, 0x0, 0 }, + { ATC_POLL, 6, 0x2c, 0xff00, 0x0, 500000 }, + { ATC_MASK, 8, 0x0, 0xaaa, 0x0, 0 }, +}; + +static const struct atc_rate_op dp_link_on[] = { + { { ATC_MASK, 39, 0x14, 0x7, 0, 0 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 39, 0x18, 0x1, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 2, 0x6c, 0xfc, 0, 0 }, 0xf, { 0x48, 0x48, 0x48, 0x48 } }, + { { ATC_MASK, 14, 0x0, 0x40fff0, 0, 0 }, 0xf, { 0x1110, 0x1110, 0x1110, 0x1110 } }, + { { ATC_MASK, 8, 0x34, 0x3, 0, 0 }, 0x1, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 7, 0x80, 0xffffffff, 0, 0 }, 0x1, { 0x1e0e021c, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 7, 0x84, 0xfffffff, 0, 0 }, 0x1, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 7, 0x88, 0x7fffff, 0, 0 }, 0x1, { 0x156600, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 8, 0x8, 0x1f0000, 0, 0 }, 0x1, { 0x70000, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 8, 0x18, 0x1, 0, 0 }, 0x1, { 0x1, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 8, 0x34, 0x3, 0, 0 }, 0x2, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 7, 0x80, 0xffffffff, 0, 0 }, 0x2, { 0x0, 0x1e0e01c2, 0x0, 0x0 } }, + { { ATC_MASK, 7, 0x84, 0xfffffff, 0, 0 }, 0x2, { 0x0, 0x7fffffe, 0x0, 0x0 } }, + { { ATC_MASK, 7, 0x88, 0x7fffff, 0, 0 }, 0x2, { 0x0, 0x155200, 0x0, 0x0 } }, + { { ATC_MASK, 8, 0x8, 0x1f0000, 0, 0 }, 0x2, { 0x0, 0x70000, 0x0, 0x0 } }, + { { ATC_MASK, 8, 0x18, 0x1, 0, 0 }, 0x2, { 0x0, 0x1, 0x0, 0x0 } }, + { { ATC_MASK, 8, 0x34, 0x3, 0, 0 }, 0x4, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 7, 0x80, 0xffffffff, 0, 0 }, 0x4, { 0x0, 0x0, 0x1e0e01c2, 0x0 } }, + { { ATC_MASK, 7, 0x84, 0xfffffff, 0, 0 }, 0x4, { 0x0, 0x0, 0x7fffffe, 0x0 } }, + { { ATC_MASK, 7, 0x88, 0x7fffff, 0, 0 }, 0x4, { 0x0, 0x0, 0x554800, 0x0 } }, + { { ATC_MASK, 8, 0x8, 0x1f0000, 0, 0 }, 0x4, { 0x0, 0x0, 0x70000, 0x0 } }, + { { ATC_MASK, 8, 0x18, 0x1, 0, 0 }, 0x4, { 0x0, 0x0, 0x1, 0x0 } }, + { { ATC_MASK, 8, 0x34, 0x3, 0, 0 }, 0x8, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 7, 0x80, 0xffffffff, 0, 0 }, 0x8, { 0x0, 0x0, 0x0, 0x1e0e02a3 } }, + { { ATC_MASK, 7, 0x84, 0xfffffff, 0, 0 }, 0x8, { 0x0, 0x0, 0x0, 0xbff7ffc } }, + { { ATC_MASK, 7, 0x88, 0x7fffff, 0, 0 }, 0x8, { 0x0, 0x0, 0x0, 0x564800 } }, + { { ATC_MASK, 8, 0x8, 0x1f0000, 0, 0 }, 0x8, { 0x0, 0x0, 0x0, 0x70000 } }, + { { ATC_MASK, 8, 0x18, 0x1, 0, 0 }, 0x8, { 0x0, 0x0, 0x0, 0x1 } }, + { { ATC_MASK, 8, 0x0, 0xaaa, 0, 0 }, 0xf, { 0xaaa, 0xaaa, 0xaaa, 0xaaa } }, + { { ATC_MASK, 6, 0x30, 0x4000, 0, 0 }, 0xf, { 0x4000, 0x4000, 0x4000, 0x4000 } }, + { { ATC_MASK, 6, 0x34, 0xf, 0, 0 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 6, 0x18, 0x1fe000, 0, 0 }, 0xf, { 0x2000, 0x2000, 0x2000, 0x2000 } }, + { { ATC_MASK, 6, 0x20, 0x1, 0, 0 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 6, 0x1c, 0x7, 0, 0 }, 0xf, { 0x7, 0x7, 0x7, 0x7 } }, + { { ATC_MASK, 6, 0x1c, 0x8, 0, 0 }, 0xf, { 0x8, 0x8, 0x8, 0x8 } }, + { { ATC_POLL, 6, 0x2c, 0xff00, 0, 500000 }, 0xf, { 0x100, 0x100, 0x100, 0x100 } }, + { { ATC_MASK, 6, 0x1c, 0x8, 0, 0 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_POLL, 6, 0x2c, 0xff00, 0, 500000 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_POLL, 14, 0x34, 0x8, 0, 500000 }, 0xf, { 0x8, 0x8, 0x8, 0x8 } }, + { { ATC_MASK, 19, 0x3c, 0x18000, 0, 0 }, 0xf, { 0x10000, 0x10000, 0x10000, 0x10000 } }, + { { ATC_MASK, 28, 0x3c, 0x18000, 0, 0 }, 0xf, { 0x10000, 0x10000, 0x10000, 0x10000 } }, + { { ATC_MASK, 14, 0x0, 0xc, 0, 0 }, 0xf, { 0xc, 0xc, 0xc, 0xc } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 2, 0x120, 0x10, 0, 0 }, 0xf, { 0x10, 0x10, 0x10, 0x10 } }, + { { ATC_MASK, 2, 0x120, 0x40, 0, 0 }, 0xf, { 0x40, 0x40, 0x40, 0x40 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 2, 0x120, 0x1, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 2, 0x120, 0x4, 0, 0 }, 0xf, { 0x4, 0x4, 0x4, 0x4 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 2, 0x120, 0x100, 0, 0 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 2, 0x120, 0x400, 0, 0 }, 0xf, { 0x400, 0x400, 0x400, 0x400 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 22, 0x44, 0xfffffc0, 0, 0 }, 0xf, { 0xaafb800, 0xaafb800, 0xaafb800, 0xaafb800 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 22, 0x44, 0x1000000, 0, 0 }, 0xf, { 0x1000000, 0x1000000, 0x1000000, 0x1000000 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 22, 0x44, 0x4000000, 0, 0 }, 0xf, { 0x4000000, 0x4000000, 0x4000000, 0x4000000 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 22, 0x44, 0x7c0, 0, 0 }, 0xf, { 0x300, 0x300, 0x300, 0x300 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 22, 0x44, 0x6c000, 0, 0 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 21, 0x100, 0x100000, 0, 0 }, 0xf, { 0x100000, 0x100000, 0x100000, 0x100000 } }, + { { ATC_MASK, 21, 0x58, 0xc00f, 0, 0 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 21, 0x104, 0x20000000, 0, 0 }, 0xf, { 0x20000000, 0x20000000, 0x20000000, 0x20000000 } }, + { { ATC_MASK, 21, 0x100, 0x7ffff, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 22, 0x44, 0x100000, 0, 0 }, 0x7, { 0x100000, 0x100000, 0x100000, 0x0 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0x7, { 0x1, 0x1, 0x1, 0x0 } }, + { { ATC_MASK, 22, 0x44, 0x400000, 0, 0 }, 0x7, { 0x400000, 0x400000, 0x400000, 0x0 } }, + { { ATC_MASK, 22, 0x4, 0x3000000, 0, 0 }, 0xf, { 0x2000000, 0x2000000, 0x2000000, 0x2000000 } }, + { { ATC_MASK, 22, 0x4, 0x30000000, 0, 0 }, 0xf, { 0x30000000, 0x30000000, 0x30000000, 0x30000000 } }, + { { ATC_MASK, 22, 0xc, 0x7000000, 0, 0 }, 0x1, { 0x5000000, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 22, 0xc, 0x7000000, 0, 0 }, 0xe, { 0x0, 0x4000000, 0x4000000, 0x4000000 } }, + { { ATC_MASK, 22, 0x8, 0x3, 0, 0 }, 0xf, { 0x2, 0x2, 0x2, 0x2 } }, + { { ATC_MASK, 22, 0x10, 0x600, 0, 0 }, 0xf, { 0x600, 0x600, 0x600, 0x600 } }, + { { ATC_MASK, 22, 0x1c, 0xc00000, 0, 0 }, 0xf, { 0x800000, 0x800000, 0x800000, 0x800000 } }, + { { ATC_MASK, 2, 0x120, 0x20, 0, 0 }, 0xf, { 0x20, 0x20, 0x20, 0x20 } }, + { { ATC_MASK, 2, 0x120, 0x80, 0, 0 }, 0xf, { 0x80, 0x80, 0x80, 0x80 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 2, 0x120, 0x2, 0, 0 }, 0xf, { 0x2, 0x2, 0x2, 0x2 } }, + { { ATC_MASK, 2, 0x120, 0x8, 0, 0 }, 0xf, { 0x8, 0x8, 0x8, 0x8 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 2, 0x120, 0x200, 0, 0 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 2, 0x120, 0x800, 0, 0 }, 0xf, { 0x800, 0x800, 0x800, 0x800 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 31, 0x44, 0xfffffc0, 0, 0 }, 0xf, { 0xaafb800, 0xaafb800, 0xaafb800, 0xaafb800 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 31, 0x44, 0x1000000, 0, 0 }, 0xf, { 0x1000000, 0x1000000, 0x1000000, 0x1000000 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 31, 0x44, 0x4000000, 0, 0 }, 0xf, { 0x4000000, 0x4000000, 0x4000000, 0x4000000 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 31, 0x44, 0x7c0, 0, 0 }, 0xf, { 0x300, 0x300, 0x300, 0x300 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 31, 0x44, 0x6c000, 0, 0 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 30, 0x100, 0x100000, 0, 0 }, 0xf, { 0x100000, 0x100000, 0x100000, 0x100000 } }, + { { ATC_MASK, 30, 0x58, 0xc00f, 0, 0 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 30, 0x104, 0x20000000, 0, 0 }, 0xf, { 0x20000000, 0x20000000, 0x20000000, 0x20000000 } }, + { { ATC_MASK, 30, 0x100, 0x7ffff, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 31, 0x44, 0x100000, 0, 0 }, 0x7, { 0x100000, 0x100000, 0x100000, 0x0 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0x7, { 0x1, 0x1, 0x1, 0x0 } }, + { { ATC_MASK, 31, 0x44, 0x400000, 0, 0 }, 0x7, { 0x400000, 0x400000, 0x400000, 0x0 } }, + { { ATC_MASK, 31, 0x4, 0x3000000, 0, 0 }, 0xf, { 0x2000000, 0x2000000, 0x2000000, 0x2000000 } }, + { { ATC_MASK, 31, 0x4, 0x30000000, 0, 0 }, 0xf, { 0x30000000, 0x30000000, 0x30000000, 0x30000000 } }, + { { ATC_MASK, 31, 0xc, 0x7000000, 0, 0 }, 0x1, { 0x5000000, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 31, 0xc, 0x7000000, 0, 0 }, 0xe, { 0x0, 0x4000000, 0x4000000, 0x4000000 } }, + { { ATC_MASK, 31, 0x8, 0x3, 0, 0 }, 0xf, { 0x2, 0x2, 0x2, 0x2 } }, + { { ATC_MASK, 31, 0x10, 0x600, 0, 0 }, 0xf, { 0x600, 0x600, 0x600, 0x600 } }, + { { ATC_MASK, 31, 0x1c, 0xc00000, 0, 0 }, 0xf, { 0x800000, 0x800000, 0x800000, 0x800000 } }, + { { ATC_MASK, 19, 0xf4, 0x3000, 0, 0 }, 0xf, { 0x3000, 0x3000, 0x3000, 0x3000 } }, + { { ATC_MASK, 16, 0x188, 0x1, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 16, 0x1d4, 0x700, 0, 0 }, 0xf, { 0x300, 0x300, 0x300, 0x300 } }, + { { ATC_MASK, 19, 0x24, 0x3f0000, 0, 0 }, 0xf, { 0x2a0000, 0x2a0000, 0x2a0000, 0x2a0000 } }, + { { ATC_MASK, 19, 0x3c, 0x18000, 0, 0 }, 0xf, { 0x10000, 0x10000, 0x10000, 0x10000 } }, + { { ATC_MASK, 19, 0x78, 0xc0000, 0, 0 }, 0xf, { 0x80000, 0x80000, 0x80000, 0x80000 } }, + { { ATC_MASK, 19, 0x7c, 0xc0, 0, 0 }, 0xf, { 0x80, 0x80, 0x80, 0x80 } }, + { { ATC_MASK, 19, 0x84, 0xc000, 0, 0 }, 0xf, { 0x8000, 0x8000, 0x8000, 0x8000 } }, + { { ATC_MASK, 19, 0xd4, 0x600000, 0, 0 }, 0xf, { 0x400000, 0x400000, 0x400000, 0x400000 } }, + { { ATC_MASK, 19, 0xd8, 0x300, 0, 0 }, 0xf, { 0x200, 0x200, 0x200, 0x200 } }, + { { ATC_MASK, 19, 0x40, 0xc00, 0, 0 }, 0xf, { 0x800, 0x800, 0x800, 0x800 } }, + { { ATC_MASK, 19, 0x48, 0x600, 0, 0 }, 0xf, { 0x400, 0x400, 0x400, 0x400 } }, + { { ATC_MASK, 19, 0x28, 0x1ff030, 0, 0 }, 0xf, { 0x11c020, 0x11c020, 0x11c020, 0x11c020 } }, + { { ATC_MASK, 19, 0x2c, 0xfc, 0, 0 }, 0xf, { 0xb0, 0xb0, 0xb0, 0xb0 } }, + { { ATC_MASK, 19, 0x28, 0xe00, 0, 0 }, 0xf, { 0x800, 0x800, 0x800, 0x800 } }, + { { ATC_MASK, 19, 0x28, 0x1c0, 0, 0 }, 0xf, { 0x180, 0x180, 0x180, 0x180 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 2, 0x10, 0x10, 0, 0 }, 0xf, { 0x10, 0x10, 0x10, 0x10 } }, + { { ATC_MASK, 2, 0x10, 0x40, 0, 0 }, 0xf, { 0x40, 0x40, 0x40, 0x40 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 2, 0x10, 0x1, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 2, 0x10, 0x4, 0, 0 }, 0xf, { 0x4, 0x4, 0x4, 0x4 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 2, 0xc, 0x400, 0, 0 }, 0xf, { 0x400, 0x400, 0x400, 0x400 } }, + { { ATC_MASK, 2, 0xc, 0x1000, 0, 0 }, 0xf, { 0x1000, 0x1000, 0x1000, 0x1000 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 2, 0xc, 0x40, 0, 0 }, 0xf, { 0x40, 0x40, 0x40, 0x40 } }, + { { ATC_MASK, 2, 0xc, 0x100, 0, 0 }, 0xf, { 0x100, 0x100, 0x100, 0x100 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 2, 0xc, 0x4000, 0, 0 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 2, 0xc, 0x10000, 0, 0 }, 0xf, { 0x10000, 0x10000, 0x10000, 0x10000 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 16, 0x1d0, 0x3, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 16, 0x1d0, 0x2, 0, 0 }, 0xf, { 0x2, 0x2, 0x2, 0x2 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 19, 0x40, 0x30000, 0, 0 }, 0xf, { 0x30000, 0x30000, 0x30000, 0x30000 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 19, 0x40, 0xc000, 0, 0 }, 0xf, { 0xc000, 0xc000, 0xc000, 0xc000 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 19, 0x28, 0x3010, 0, 0 }, 0xf, { 0x1010, 0x1010, 0x1010, 0x1010 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 19, 0x28, 0x600, 0, 0 }, 0xf, { 0x200, 0x200, 0x200, 0x200 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 19, 0x28, 0x3000, 0, 0 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 19, 0x24, 0x30000000, 0, 0 }, 0xf, { 0x30000000, 0x30000000, 0x30000000, 0x30000000 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 19, 0x24, 0xc0000000, 0, 0 }, 0xf, { 0xc0000000, 0xc0000000, 0xc0000000, 0xc0000000 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 19, 0x28, 0x600, 0, 0 }, 0xf, { 0x600, 0x600, 0x600, 0x600 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 19, 0x28, 0x600, 0, 0 }, 0xf, { 0x400, 0x400, 0x400, 0x400 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 19, 0x28, 0x600, 0, 0 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x2, 0x2, 0x2, 0x2 } }, + { { ATC_MASK, 19, 0x44, 0x18000000, 0, 0 }, 0xf, { 0x18000000, 0x18000000, 0x18000000, 0x18000000 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x2, 0x2, 0x2, 0x2 } }, + { { ATC_MASK, 19, 0x40, 0x38, 0, 0 }, 0x1, { 0x28, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 19, 0x40, 0x38, 0, 0 }, 0xe, { 0x0, 0x20, 0x20, 0x20 } }, + { { ATC_MASK, 19, 0x44, 0x60000000, 0, 0 }, 0xf, { 0x40000000, 0x40000000, 0x40000000, 0x40000000 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 19, 0x44, 0x6000000, 0, 0 }, 0xf, { 0x6000000, 0x6000000, 0x6000000, 0x6000000 } }, + { { ATC_MASK, 19, 0x40, 0xc000, 0, 0 }, 0xf, { 0x8000, 0x8000, 0x8000, 0x8000 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 20, 0x68, 0xc00f, 0, 0 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 20, 0xac, 0x20000000, 0, 0 }, 0xf, { 0x20000000, 0x20000000, 0x20000000, 0x20000000 } }, + { { ATC_MASK, 20, 0xa8, 0x7ffff, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 20, 0x18, 0x38, 0, 0 }, 0xf, { 0x20, 0x20, 0x20, 0x20 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 28, 0xf4, 0x3000, 0, 0 }, 0xf, { 0x3000, 0x3000, 0x3000, 0x3000 } }, + { { ATC_MASK, 25, 0x188, 0x1, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 25, 0x1d4, 0x700, 0, 0 }, 0xf, { 0x300, 0x300, 0x300, 0x300 } }, + { { ATC_MASK, 28, 0x24, 0x3f0000, 0, 0 }, 0xf, { 0x2a0000, 0x2a0000, 0x2a0000, 0x2a0000 } }, + { { ATC_MASK, 28, 0x3c, 0x18000, 0, 0 }, 0xf, { 0x10000, 0x10000, 0x10000, 0x10000 } }, + { { ATC_MASK, 28, 0x78, 0xc0000, 0, 0 }, 0xf, { 0x80000, 0x80000, 0x80000, 0x80000 } }, + { { ATC_MASK, 28, 0x7c, 0xc0, 0, 0 }, 0xf, { 0x80, 0x80, 0x80, 0x80 } }, + { { ATC_MASK, 28, 0x84, 0xc000, 0, 0 }, 0xf, { 0x8000, 0x8000, 0x8000, 0x8000 } }, + { { ATC_MASK, 28, 0xd4, 0x600000, 0, 0 }, 0xf, { 0x400000, 0x400000, 0x400000, 0x400000 } }, + { { ATC_MASK, 28, 0xd8, 0x300, 0, 0 }, 0xf, { 0x200, 0x200, 0x200, 0x200 } }, + { { ATC_MASK, 28, 0x40, 0xc00, 0, 0 }, 0xf, { 0x800, 0x800, 0x800, 0x800 } }, + { { ATC_MASK, 28, 0x48, 0x600, 0, 0 }, 0xf, { 0x400, 0x400, 0x400, 0x400 } }, + { { ATC_MASK, 28, 0x28, 0x1ff030, 0, 0 }, 0xf, { 0x11c020, 0x11c020, 0x11c020, 0x11c020 } }, + { { ATC_MASK, 28, 0x2c, 0xfc, 0, 0 }, 0xf, { 0xb0, 0xb0, 0xb0, 0xb0 } }, + { { ATC_MASK, 28, 0x28, 0xe00, 0, 0 }, 0xf, { 0x800, 0x800, 0x800, 0x800 } }, + { { ATC_MASK, 28, 0x28, 0x1c0, 0, 0 }, 0xf, { 0x180, 0x180, 0x180, 0x180 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 2, 0x10, 0x20, 0, 0 }, 0xf, { 0x20, 0x20, 0x20, 0x20 } }, + { { ATC_MASK, 2, 0x10, 0x80, 0, 0 }, 0xf, { 0x80, 0x80, 0x80, 0x80 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 2, 0x10, 0x2, 0, 0 }, 0xf, { 0x2, 0x2, 0x2, 0x2 } }, + { { ATC_MASK, 2, 0x10, 0x8, 0, 0 }, 0xf, { 0x8, 0x8, 0x8, 0x8 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 2, 0xc, 0x800, 0, 0 }, 0xf, { 0x800, 0x800, 0x800, 0x800 } }, + { { ATC_MASK, 2, 0xc, 0x2000, 0, 0 }, 0xf, { 0x2000, 0x2000, 0x2000, 0x2000 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 2, 0xc, 0x80, 0, 0 }, 0xf, { 0x80, 0x80, 0x80, 0x80 } }, + { { ATC_MASK, 2, 0xc, 0x200, 0, 0 }, 0xf, { 0x200, 0x200, 0x200, 0x200 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 2, 0xc, 0x8000, 0, 0 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 2, 0xc, 0x20000, 0, 0 }, 0xf, { 0x20000, 0x20000, 0x20000, 0x20000 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 25, 0x1d0, 0x3, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 25, 0x1d0, 0x2, 0, 0 }, 0xf, { 0x2, 0x2, 0x2, 0x2 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 28, 0x40, 0x30000, 0, 0 }, 0xf, { 0x30000, 0x30000, 0x30000, 0x30000 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 28, 0x40, 0xc000, 0, 0 }, 0xf, { 0xc000, 0xc000, 0xc000, 0xc000 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 28, 0x28, 0x3010, 0, 0 }, 0xf, { 0x1010, 0x1010, 0x1010, 0x1010 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 28, 0x28, 0x600, 0, 0 }, 0xf, { 0x200, 0x200, 0x200, 0x200 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 28, 0x28, 0x3000, 0, 0 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 28, 0x24, 0x30000000, 0, 0 }, 0xf, { 0x30000000, 0x30000000, 0x30000000, 0x30000000 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 28, 0x24, 0xc0000000, 0, 0 }, 0xf, { 0xc0000000, 0xc0000000, 0xc0000000, 0xc0000000 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 28, 0x28, 0x600, 0, 0 }, 0xf, { 0x600, 0x600, 0x600, 0x600 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 28, 0x28, 0x600, 0, 0 }, 0xf, { 0x400, 0x400, 0x400, 0x400 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 28, 0x28, 0x600, 0, 0 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x2, 0x2, 0x2, 0x2 } }, + { { ATC_MASK, 28, 0x44, 0x18000000, 0, 0 }, 0xf, { 0x18000000, 0x18000000, 0x18000000, 0x18000000 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x2, 0x2, 0x2, 0x2 } }, + { { ATC_MASK, 28, 0x40, 0x38, 0, 0 }, 0x1, { 0x28, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 28, 0x40, 0x38, 0, 0 }, 0xe, { 0x0, 0x20, 0x20, 0x20 } }, + { { ATC_MASK, 28, 0x44, 0x60000000, 0, 0 }, 0xf, { 0x40000000, 0x40000000, 0x40000000, 0x40000000 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 28, 0x44, 0x6000000, 0, 0 }, 0xf, { 0x6000000, 0x6000000, 0x6000000, 0x6000000 } }, + { { ATC_MASK, 28, 0x40, 0xc000, 0, 0 }, 0xf, { 0x8000, 0x8000, 0x8000, 0x8000 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 29, 0x68, 0xc00f, 0, 0 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 29, 0xac, 0x20000000, 0, 0 }, 0xf, { 0x20000000, 0x20000000, 0x20000000, 0x20000000 } }, + { { ATC_MASK, 29, 0xa8, 0x7ffff, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 29, 0x18, 0x38, 0, 0 }, 0xf, { 0x20, 0x20, 0x20, 0x20 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 14, 0x0, 0x1, 0, 0 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_POLL, 39, 0x1c, 0x1, 0, 5000 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, +}; + +static const u32 dp_training_default[16] = { + 0x3f024, 0x237013, 0x37200b, 0x46e004, 0x3f01c, 0x23700b, 0x372004, 0x3f024, 0x3f013, 0x237004, 0x3f024, 0x3f024, 0x3f004, 0x3f024, 0x3f024, 0x3f024 +}; From 4582748f447b52778865ffd8defa51fe83cd3dd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Bal=C3=A1zsy?= Date: Sun, 25 Oct 2026 02:10:28 +0200 Subject: [PATCH 02/12] dt-bindings: phy: apple,t6040-atcphy: DisplayPort mode switch and fixed DP PHY MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The T6040 Type-C PHY also carries DisplayPort: the PHY of a USB-C port as the alt mode switch of its connector (mode-switch), and the PHY of an HDMI converter as a fixed DisplayPort link. The fixed instance has no USB controller behind it, so no PIPE interface ("pipehandler" region), no reset line and no connector port. Describe the DP phy type, the mode switch and the ten-region variant, and require #reset-cells only where the PIPE interface exists. Signed-off-by: František Balázsy --- .../bindings/phy/apple,t6040-atcphy.yaml | 101 ++++++++++++++---- 1 file changed, 83 insertions(+), 18 deletions(-) diff --git a/Documentation/devicetree/bindings/phy/apple,t6040-atcphy.yaml b/Documentation/devicetree/bindings/phy/apple,t6040-atcphy.yaml index ce11949c34455..04c8907584946 100644 --- a/Documentation/devicetree/bindings/phy/apple,t6040-atcphy.yaml +++ b/Documentation/devicetree/bindings/phy/apple,t6040-atcphy.yaml @@ -11,10 +11,13 @@ maintainers: description: > The Type-C PHY of the Apple T6040 (M4 Pro) SoC, as used for USB2 and USB3 - host operation. USB3 is brought up by a PHY coprocessor running RTKit - firmware, reached through an ASC mailbox. The PHY also provides the reset - line of the attached DWC3 controller and acts as the Type-C orientation - switch. + host operation and for DisplayPort. USB3 is brought up by a PHY coprocessor + running RTKit firmware, reached through an ASC mailbox. The PHY also + provides the reset line of the attached DWC3 controller and acts as the + Type-C orientation switch and, where the port carries DisplayPort alt mode, + as its mode switch. The same PHY drives the fixed DisplayPort link of an + HDMI converter; that instance has no USB controller behind it and therefore + no PIPE interface. $defs: apple,tunable: @@ -35,6 +38,7 @@ properties: const: apple,t6040-atcphy reg: + minItems: 10 items: - description: USB2 PHY controls - description: USB2 event counter controls @@ -49,21 +53,37 @@ properties: - description: ATC common configuration controls reg-names: - items: - - const: bank0 - - const: bank1 - - const: pipehandler - - const: core - - const: cfg - - const: cpu - - const: cpu-data - - const: axi2af - - const: axi2af-lioa - - const: fabric - - const: common-cfg + oneOf: + - items: + - const: bank0 + - const: bank1 + - const: pipehandler + - const: core + - const: cfg + - const: cpu + - const: cpu-data + - const: axi2af + - const: axi2af-lioa + - const: fabric + - const: common-cfg + - description: A fixed DisplayPort PHY without a USB controller + items: + - const: bank0 + - const: bank1 + - const: core + - const: cfg + - const: cpu + - const: cpu-data + - const: axi2af + - const: axi2af-lioa + - const: fabric + - const: common-cfg "#phy-cells": const: 1 + description: > + The PHY type (dt-bindings/phy/phy.h): PHY_TYPE_USB2, PHY_TYPE_USB3 or + PHY_TYPE_DP. "#reset-cells": const: 0 @@ -81,9 +101,13 @@ properties: orientation-switch: true + mode-switch: true + port: $ref: /schemas/graph.yaml#/properties/port - description: Link to the SuperSpeed lanes of the Type-C connector. + description: > + Link to the SuperSpeed lanes of the Type-C connector. Absent on a fixed + DisplayPort PHY. apple,tunable-axi2af: $ref: "#/$defs/apple,tunable" @@ -134,9 +158,24 @@ required: - reg - reg-names - "#phy-cells" - - "#reset-cells" - power-domains +allOf: + - if: + properties: + reg-names: + contains: + const: pipehandler + then: + required: + - "#reset-cells" + else: + properties: + "#reset-cells": false + orientation-switch: false + mode-switch: false + port: false + additionalProperties: false examples: @@ -170,4 +209,30 @@ examples: orientation-switch; }; }; + - | + soc { + #address-cells = <2>; + #size-cells = <2>; + + phy@39aa90000 { + compatible = "apple,t6040-atcphy"; + reg = <0x3 0x9aa90000 0x0 0x4000>, + <0x3 0x9a800000 0x0 0x4000>, + <0x3 0x9b000000 0x0 0x30000>, + <0x3 0x9b200000 0x0 0x4000>, + <0x3 0x9b140000 0x0 0x4000>, + <0x3 0x9b0c0000 0x0 0x10000>, + <0x3 0x98000000 0x0 0x8000>, + <0x3 0x98020000 0x0 0x4000>, + <0x3 0x9b244000 0x0 0x4000>, + <0x3 0x9b250000 0x0 0x4000>; + reg-names = "bank0", "bank1", "core", "cfg", "cpu", "cpu-data", + "axi2af", "axi2af-lioa", "fabric", "common-cfg"; + #phy-cells = <1>; + power-domains = <&ps_atc3_common_dp>; + mboxes = <&atcphy3_mbox>; + mbox-names = "phy-cpu"; + nonposted-mmio; + }; + }; ... From 9ac82abf37005ef035664b670a1730ddb5aca82f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Bal=C3=A1zsy?= Date: Sun, 25 Oct 2026 02:10:28 +0200 Subject: [PATCH 03/12] phy: apple: t6040: DisplayPort mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add PHY_TYPE_DP to the T6040 Type-C PHY: PHY mode 3 of the paired AppleT6040TypeCPhy driver, DisplayPort on all four lanes, for a Type-C port whose controller selected DP alt mode with pin assignment C or E, and for the fixed DP PHY of an HDMI converter, a node without the PIPE interface and the DWC3 reset line, which now probes too. Bring-up follows the paired driver's mode 3: wake the CFG block, apply the generic ADT tunables and the DPTX tunables, set the lane crossbar to DP, hold the common block and both TX lanes on by override (the PHY coprocessor does not run in this mode), release the reset, then set the lane mode and the AUX block. On a Type-C port the USB3 side is shut down first. The per-rate link sequences (RBR to HBR3) and the drive-setting presets are the T6050 fixed-DP tables, whose PHY the T6050 ADT marks as T6040-compatible ("atc-phy,t6050", "atc-phy,t6040"); the T6050 clock shim steps are replaced by the T6040 DP clock switch of the CFG window, run after the lane program as the paired driver does. Pin assignment D (two DP lanes next to USB3) is not supported. The Type-C mode switch latches whether the port is in DP alt mode; apple_t6040_phy_dp_alt() (include/linux/soc/apple/dp-tunnel.h) exposes it for a DCP that lists several Type-C DP PHYs and routes to the one with a sink plugged. Signed-off-by: František Balázsy --- drivers/phy/apple/Kconfig | 21 +- drivers/phy/apple/t6040-usb2.c | 708 ++++++++++++++++++++++++++-- include/linux/soc/apple/dp-tunnel.h | 8 + 3 files changed, 698 insertions(+), 39 deletions(-) diff --git a/drivers/phy/apple/Kconfig b/drivers/phy/apple/Kconfig index 10c94455f0974..5348987cf1dc8 100644 --- a/drivers/phy/apple/Kconfig +++ b/drivers/phy/apple/Kconfig @@ -32,23 +32,26 @@ config PHY_APPLE_ATC_KUNIT_TEST If unsure, say N. config PHY_APPLE_T6040_USB2 - tristate "Apple T6040 Type-C PHY (USB2 and USB3 host)" + tristate "Apple T6040 Type-C PHY (USB2/USB3 host, DisplayPort)" depends on ARM64 && ARCH_APPLE depends on TYPEC depends on APPLE_RTKIT select GENERIC_PHY select APPLE_TUNABLE help - Build the PHY provider for the T6040 generation's Type-C ports. It - implements the USB2 host sequence and the USB3 host attach decoded - from the paired AppleT6040TypeCPhy driver, including the start of - the PHY coprocessor, and provides no device mode, DisplayPort, - USB4/Thunderbolt or retimer support. + Build the PHY provider for the T6040 generation's Type-C ports and + fixed DisplayPort PHYs. It implements the USB2 host sequence and the + USB3 host attach decoded from the paired AppleT6040TypeCPhy driver, + including the start of the PHY coprocessor, and the DisplayPort mode + (all four lanes, RBR to HBR3) a Type-C controller selects through the + PHY's mode switch or that an HDMI converter's PHY runs permanently. + It provides no device mode, DisplayPort pin assignment D, USB4, + Thunderbolt or retimer support. This driver performs no ATC-register writes at probe time. Enabling a - matching device-tree node causes MMIO writes when a USB2 consumer - powers on the PHY and therefore requires platform-specific safety - review. + matching device-tree node causes MMIO writes when a USB2, USB3 or + DisplayPort consumer powers on the PHY and therefore requires + platform-specific safety review. config PHY_APPLE_DPTX tristate "Apple DPTX PHY" diff --git a/drivers/phy/apple/t6040-usb2.c b/drivers/phy/apple/t6040-usb2.c index 1a1fe530f2111..9ff40f1c11019 100644 --- a/drivers/phy/apple/t6040-usb2.c +++ b/drivers/phy/apple/t6040-usb2.c @@ -43,9 +43,12 @@ #include #include #include +#include #include #include #include +#include +#include #define T6040_USB2_USBCTL 0x00 #define T6040_USB2_USBCTL_MODE GENMASK(2, 0) @@ -102,6 +105,16 @@ #define T6040_PIPEHANDLER_NONSELECTED_BUSY BIT(12) #define T6040_PIPEHANDLER_DUMMY_PHY_READY BIT(15) +/* + * DisplayPort (PHY mode 3). Offsets are into the "core" and "cfg" windows + * the USB3 path uses; names follow the paired AppleT6040TypeCPhy driver. + */ +#define T6040_CORE_AUX_CTRL 0x28000 /* ACIOPHY_AUX_TOP_BLK_AUX_CTRL */ +#define T6040_CORE_AUX_PWR 0x28400 /* ACIOPHY_AUX_SHM_BLK_AUX_PWR_REG */ + +#define T6040_CFG_DP_CLKSWITCH_CTRL 0x14 /* ATCPHY_DP_CLKSWITCH_CONTROL_REGS */ +#define T6040_CFG_DP_CLKSWITCH_STAT 0x18 /* ATCPHY_DP_CLKSWITCH_STATUS_REGS */ + /* PHY core ("core"), atc-phy reg[2] and the blocks behind it */ #define T6040_CORE_CMN_OVRD 0x0c /* CFG0_TOP */ #define T6040_CORE_CMN_OVRD_SLEEP_B_BIG BIT(0) @@ -214,6 +227,7 @@ struct t6040_usb2_phy { struct phy *usb3; bool usb3_ok; void __iomem *regs[T6040_USB3_NR_REGIONS]; + struct resource *res[T6040_USB3_NR_REGIONS]; struct resource *cpu_data; struct apple_tunable *tunables[T6040_USB3_NR_TUNABLES]; struct apple_tunable *tunable_pipehandler; @@ -224,6 +238,15 @@ struct t6040_usb2_phy { enum typec_orientation orientation; bool usb3_up; /* tuned by the coprocessor this power cycle */ bool pipe_switched; /* the controller's PIPE runs on this PHY */ + + /* DisplayPort (PHY mode 3); dp_ok is false when the DT lacks a DP route */ + struct phy *dp; + struct typec_mux_dev *mux; + bool dp_ok; + bool dp_up; /* DP mode is programmed on the PHY */ + bool dp_alt; /* the Type-C port is in DP alt mode, pin assignment C/E */ + bool dp_flip; /* plug orientation reversed for the DP lanes */ + int dp_rate; /* link rate index of the running link sequence, -1 = off */ }; static inline void t6040_usb2_mask32(void __iomem *reg, u32 mask, u32 set) @@ -906,16 +929,13 @@ static int t6040_usb3_power_on(struct phy *phy) return 0; } -static int t6040_usb3_power_off(struct phy *phy) +/* aciophy_phy_shutdown with the lock held: lanes, coprocessor, then the CFG block. */ +static void t6040_usb3_down_locked(struct t6040_usb2_phy *tphy) { - struct t6040_usb2_phy *tphy = phy_get_drvdata(phy); - - guard(mutex)(&tphy->lock); if (tphy->pipe_switched) { t6040_usb3_pipe_park(tphy); tphy->pipe_switched = false; } - /* aciophy_phy_shutdown: lanes, coprocessor, then the CFG block */ if (tphy->usb3_up) { u32 uc = readl(t6040_core(tphy) + T6040_CORE_UC_CTRL); @@ -928,6 +948,14 @@ static int t6040_usb3_power_off(struct phy *phy) readl(t6040_cfg(tphy) + T6040_CFG_STATUS)); tphy->usb3_up = false; } +} + +static int t6040_usb3_power_off(struct phy *phy) +{ + struct t6040_usb2_phy *tphy = phy_get_drvdata(phy); + + guard(mutex)(&tphy->lock); + t6040_usb3_down_locked(tphy); return 0; } @@ -974,6 +1002,591 @@ static void t6040_usb3_switch_unregister(void *sw) typec_switch_unregister(sw); } +/* ---- DisplayPort (PHY mode 3: DP on all four lanes) ---- + * + * The fixed DP PHY of an HDMI converter (no USB controller, no pipehandler) + * and a Type-C PHY whose port the Type-C controller has put into DP alt mode + * with pin assignment C or E are brought up the same way, as the paired + * AppleT6040TypeCPhy driver does for its mode 3: wake the CFG block, apply the + * generic ADT tunables and the DPTX tunables, set the lane crossbar to DP on + * all lanes, hold the common block and both TX lanes on (the PHY coprocessor + * does not run in this mode) and release the PHY reset. The DCP then sets the + * link rate through phy_configure(): the per-rate link sequence is the T6050 + * fixed-DP table (atc-t6050-dp-data.h; the T6050 ADT names its PHY + * "atc-phy,t6050", "atc-phy,t6040"), followed by the T6040 DP clock switch in + * the "cfg" window. Pin assignment D (two DP lanes next to USB3) is not + * supported. + */ + +/* The fixed DPTX tunable writes (apply_dptx_tunables, shared with USB3). */ +static void t6040_dp_fixed_tunables(void __iomem *core) +{ + static const struct { u32 off, clear, set; } dptx[] = { + { 0x00800, 0x000003f0, 0x000001c0 }, /* AUS40CMN_DIG RCAL0 */ + { 0x00818, 0x0000f000, 0x00003000 }, /* AUS40CMN_DIG RCAL5 */ + { 0x03888, 0x000000ff, 0x00000027 }, /* CIO4PLL FREQDESCRIPTOR_0_C */ + { 0x28004, 0x00000038, 0x00000008 }, /* AUX_LDO_CTRL */ + { 0x03adc, 0x00003ff0, 0x00001760 }, /* CIO4PLL CFG_FRACN_FUNC */ + { 0x0e0cc, 0x01f80000, 0x00380000 }, /* LANE0 TX_EQ_CTRL_REG11 */ + { 0x1e0cc, 0x01f80000, 0x00380000 }, /* LANE1 TX_EQ_CTRL_REG11 */ + { 0x0d090, 0x01f80000, 0x00380000 }, /* LANE0 RX_CFG TX_EQ_CTRL_REG11 */ + { 0x1d090, 0x01f80000, 0x00380000 }, /* LANE1 RX_CFG TX_EQ_CTRL_REG11 */ + }; + size_t i; + + for (i = 0; i < ARRAY_SIZE(dptx); i++) + t6040_usb2_mask32(core + dptx[i].off, dptx[i].clear, dptx[i].set); +} + +/* aciophy_xbar_config for PHY mode 3 (DP on all four lanes). */ +static void t6040_dp_xbar_config(struct t6040_usb2_phy *tphy, bool flip) +{ + void __iomem *core = t6040_core(tphy); + + /* core+0x6c, orientation-independent for DP */ + t6040_usb2_mask32(core + T6040_CORE_XBAR, ~0xfc3fff03u, 0x48); + /* core+0x78: lanes[4:0] = 0x14, then [17:5] by orientation */ + t6040_usb2_mask32(core + T6040_CORE_XBAR_LANES, 0x1f, 0x14); + t6040_usb2_mask32(core + T6040_CORE_XBAR_LANES, 0x3ffe0, flip ? 0x100 : 0x22000); + /* core+0x6c[1:0] = 2 (common to every mode) */ + t6040_usb2_mask32(core + T6040_CORE_XBAR, 0x3, 0x2); +} + +/* + * aciophy_dptx_adjust_lane_mode: the core+0x6c lane fields [4:2] and [7:5] + * are 7 for every DP 1.x link rate (UHBR rates would use 2). + */ +static void t6040_dp_adjust_lane_mode(struct t6040_usb2_phy *tphy) +{ + t6040_usb2_set32(t6040_core(tphy) + T6040_CORE_XBAR, 0x1c | 0xe0); +} + +/* aciophy_aux_init: the DP AUX block. */ +static void t6040_dp_aux_init(struct t6040_usb2_phy *tphy) +{ + void __iomem *core = t6040_core(tphy); + + t6040_usb2_set32(core + T6040_CORE_AUX_PWR, 0x3); + t6040_usb2_set32(core + T6040_CORE_AUX_PWR, 0xc); + t6040_usb2_mask32(core + T6040_CORE_AUX_PWR, 0x300, 0x100); + t6040_usb2_clear32(core + T6040_CORE_AUX_CTRL, 0x1); +} + +/* + * aciophy_configure_dp_clks: the two DP clock switches of the "cfg" window + * (control at 0x14, status at 0x18). Each switch enables its pixel clock, + * requests the two PMGR DP clocks and releases the requests one by one once + * they are acknowledged. On the J616s the pixel-clock enable is acknowledged + * with the enable bit alone and the PMGR acknowledgements arrive with the + * 0x400 (SWITCH1) / 0x800 (SWITCH2) bit set next to the requests; the + * requests are retried without that bit should the acknowledgment not come. + */ +static int t6040_dp_configure_clks(struct t6040_usb2_phy *tphy) +{ + void __iomem *cfg = t6040_cfg(tphy); + u32 s; + int ret; + +#define DP_CLK_POLL(bit, set) \ + readl_poll_timeout(cfg + T6040_CFG_DP_CLKSWITCH_STAT, s, \ + !!(s & BIT(bit)) == (set), 10, 200000) + + /* SWITCH1: pixel clock, then the PMGR DP0/DP1 requests */ + t6040_usb2_mask32(cfg + T6040_CFG_DP_CLKSWITCH_CTRL, 0x473, 0x10); + ret = DP_CLK_POLL(0, true); + if (ret) + goto timeout; + t6040_usb2_set32(cfg + T6040_CFG_DP_CLKSWITCH_CTRL, 0x60 | 0x400); + ret = DP_CLK_POLL(2, true) ?: DP_CLK_POLL(1, true); + if (ret) { + dev_dbg(tphy->dev, "DP clock switch 1: no PMGR ack with 0x400 (status %#x)\n", s); + t6040_usb2_clear32(cfg + T6040_CFG_DP_CLKSWITCH_CTRL, 0x400); + ret = DP_CLK_POLL(2, true) ?: DP_CLK_POLL(1, true); + if (ret) + goto timeout; + } + t6040_usb2_clear32(cfg + T6040_CFG_DP_CLKSWITCH_CTRL, 0x20); + ret = DP_CLK_POLL(1, false); + if (ret) + goto timeout; + t6040_usb2_clear32(cfg + T6040_CFG_DP_CLKSWITCH_CTRL, 0x40); + ret = DP_CLK_POLL(2, false); + if (ret) + goto timeout; + + /* SWITCH2, the same way */ + t6040_usb2_clear32(cfg + T6040_CFG_DP_CLKSWITCH_CTRL, 0x403); + t6040_usb2_mask32(cfg + T6040_CFG_DP_CLKSWITCH_CTRL, 0xb8c, 0x80); + ret = DP_CLK_POLL(3, true); + if (ret) + goto timeout; + t6040_usb2_set32(cfg + T6040_CFG_DP_CLKSWITCH_CTRL, 0x300 | 0x800); + ret = DP_CLK_POLL(5, true) ?: DP_CLK_POLL(4, true); + if (ret) { + dev_dbg(tphy->dev, "DP clock switch 2: no PMGR ack with 0x800 (status %#x)\n", s); + t6040_usb2_clear32(cfg + T6040_CFG_DP_CLKSWITCH_CTRL, 0x800); + ret = DP_CLK_POLL(5, true) ?: DP_CLK_POLL(4, true); + if (ret) + goto timeout; + } + t6040_usb2_clear32(cfg + T6040_CFG_DP_CLKSWITCH_CTRL, 0x100); + ret = DP_CLK_POLL(4, false); + if (ret) + goto timeout; + t6040_usb2_clear32(cfg + T6040_CFG_DP_CLKSWITCH_CTRL, 0x200); + ret = DP_CLK_POLL(5, false); + if (ret) + goto timeout; + t6040_usb2_clear32(cfg + T6040_CFG_DP_CLKSWITCH_CTRL, 0x80c); + dev_dbg(tphy->dev, "DP clock switches on (ctrl %#x status %#x)\n", + readl(cfg + T6040_CFG_DP_CLKSWITCH_CTRL), + readl(cfg + T6040_CFG_DP_CLKSWITCH_STAT)); + return 0; + +timeout: + dev_err(tphy->dev, "DP clock switch timed out (ctrl %#x status %#x)\n", + readl(cfg + T6040_CFG_DP_CLKSWITCH_CTRL), + readl(cfg + T6040_CFG_DP_CLKSWITCH_STAT)); + return ret; +#undef DP_CLK_POLL +} + +/* A PHY without the USB3 pieces (the HDMI converter's) only ever does DP. */ +static bool t6040_dp_only(struct t6040_usb2_phy *tphy) +{ + return !tphy->usb3_ok; +} + +/* apply_tunables' generic tables: the first six of t6040_usb3_tunable_list. */ +#define T6040_DP_NR_GENERIC_TUNABLES 6 + +/* + * slpctrl_init for DP on both lanes: the cmn_rm_ovrd/txrx_rm_ovrd releases of + * t6040_usb3_lanes for lane 0 and lane 1 (USB3 releases only the plug's lane). + * Without the PHY coprocessor nothing powers the common block and the TX lanes + * once their overrides are released, so they are held on by override: the + * reverse of aciophy_cmn_shutdown / aciophy_tx_powerdown, with the override + * enables (0x2a; 0x4/0x40/0x400 per lane) set. + */ +static void t6040_dp_slpctrl(struct t6040_usb2_phy *tphy) +{ + void __iomem *core = t6040_core(tphy); + unsigned int lane; + + t6040_usb2_set32(core + T6040_CORE_CMN_BIAS, T6040_CORE_CMN_BIAS_BGBIAS_EN_OV); + udelay(1); + t6040_usb2_set32(core + T6040_CORE_CMN_OVRD, T6040_CORE_CMN_OVRD_SLEEP_B_SML | 0x2a); + udelay(1); + t6040_usb2_set32(core + T6040_CORE_CMN_OVRD, T6040_CORE_CMN_OVRD_SLEEP_B_BIG); + udelay(1); + t6040_usb2_clear32(core + T6040_CORE_CMN_OVRD, T6040_CORE_CMN_OVRD_CLAMP); + udelay(1); + for (lane = 0; lane < 2; lane++) { + t6040_usb2_set32(core + T6040_CORE_SLP_CTRL, (0x4 | 0x40 | 0x400) << lane); + t6040_usb2_set32(core + T6040_CORE_SLP_CTRL, 0x10 << lane); + udelay(1); + t6040_usb2_set32(core + T6040_CORE_SLP_CTRL, 0x1 << lane); + udelay(1); + t6040_usb2_clear32(core + T6040_CORE_SLP_CTRL, 0x100 << lane); + udelay(1); + } + dev_dbg(tphy->dev, "DP lanes held on: cmn_ovrd %#x slp_ctrl %#x bias %#x\n", + readl(core + T6040_CORE_CMN_OVRD), readl(core + T6040_CORE_SLP_CTRL), + readl(core + T6040_CORE_CMN_BIAS)); +} + +/* + * aciophy_phy_init for mode 3: wake the CFG block (shared with USB3), apply + * the generic ADT tables and the DPTX tunables, xbar_config, slpctrl_init and + * the reset deassert of partC (no coprocessor firmware in this mode). + */ +static int t6040_dp_phy_init(struct t6040_usb2_phy *tphy, bool flip) +{ + void __iomem *cfg = t6040_cfg(tphy); + size_t i; + int ret; + + ret = t6040_usb3_wake(tphy); + if (ret) + return ret; + for (i = 0; i < T6040_DP_NR_GENERIC_TUNABLES; i++) + apple_tunable_apply(tphy->regs[t6040_usb3_tunable_list[i].region], + tphy->tunables[i]); + t6040_dp_fixed_tunables(t6040_core(tphy)); + t6040_dp_xbar_config(tphy, flip); + t6040_dp_slpctrl(tphy); + t6040_usb2_set32(cfg + T6040_CFG_CTRL, T6040_CFG_CTRL_PHY_RESET_N); + fsleep(100); + dev_dbg(tphy->dev, "DP PHY init (mode 3): cfg %#x/%#x\n", + readl(cfg + T6040_CFG_CTRL), readl(cfg + T6040_CFG_STATUS)); + return 0; +} + +/* aciophy_dptx_main_init up to the link rate, which .configure sets. */ +static void t6040_dp_mode_setup(struct t6040_usb2_phy *tphy, bool flip) +{ + t6040_dp_fixed_tunables(t6040_core(tphy)); + t6040_dp_xbar_config(tphy, flip); + t6040_dp_adjust_lane_mode(tphy); + t6040_dp_aux_init(tphy); +} + +/* + * The per-rate link sequences of the T6050 fixed DP PHY, which shares the + * ATC PHY core with the T6040. Bank numbers are the atc-phy "reg" indices of + * the T6050/T6040 ADT; banks 2..34 are offsets into "core". Bank 39 is the + * T6050 clock shim; on the T6040 the DP clock switch of the "cfg" window + * stands in for it and runs after the lane program, as AppleT6040TypeCPhy + * does. The sequences hold no power-domain or tunable steps that apply here. + */ +enum atc_operation { ATC_MASK, ATC_TUNABLE, ATC_DELAY, ATC_POLL, ATC_POWER }; +struct atc_op { + u8 op, bank; + u16 offset; + u32 mask, value, timeout; +}; + +struct atc_rate_op { + struct atc_op op; + u8 rates; + u32 values[4]; +}; + +#include "atc-t6050-dp-data.h" + +static const struct { u8 bank; u32 offset; } t6040_dp_core_banks[] = { + {2, 0}, {3, 0x400}, {4, 0x800}, {5, 0xa00}, {6, 0x1000}, + {7, 0x3000}, {8, 0x3200}, {9, 0x3800}, {10, 0x3a00}, + {14, 0x7000}, {16, 0x9000}, {18, 0xb000}, {19, 0xc000}, + {20, 0xd000}, {21, 0xe000}, {22, 0xf000}, {25, 0x19000}, + {27, 0x1b000}, {28, 0x1c000}, {29, 0x1d000}, {30, 0x1e000}, + {31, 0x1f000}, {33, 0x28000}, {34, 0x28400}, +}; + +static void __iomem *t6040_dp_bank(struct t6040_usb2_phy *tphy, u8 bank) +{ + size_t i; + + for (i = 0; i < ARRAY_SIZE(t6040_dp_core_banks); i++) + if (t6040_dp_core_banks[i].bank == bank) + return t6040_core(tphy) + t6040_dp_core_banks[i].offset; + return NULL; +} + +static int t6040_dp_seq_step(struct t6040_usb2_phy *tphy, const struct atc_op *s) +{ + void __iomem *reg; + u32 v; + int ret; + + if (s->op == ATC_DELAY) { + udelay(s->value); + return 0; + } + if (s->op == ATC_POWER || s->op == ATC_TUNABLE || s->bank == 39) + return 0; + reg = t6040_dp_bank(tphy, s->bank); + if (!reg) { + dev_err(tphy->dev, "DP link sequence: no bank %u\n", s->bank); + return -EINVAL; + } + reg += s->offset; + if (s->op == ATC_MASK) { + writel((readl(reg) & ~s->mask) | s->value, reg); + return 0; + } + ret = readl_poll_timeout(reg, v, (v & s->mask) == s->value, + s->timeout > 1000 ? 1000 : 1, s->timeout); + if (ret) + dev_err(tphy->dev, "DP link sequence: bank %u+%#x timed out: %#x & %#x != %#x\n", + s->bank, s->offset, v, s->mask, s->value); + return ret; +} + +/* Take the link down if it is up, then bring it up at rate index @idx (-1: off). */ +static int t6040_dp_set_rate(struct t6040_usb2_phy *tphy, int idx) +{ + size_t i; + int ret; + + if (tphy->dp_rate >= 0) { + for (i = 0; i < ARRAY_SIZE(dp_link_off); i++) { + ret = t6040_dp_seq_step(tphy, &dp_link_off[i]); + if (ret) + return ret; + } + tphy->dp_rate = -1; + } + if (idx < 0) + return 0; + for (i = 0; i < ARRAY_SIZE(dp_link_on); i++) { + struct atc_op op = dp_link_on[i].op; + + if (!(dp_link_on[i].rates & BIT(idx))) + continue; + op.value = dp_link_on[i].values[idx]; + ret = t6040_dp_seq_step(tphy, &op); + if (ret) + return ret; + } + ret = t6040_dp_configure_clks(tphy); + if (ret) + return ret; + tphy->dp_rate = idx; + dev_dbg(tphy->dev, "DP link on, rate index %d\n", idx); + return 0; +} + +/* Drive settings of the T6050 sequence: lanes 0/1 on PMA group 1, 2/3 on group 0. */ +static void t6040_dp_drive(struct t6040_usb2_phy *tphy, unsigned int lane, u32 preset) +{ + unsigned int group = lane < 2 ? 1 : 0; + bool tx = lane & 1; + void __iomem *base = t6040_dp_bank(tphy, (tx ? 21 : 20) + 9 * group); + u32 ctrl = ((preset >> 2) & 0xf) | ((preset & 3) << 14); + u32 eq = ((preset >> 5) & 0x7fffe) | 1; + + if (tx) + t6040_usb2_mask32(base + 0x100, BIT(20), BIT(20)); + t6040_usb2_mask32(base + (tx ? 0x58 : 0x68), 0xc00f, ctrl); + t6040_usb2_mask32(base + (tx ? 0x104 : 0xac), BIT(29), BIT(29)); + t6040_usb2_mask32(base + (tx ? 0x100 : 0xa8), 0x7ffff, eq); +} + +/* ---- DP phy_ops ---- */ + +static int t6040_dp_power_on(struct phy *phy) +{ + struct t6040_usb2_phy *tphy = phy_get_drvdata(phy); + int ret; + + guard(mutex)(&tphy->lock); + if (!tphy->dp_ok || tphy->dp_up) + return 0; + /* + * A Type-C PHY in DP alt mode (pin assignment C/E) hands all four + * lanes to DP: aciophy_phy_shutdown of the USB3 side first, then the + * same mode-3 init as the fixed DP PHY. + */ + if (!t6040_dp_only(tphy)) + t6040_usb3_down_locked(tphy); + ret = t6040_dp_phy_init(tphy, tphy->dp_flip); + if (ret) { + dev_err(tphy->dev, "DP PHY init failed: %d\n", ret); + return ret; + } + t6040_dp_mode_setup(tphy, tphy->dp_flip); + tphy->dp_rate = -1; + tphy->dp_up = true; + dev_dbg(tphy->dev, "DP PHY mode set, lane map %s\n", + tphy->dp_flip ? "reversed" : "normal"); + return 0; +} + +static int t6040_dp_power_off(struct phy *phy) +{ + struct t6040_usb2_phy *tphy = phy_get_drvdata(phy); + + guard(mutex)(&tphy->lock); + tphy->dp_up = false; + return 0; +} + +/* GET_MAX_LANE_COUNT: DP on all four lanes in mode 3. */ +static int t6040_dp_validate(struct phy *phy, enum phy_mode mode, int submode, + union phy_configure_opts *opts_) +{ + struct phy_configure_opts_dp *opts = &opts_->dp; + + if (mode != PHY_MODE_DP) + return -EINVAL; + opts->lanes = 4; + return 0; +} + +static int t6040_dp_configure(struct phy *phy, union phy_configure_opts *opts_) +{ + struct t6040_usb2_phy *tphy = phy_get_drvdata(phy); + struct phy_configure_opts_dp *opts = &opts_->dp; + int idx = -1; + int ret; + + guard(mutex)(&tphy->lock); + if (!tphy->dp_up) + return -EINVAL; + + if (opts->set_rate) { + switch (opts->link_rate) { + case 1620: + idx = 0; + break; + case 2700: + idx = 1; + break; + case 5400: + idx = 2; + break; + case 8100: + idx = 3; + break; + case 0: + idx = -1; + break; + default: + return -EINVAL; + } + if (idx != tphy->dp_rate) { + ret = t6040_dp_set_rate(tphy, idx); + if (ret) + return ret; + } + } + if (opts->set_voltages && tphy->dp_rate >= 0) { + unsigned int i; + + for (i = 0; i < opts->lanes && i < 4; i++) { + unsigned int v = min_t(unsigned int, opts->voltage[i], 3); + unsigned int pe = min_t(unsigned int, opts->pre[i], 3); + + t6040_dp_drive(tphy, i, dp_training_default[4 * v + pe]); + } + } + return 0; +} + +/* ACTIVATE/DEACTIVATE: PHY_MODE_DP, submode = the DCP's DPTX port index. */ +static int t6040_dp_set_mode(struct phy *phy, enum phy_mode mode, int submode) +{ + if (mode == PHY_MODE_DP || mode == PHY_MODE_INVALID) + return 0; + return -EOPNOTSUPP; +} + +static const struct phy_ops t6040_dp_phy_ops = { + .owner = THIS_MODULE, + .power_on = t6040_dp_power_on, + .power_off = t6040_dp_power_off, + .configure = t6040_dp_configure, + .validate = t6040_dp_validate, + .set_mode = t6040_dp_set_mode, +}; + +/* ---- Type-C mode switch: the Type-C controller asks for DP on a USB-C port ---- */ + +static int t6040_dp_mux_set(struct typec_mux_dev *mux, struct typec_mux_state *state) +{ + struct t6040_usb2_phy *tphy = typec_mux_get_drvdata(mux); + bool dp = state->alt && state->alt->svid == USB_TYPEC_DP_SID; + bool alt = false; + + guard(mutex)(&tphy->lock); + tphy->dp_flip = (tphy->orientation == TYPEC_ORIENTATION_REVERSE); + /* + * Latch the DP alt mode state for the DCP's Type-C route. Pin + * assignments C and E put DP on all four lanes (PHY mode 3, as the + * fixed DP route); D (two DP lanes next to USB3, PHY mode 4 with the + * coprocessor) is not supported. The DP lanes themselves are brought + * up by the DCP through phy_power_on()/phy_configure() on the DP phy. + */ + if (dp) { + switch (state->mode) { + case TYPEC_DP_STATE_C: + case TYPEC_DP_STATE_E: + alt = true; + break; + case TYPEC_DP_STATE_D: + dev_warn_once(tphy->dev, "Type-C mux: DP pin assignment D (two lanes next to USB3) not supported\n"); + break; + default: + break; + } + } + tphy->dp_alt = alt; + dev_dbg(tphy->dev, "Type-C mux: %s state %lu -> DP alt mode %d, lane map %s\n", + dp ? "DP" : "non-DP", state->mode, alt, tphy->dp_flip ? "reversed" : "normal"); + if (alt && tphy->dp_up) + t6040_dp_xbar_config(tphy, tphy->dp_flip); + return 0; +} + +/* For the DCP's Type-C route: is this PHY's port in DP alt mode (C/E)? */ +bool apple_t6040_phy_dp_alt(struct phy *phy) +{ + struct t6040_usb2_phy *tphy = phy_get_drvdata(phy); + + return tphy && tphy->dp_alt; +} +EXPORT_SYMBOL_GPL(apple_t6040_phy_dp_alt); + +static void t6040_dp_mux_unregister(void *mux) +{ + typec_mux_unregister(mux); +} + +/* + * Everything the DP route needs from the device tree; a node without it gets + * no DP phy. A USB-C PHY offers DP through a Type-C mode switch; a fixed DP + * route (an HDMI converter's PHY) is driven straight by the DCP with no mux + * and uses the normal lane map. + */ +static int t6040_dp_probe(struct t6040_usb2_phy *tphy) +{ + struct device *dev = tphy->dev; + unsigned int i; + + tphy->dp_rate = -1; + + /* + * DP mode needs the "core" and "cfg" windows. A USB-C PHY has them + * alongside its USB regions; a fixed DP PHY has them without any + * pipehandler or USB3 coprocessor. A node with neither is USB2 only. + */ + if (!tphy->regs[T6040_USB3_CORE] || !tphy->regs[T6040_USB3_CFG]) + return 0; + + tphy->dp = devm_phy_create(dev, NULL, &t6040_dp_phy_ops); + if (IS_ERR(tphy->dp)) + return PTR_ERR(tphy->dp); + phy_set_drvdata(tphy->dp, tphy); + + /* DP-only node: the generic apply_tunables tables (absent = not applied). */ + if (t6040_dp_only(tphy)) { + for (i = 0; i < T6040_DP_NR_GENERIC_TUNABLES; i++) { + const char *prop = t6040_usb3_tunable_list[i].prop; + + if (!of_property_present(dev->of_node, prop) || + !tphy->res[t6040_usb3_tunable_list[i].region]) { + dev_warn(dev, "DP: no %s\n", prop); + continue; + } + struct resource *res = tphy->res[t6040_usb3_tunable_list[i].region]; + + tphy->tunables[i] = devm_apple_tunable_parse(dev, dev->of_node, prop, res); + if (IS_ERR(tphy->tunables[i])) { + dev_warn(dev, "DP: %s: %pe\n", prop, tphy->tunables[i]); + tphy->tunables[i] = NULL; + } + } + } + + if (of_property_present(dev->of_node, "mode-switch")) { + struct typec_mux_desc mux_desc = { + .drvdata = tphy, + .fwnode = dev_fwnode(dev), + .set = t6040_dp_mux_set, + }; + + tphy->mux = typec_mux_register(dev, &mux_desc); + if (IS_ERR(tphy->mux)) + return PTR_ERR(tphy->mux); + if (devm_add_action_or_reset(dev, t6040_dp_mux_unregister, tphy->mux)) + return -ENOMEM; + } + + tphy->dp_ok = true; + return 0; +} + /* ---- the coprocessor's RTKit ---- */ /* The coprocessor places its buffers in its own __DATA SRAM. */ @@ -1027,6 +1640,33 @@ static const struct apple_rtkit_ops t6040_cpu_rtkit_ops = { .shmem_destroy = t6040_cpu_shmem_destroy, }; +/* + * Map the PHY register windows shared by the USB3 and DisplayPort paths. Each + * is optional: a USB-C PHY has the full set, the fixed HDMI DP PHY (atcphy3) + * lacks none of core/cfg but has no pipehandler. Returns true once at least + * "core" and "cfg" are mapped, which is all DP mode needs. + */ +static bool t6040_map_regions(struct t6040_usb2_phy *tphy, struct platform_device *pdev) +{ + struct device *dev = tphy->dev; + size_t i; + + for (i = 0; i < T6040_USB3_NR_REGIONS; i++) { + tphy->res[i] = platform_get_resource_byname(pdev, IORESOURCE_MEM, + t6040_usb3_region_names[i]); + if (!tphy->res[i]) + continue; + tphy->regs[i] = devm_ioremap_resource(dev, tphy->res[i]); + if (IS_ERR(tphy->regs[i])) { + dev_warn(dev, "\"%s\" region: %pe\n", + t6040_usb3_region_names[i], tphy->regs[i]); + tphy->regs[i] = NULL; + tphy->res[i] = NULL; + } + } + return tphy->regs[T6040_USB3_CORE] && tphy->regs[T6040_USB3_CFG]; +} + /* * Everything USB3 needs from the device tree. A missing piece leaves the * port at USB2; only a mailbox that has not probed yet defers. @@ -1040,7 +1680,6 @@ static int t6040_usb3_probe(struct t6040_usb2_phy *tphy, struct platform_device .set = t6040_usb3_sw_set, }; struct typec_switch_dev *sw; - struct resource *res[T6040_USB3_NR_REGIONS]; struct resource *ph; size_t i; int ret; @@ -1066,19 +1705,11 @@ static int t6040_usb3_probe(struct t6040_usb2_phy *tphy, struct platform_device return ret; for (i = 0; i < T6040_USB3_NR_REGIONS; i++) { - res[i] = platform_get_resource_byname(pdev, IORESOURCE_MEM, - t6040_usb3_region_names[i]); - if (!res[i]) { + if (!tphy->regs[i]) { dev_warn(dev, "USB3: no \"%s\" region, USB2 only\n", t6040_usb3_region_names[i]); return 0; } - tphy->regs[i] = devm_ioremap_resource(dev, res[i]); - if (IS_ERR(tphy->regs[i])) { - dev_warn(dev, "USB3: \"%s\" region: %pe, USB2 only\n", - t6040_usb3_region_names[i], tphy->regs[i]); - return 0; - } } tphy->cpu_data = platform_get_resource_byname(pdev, IORESOURCE_MEM, "cpu-data"); ph = platform_get_resource_byname(pdev, IORESOURCE_MEM, "pipehandler"); @@ -1090,7 +1721,7 @@ static int t6040_usb3_probe(struct t6040_usb2_phy *tphy, struct platform_device for (i = 0; i < T6040_USB3_NR_TUNABLES; i++) { tphy->tunables[i] = devm_apple_tunable_parse(dev, dev->of_node, t6040_usb3_tunable_list[i].prop, - res[t6040_usb3_tunable_list[i].region]); + tphy->res[t6040_usb3_tunable_list[i].region]); if (IS_ERR(tphy->tunables[i])) { dev_warn(dev, "USB3: %s: %pe, USB2 only\n", t6040_usb3_tunable_list[i].prop, tphy->tunables[i]); @@ -1206,6 +1837,8 @@ static struct phy *t6040_usb2_xlate(struct device *dev, return tphy->phy; case PHY_TYPE_USB3: return tphy->usb3; + case PHY_TYPE_DP: + return tphy->dp; default: return ERR_PTR(-ENODEV); } @@ -1267,22 +1900,37 @@ static int t6040_usb2_probe(struct platform_device *pdev) return PTR_ERR(tphy->phy); phy_set_drvdata(tphy->phy, tphy); - tphy->pipehandler = devm_platform_ioremap_resource_byname(pdev, "pipehandler"); - if (IS_ERR(tphy->pipehandler)) - return dev_err_probe(dev, PTR_ERR(tphy->pipehandler), - "failed to map pipehandler bank\n"); - - tphy->rcdev.owner = THIS_MODULE; - tphy->rcdev.nr_resets = 1; - tphy->rcdev.ops = &t6040_usb2_reset_ops; - tphy->rcdev.of_node = dev->of_node; - tphy->rcdev.of_reset_n_cells = 0; - tphy->rcdev.of_xlate = t6040_usb2_reset_xlate; - ret = devm_reset_controller_register(dev, &tphy->rcdev); + /* + * The USB-C PHYs have a pipehandler and drive a dwc3 (reset provider); + * the fixed HDMI DP PHY (atcphy3) has neither. Map it and register the + * reset controller only when present, so a DP-only node still probes. + */ + if (platform_get_resource_byname(pdev, IORESOURCE_MEM, "pipehandler")) { + tphy->pipehandler = devm_platform_ioremap_resource_byname(pdev, + "pipehandler"); + if (IS_ERR(tphy->pipehandler)) + return dev_err_probe(dev, PTR_ERR(tphy->pipehandler), + "failed to map pipehandler bank\n"); + + tphy->rcdev.owner = THIS_MODULE; + tphy->rcdev.nr_resets = 1; + tphy->rcdev.ops = &t6040_usb2_reset_ops; + tphy->rcdev.of_node = dev->of_node; + tphy->rcdev.of_reset_n_cells = 0; + tphy->rcdev.of_xlate = t6040_usb2_reset_xlate; + ret = devm_reset_controller_register(dev, &tphy->rcdev); + if (ret) + return ret; + } + + /* Map the core/cfg (and USB3) windows once; both sub-probes share them. */ + t6040_map_regions(tphy, pdev); + + ret = t6040_usb3_probe(tphy, pdev); if (ret) return ret; - ret = t6040_usb3_probe(tphy, pdev); + ret = t6040_dp_probe(tphy); if (ret) return ret; @@ -1306,5 +1954,5 @@ static struct platform_driver t6040_usb2_driver = { module_platform_driver(t6040_usb2_driver); MODULE_AUTHOR("The Asahi Linux Contributors"); -MODULE_DESCRIPTION("Apple T6040 Type-C PHY, USB2 and USB3 host"); +MODULE_DESCRIPTION("Apple T6040 Type-C PHY, USB2/USB3 host and DisplayPort"); MODULE_LICENSE("Dual BSD/GPL"); diff --git a/include/linux/soc/apple/dp-tunnel.h b/include/linux/soc/apple/dp-tunnel.h index 7bd546080f25b..194a238927190 100644 --- a/include/linux/soc/apple/dp-tunnel.h +++ b/include/linux/soc/apple/dp-tunnel.h @@ -51,6 +51,14 @@ int apple_atc_dp_tunnel_rate(struct phy *phy, unsigned int dpin, u8 rate); */ int apple_atc_dp_tunnel_open(struct phy *phy); +/* + * T6040 Type-C PHY: true while the Type-C controller holds the PHY's port in + * DisplayPort alt mode with pin assignment C or E (DP on all four lanes), so + * appledrm can route a DCP that lists several Type-C DP PHYs to the one that + * has a DisplayPort sink plugged. + */ +bool apple_t6040_phy_dp_alt(struct phy *phy); + /* * Display crossbar: take the connection of an already selected output down or * bring it back up (clock gates and enables) around a link reconfiguration. From 2addd75a379958c0b28c5b38338483ca90a9de2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Bal=C3=A1zsy?= Date: Sun, 25 Oct 2026 12:59:20 +0100 Subject: [PATCH 04/12] drm/apple: tell the H17 26.x lineage apart from the desktop one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The IOMFB template keys its H17 (A18 Pro, M5) wire changes on DCP_FW_VER >= 26.0. The desktop firmware of the M4 Pro/Max DCP (T6041) also reports 26.x, but continues the 14.7 IOMFB. Mark a desktop instance's version with DCP_FW_DESKTOP and key the H17 branches on DCP_FW_H17(). No functional change: the 12.3, 13.3, 14.7 and H17P template objects are unchanged. Signed-off-by: František Balázsy --- drivers/gpu/drm/apple/iomfb_template.c | 104 ++++++++++++------------- drivers/gpu/drm/apple/iomfb_template.h | 16 ++-- drivers/gpu/drm/apple/version_utils.h | 15 ++++ 3 files changed, 75 insertions(+), 60 deletions(-) diff --git a/drivers/gpu/drm/apple/iomfb_template.c b/drivers/gpu/drm/apple/iomfb_template.c index c1b0e21b65e5e..fcb74e0b88dc1 100644 --- a/drivers/gpu/drm/apple/iomfb_template.c +++ b/drivers/gpu/drm/apple/iomfb_template.c @@ -65,7 +65,7 @@ IOMFB_THUNK_INOUT(get_color_remap_mode); IOMFB_THUNK_INOUT(last_client_close); IOMFB_THUNK_INOUT(abort_swaps_dcp); -#if DCP_FW_VERSION(26, 0, 0) <= DCP_FW_VER +#if DCP_FW_H17(DCP_FW_VER) static void dcp_swap_submit(struct apple_dcp *dcp, bool oob, struct dcp_swap_submit_req_h17p *request, dcp_callback_t cb, void *cookie) @@ -103,7 +103,7 @@ DCP_THUNK_INOUT(dcp_set_power_state, dcpep_set_power_state, * channel. Sent on the ordinary command channel it is never answered, which * strands the power transition and every later call behind it. */ -#if DCP_FW_VERSION(26, 0, 0) <= DCP_FW_VER +#if DCP_FW_H17(DCP_FW_VER) #define DCP_POWER_OOB true #else #define DCP_POWER_OOB false @@ -116,7 +116,7 @@ DCP_THUNK_INOUT(dcp_set_power_state, dcpep_set_power_state, * is no vblank and no swap completion. Keep the mode the bootloader * programmed, which is the panel's native mode, instead of requesting one. */ -#if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) +#if DCP_FW_H17(DCP_FW_VER) #define DCP_INHERIT_BOOT_MODE true #else #define DCP_INHERIT_BOOT_MODE false @@ -129,7 +129,7 @@ DCP_THUNK_INOUT(dcp_set_power_state, dcpep_set_power_state, * straight to set_power_state, whose number and sizes are known. Power-off * still sends its clear swap first. */ -#if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) +#if DCP_FW_H17(DCP_FW_VER) #define DCP_HAS_CLIENT_TEARDOWN false #else #define DCP_HAS_CLIENT_TEARDOWN true @@ -154,7 +154,7 @@ DCP_THUNK_OUT(dcp_start_signal, dcpep_start_signal, u32); DCP_THUNK_VOID(dcp_setup_video_limits, dcpep_setup_video_limits); DCP_THUNK_VOID(dcp_set_create_dfb, dcpep_set_create_dfb); -#if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) +#if DCP_FW_H17(DCP_FW_VER) /* * Default-framebuffer queries issued from inside start_hardware_boot (D121). * The getters take a zeroed input of the expected size; the firmware only @@ -252,7 +252,7 @@ static void dcpep_cb_swap_complete(struct apple_dcp *dcp, ktime_t now = ktime_get(); bool retire = true; -#if DCP_FW_VERSION(26, 0, 0) <= DCP_FW_VER +#if DCP_FW_H17(DCP_FW_VER) if (dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17G && !dcp_present_complete_h17p(&dcp->present_state_h17p, resp->swap_id)) { /* @@ -294,7 +294,7 @@ static void dcpep_cb_swap_complete(struct apple_dcp *dcp, dcp_drm_crtc_page_flip(dcp, now); } if (READ_ONCE(dcp->crc_enabled)) { -#if DCP_FW_VERSION(26, 0, 0) <= DCP_FW_VER +#if DCP_FW_H17(DCP_FW_VER) if (dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17G) { iomfb_queue_crc_h17p(dcp, resp->swap_id); return; @@ -323,7 +323,7 @@ static bool iomfbep_cb_match_pmu_service(struct apple_dcp *dcp, int tag, void *o return false; } -#if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) +#if DCP_FW_H17(DCP_FW_VER) /* * H17P asks the AP to make the default framebuffer surface (D582). The * firmware expects the AP to register a surface from inside that callback, @@ -480,7 +480,7 @@ static void iomfb_cb_pr_publish(struct apple_dcp *dcp, struct iomfb_property *pr case IOMFB_PROPERTY_NITS_V14_7: #endif { -#if DCP_FW_VERSION(26, 0, 0) <= DCP_FW_VER +#if DCP_FW_H17(DCP_FW_VER) /* The H17P takeover level comes from the powerlog hint interface. */ if (dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17G) break; @@ -577,7 +577,7 @@ static bool dcpep_map_piodma(struct apple_dcp *dcp, goto reject; } -#if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) +#if DCP_FW_H17(DCP_FW_VER) memdesc->piodma_mapped = true; /* the H17P reply echoes the buffer index */ @@ -604,7 +604,7 @@ static struct DCP_FW_NAME(dcp_map_buf_resp) dcpep_cb_map_piodma(struct apple_dcp if (dcpep_map_piodma(dcp, req, &resp)) return resp; -#if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) +#if DCP_FW_H17(DCP_FW_VER) resp.buffer = req->buffer; #else resp.ret = EINVAL; @@ -629,7 +629,7 @@ static bool dcpep_unmap_piodma(struct apple_dcp *dcp, if (!test_bit(request->buffer, dcp->memdesc_map) || !memdesc->buf || !dcp->iommu_dom) goto not_mapped; -#if DCP_FW_VERSION(26, 0, 0) <= DCP_FW_VER +#if DCP_FW_H17(DCP_FW_VER) if (!memdesc->piodma_mapped) goto not_mapped; #endif @@ -716,7 +716,7 @@ dcpep_cb_allocate_buffer(struct apple_dcp *dcp, resp.dva_size = allocated.size; resp.dva = allocated.dva; -#if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) +#if DCP_FW_H17(DCP_FW_VER) /* * Linux allocation traces correlate the first H17P reply address with * the first SG page. The remaining pages need not be contiguous. @@ -860,7 +860,7 @@ static u64 dcpep_cb_get_frequency(struct apple_dcp *dcp) return clk_get_rate(dcp->clk); } -#if DCP_FW_VER >= DCP_FW_VERSION(13, 2, 0) && DCP_FW_VER < DCP_FW_VERSION(26, 0, 0) +#if DCP_FW_VER >= DCP_FW_VERSION(13, 2, 0) && !DCP_FW_H17(DCP_FW_VER) /* The DVA that map_reg hands out for display register range @index. */ static dma_addr_t dcpep_map_reg_dva(struct apple_dcp *dcp, u32 index, struct resource *rsrc) @@ -898,7 +898,7 @@ static struct DCP_FW_NAME(dcp_map_reg_resp) dcpep_cb_map_reg(struct apple_dcp *d return (struct DCP_FW_NAME(dcp_map_reg_resp)){ .ret = 1 }; } else { struct resource *rsrc = dcp->disp_registers[req->index]; -#if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) +#if DCP_FW_H17(DCP_FW_VER) /* * H17P reaches these apertures physically, not through its * DART, and expects dva == addr in the reply. Trying to @@ -940,7 +940,7 @@ dcpep_cb_read_edt_data(struct apple_dcp *dcp, struct dcp_read_edt_data_req *req) static void iomfbep_cb_enable_backlight_message_ap_gated(struct apple_dcp *dcp, u8 *enabled) { -#if DCP_FW_VERSION(26, 0, 0) <= DCP_FW_VER +#if DCP_FW_H17(DCP_FW_VER) /* H17P carries the level only in presents; ask for one more. */ if (dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17G) { if (dcp_has_panel(dcp) && dcp_backlight_active(dcp) && @@ -967,7 +967,7 @@ static u8 dcpep_cb_prop_start(struct apple_dcp *dcp, u32 *length) return false; } -#if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) +#if DCP_FW_H17(DCP_FW_VER) /* * This length comes straight off the wire. A bogus value would turn * the allocation below into a multi-gigabyte GFP_KERNEL request. @@ -1168,7 +1168,7 @@ static void boot_5b(struct apple_dcp *dcp, void *out, void *cookie) static void boot_5(struct apple_dcp *dcp, void *out, void *cookie) { -#if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) +#if DCP_FW_H17(DCP_FW_VER) /* H17P expects A380 right before set_display_refresh_properties */ dcp_dfb_ready_query(dcp, false, boot_5b, NULL); #else @@ -1179,7 +1179,7 @@ static void boot_5(struct apple_dcp *dcp, void *out, void *cookie) static void boot_4b(struct apple_dcp *dcp, void *out, void *cookie) { #if DCP_FW_VER >= DCP_FW_VERSION(13, 2, 0) -#if DCP_FW_VERSION(26, 0, 0) <= DCP_FW_VER +#if DCP_FW_H17(DCP_FW_VER) u32 value = dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17G; #else u32 value = 1; @@ -1192,7 +1192,7 @@ static void boot_4b(struct apple_dcp *dcp, void *out, void *cookie) static void boot_4(struct apple_dcp *dcp, void *out, void *cookie) { -#if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) +#if DCP_FW_H17(DCP_FW_VER) /* H17P expects A033 between flush_supports_power and late_init_signal */ dcp_dfb_query(dcp, false, boot_4b, NULL); #else @@ -1212,7 +1212,7 @@ static void boot_2(struct apple_dcp *dcp, void *out, void *cookie) dcp_setup_video_limits(dcp, false, boot_3, NULL); } -#if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) +#if DCP_FW_H17(DCP_FW_VER) /* * H17P expects the default framebuffer to be described between * create_default_fb and setup_video_limits: @@ -1280,14 +1280,14 @@ static void boot_1_6(struct apple_dcp *dcp, void *out, void *cookie) static void boot_1_5(struct apple_dcp *dcp, void *out, void *cookie) { -#if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) +#if DCP_FW_H17(DCP_FW_VER) dcp_create_default_fb(dcp, false, boot_1_6, NULL); #else dcp_create_default_fb(dcp, false, boot_2, NULL); #endif } -#if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) +#if DCP_FW_H17(DCP_FW_VER) static void d000_done(struct apple_dcp *dcp, void *out, void *cookie) { struct dcp_channel *ch = &dcp->ch_cb; @@ -1356,7 +1356,7 @@ static struct dcp_rt_bandwidth dcpep_cb_rt_bandwidth(struct apple_dcp *dcp) return rt_bw; } -#if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) +#if DCP_FW_H17(DCP_FW_VER) /* * H17P's D006 is 0x54 in / 0x50 out, far wider than the 28-byte * frame_sync_props of older firmware, so the fields that matter sit past @@ -1424,7 +1424,7 @@ static void release_swap_cookie(struct kref *ref) kfree(cookie); } -#if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) +#if DCP_FW_H17(DCP_FW_VER) #define DCP_SWAP_FLAGS1 0x34865202ULL #define DCP_SWAP_FLAGS2 0x4ULL @@ -1496,7 +1496,7 @@ static void dcp_h17p_prepare_swap(struct apple_dcp *dcp) { } static bool dcp_present_retires(struct apple_dcp *dcp) { -#if DCP_FW_VERSION(26, 0, 0) <= DCP_FW_VER +#if DCP_FW_H17(DCP_FW_VER) if (dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17G) return !iomfb_present_brightness_only_h17p(dcp); #endif @@ -1505,7 +1505,7 @@ static bool dcp_present_retires(struct apple_dcp *dcp) static void dcp_present_failed(struct apple_dcp *dcp) { -#if DCP_FW_VERSION(26, 0, 0) <= DCP_FW_VER +#if DCP_FW_H17(DCP_FW_VER) if (dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17G) iomfb_present_failed_h17p(dcp); #endif @@ -1513,7 +1513,7 @@ static void dcp_present_failed(struct apple_dcp *dcp) static bool dcp_present_begin(struct apple_dcp *dcp, u32 swap_id) { -#if DCP_FW_VERSION(26, 0, 0) <= DCP_FW_VER +#if DCP_FW_H17(DCP_FW_VER) if (dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17G && !dcp_present_begin_h17p(&dcp->present_state_h17p, swap_id)) { dev_err(dcp->dev, "overlapping present %u\n", swap_id); @@ -1531,7 +1531,7 @@ static bool dcp_present_begin(struct apple_dcp *dcp, u32 swap_id) */ static int dcp_present_submit(struct apple_dcp *dcp, u32 swap_id, bool accepted) { -#if DCP_FW_VERSION(26, 0, 0) <= DCP_FW_VER +#if DCP_FW_H17(DCP_FW_VER) if (dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17G && !dcp_present_submit_h17p(&dcp->present_state_h17p, swap_id, accepted)) { dev_err(dcp->dev, "unexpected present submission %u\n", swap_id); @@ -1581,7 +1581,7 @@ static void dcp_prepare_clear_swap(struct apple_dcp *dcp, void *request) for (int l = 0; l < SWAP_SURFACES; l++) swap->surf_null[l] = true; #if DCP_FW_VERSION(13, 2, 0) <= DCP_FW_VER -#if DCP_FW_VERSION(26, 0, 0) > DCP_FW_VER +#if !DCP_FW_H17(DCP_FW_VER) for (int l = 0; l < 5; l++) swap->surf2_null[l] = true; #endif @@ -1643,7 +1643,7 @@ static void dcp_swap_clear_started(struct apple_dcp *dcp, void *data, dcp_drm_crtc_vblank(dcp->crtc); return; } -#if DCP_FW_VERSION(26, 0, 0) <= DCP_FW_VER +#if DCP_FW_H17(DCP_FW_VER) if (dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17G) dcp_prepare_clear_swap(dcp, &DCP_FW_UNION(dcp->swap)); #endif @@ -1662,7 +1662,7 @@ static void dcp_on_final(struct apple_dcp *dcp, void *out, void *cookie) { struct dcp_wait_cookie *wait = cookie; -#if DCP_FW_VERSION(26, 0, 0) <= DCP_FW_VER +#if DCP_FW_H17(DCP_FW_VER) if (dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17G && out && get_unaligned_le64(out) == 1) WRITE_ONCE(dcp->pipe_enabled_h17p, true); @@ -1705,7 +1705,7 @@ void DCP_FW_NAME(iomfb_poweron)(struct apple_dcp *dcp) u32 handle; dev_info(dcp->dev, "dcp_poweron() starting\n"); -#if DCP_FW_VERSION(26, 0, 0) <= DCP_FW_VER +#if DCP_FW_H17(DCP_FW_VER) if (dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17G) iomfb_opaque_x_reset_h17p(dcp); #endif @@ -1721,7 +1721,7 @@ void DCP_FW_NAME(iomfb_poweron)(struct apple_dcp *dcp) kref_get(&cookie->refcount); if (dcp->main_display) { -#if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) +#if DCP_FW_H17(DCP_FW_VER) /* * H17P takes set_display_device (A411) once, in the * client-open sequence. Repeating it after the client is open @@ -1777,7 +1777,7 @@ static void complete_set_powerstate(struct apple_dcp *dcp, void *out, static bool dcp_power_stop_confirmed(struct dcp_wait_cookie *cookie) { /* H17P does not confirm that scanout has stopped. */ -#if DCP_FW_VERSION(26, 0, 0) <= DCP_FW_VER +#if DCP_FW_H17(DCP_FW_VER) return false; #else return READ_ONCE(cookie->status) == 0; @@ -1818,7 +1818,7 @@ void DCP_FW_NAME(iomfb_poweroff)(struct apple_dcp *dcp) struct DCP_FW_NAME(dcp_swap_start_req) swap_req = { 0 }; typeof(DCP_FW_UNION(dcp->swap)) *swap = &DCP_FW_UNION(dcp->swap); -#if DCP_FW_VERSION(26, 0, 0) <= DCP_FW_VER +#if DCP_FW_H17(DCP_FW_VER) /* H17P has no known safe pipe-off or surface-free transition. */ if (dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17G) return; @@ -1833,7 +1833,7 @@ void DCP_FW_NAME(iomfb_poweroff)(struct apple_dcp *dcp) /* increase refcount to ensure the receiver has a reference */ kref_get(&cookie->refcount); -#if DCP_FW_VERSION(26, 0, 0) <= DCP_FW_VER +#if DCP_FW_H17(DCP_FW_VER) if (dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17G) dcp_prepare_clear_swap(dcp, swap); #else @@ -1922,7 +1922,7 @@ void DCP_FW_NAME(iomfb_sleep)(struct apple_dcp *dcp) struct dcp_wait_cookie *cookie; -#if DCP_FW_VERSION(26, 0, 0) <= DCP_FW_VER +#if DCP_FW_H17(DCP_FW_VER) if (dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17G) return; #endif @@ -2039,7 +2039,7 @@ dcpep_cb_swap_complete_intent_gated(struct apple_dcp *dcp, info->width, info->height); } -#if DCP_FW_VERSION(26, 0, 0) <= DCP_FW_VER +#if DCP_FW_H17(DCP_FW_VER) /* * An aborted H17P present never completes. Finish it like a rejected one: * keep the displaced framebuffers for the next completed present and signal @@ -2072,7 +2072,7 @@ static void dcpep_cb_abort_swap_ap_gated(struct apple_dcp *dcp, u32 *swap_id) { trace_iomfb_abort_swap_ap_gated(dcp, *swap_id); -#if DCP_FW_VERSION(26, 0, 0) <= DCP_FW_VER +#if DCP_FW_H17(DCP_FW_VER) if (dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17G) dcp_present_aborted(dcp, *swap_id); #endif @@ -2090,7 +2090,7 @@ dcpep_cb_get_tiling_state(struct apple_dcp *dcp, static u8 dcpep_cb_create_backlight_service(struct apple_dcp *dcp) { -#if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) +#if DCP_FW_H17(DCP_FW_VER) /* * H17P asks during start_ios_gated, before any mode is known, but * dcp_has_panel() only becomes true once the panel dimensions have been @@ -2145,7 +2145,7 @@ TRAMPOLINE_OUT(trampoline_rt_bandwidth, dcpep_cb_rt_bandwidth, TRAMPOLINE_INOUT(trampoline_set_frame_sync_props, dcpep_cb_set_frame_sync_props, struct dcp_set_frame_sync_props_req, struct dcp_set_frame_sync_props_resp); -#if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) +#if DCP_FW_H17(DCP_FW_VER) TRAMPOLINE_INOUT(trampoline_set_frame_sync_props_h17p, dcpep_cb_set_frame_sync_props_h17p, struct dcp_set_frame_sync_props_req_h17p, @@ -2253,12 +2253,12 @@ static void do_swap(struct apple_dcp *dcp, void *data, void *cookie) } } -#if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) +#if DCP_FW_H17(DCP_FW_VER) /* * H17P expects A428 between set_matrix and swap_start, with 0x10000 (1.0 in * 16.16 fixed point), the same identity value the D006 reply carries. */ -#if DCP_FW_VERSION(26, 0, 0) <= DCP_FW_VER +#if DCP_FW_H17(DCP_FW_VER) void iomfb_present_backlight_h17p(struct apple_dcp *dcp) { do_swap(dcp, NULL, NULL); @@ -2455,7 +2455,7 @@ void DCP_FW_NAME(iomfb_flush)(struct apple_dcp *dcp, struct drm_crtc *crtc, stru for (l = 0; l < SWAP_SURFACES; l++) req->surf_null[l] = true; #if DCP_FW_VER >= DCP_FW_VERSION(13, 2, 0) -#if DCP_FW_VER < DCP_FW_VERSION(26, 0, 0) +#if !DCP_FW_H17(DCP_FW_VER) for (l = 0; l < 5; l++) req->surf2_null[l] = true; #endif @@ -2493,7 +2493,7 @@ void DCP_FW_NAME(iomfb_flush)(struct apple_dcp *dcp, struct drm_crtc *crtc, stru req->swap.src_rect[l] = apple_state->src_rect; req->swap.dst_rect[l] = apple_state->dst_rect; -#if DCP_FW_VER < DCP_FW_VERSION(26, 0, 0) +#if !DCP_FW_H17(DCP_FW_VER) req->swap.surf_ids[apl_plane->iomfb_surf] = plane->base.id; #endif @@ -2502,7 +2502,7 @@ void DCP_FW_NAME(iomfb_flush)(struct apple_dcp *dcp, struct drm_crtc *crtc, stru req->surf_iova[l] = apple_state->iova; req->surf[l].base = apple_state->surf; -#if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) +#if DCP_FW_H17(DCP_FW_VER) req->surf[l].base.surface_id = 0; #endif @@ -2515,7 +2515,7 @@ void DCP_FW_NAME(iomfb_flush)(struct apple_dcp *dcp, struct drm_crtc *crtc, stru req->surf[l].base.colorspace == DCP_COLORSPACE_BG_SRGB) req->surf[l].base.colorspace = DCP_COLORSPACE_NATIVE; -#if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) +#if DCP_FW_H17(DCP_FW_VER) /* H17P external SDR RGB needs an sRGB source tag. Native * tagging washes out the same pixel values on this profile. * Keep YUV, other source spaces and other profiles unchanged. @@ -2568,7 +2568,7 @@ void DCP_FW_NAME(iomfb_flush)(struct apple_dcp *dcp, struct drm_crtc *crtc, stru /* update brightness if changed */ update_brightness = dcp_has_panel(dcp) && dcp->brightness.update; -#if DCP_FW_VERSION(26, 0, 0) <= DCP_FW_VER +#if DCP_FW_H17(DCP_FW_VER) update_brightness = update_brightness && dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17G; #endif @@ -2595,7 +2595,7 @@ void DCP_FW_NAME(iomfb_flush)(struct apple_dcp *dcp, struct drm_crtc *crtc, stru mat.matrix[0] = mat.matrix[4] = mat.matrix[8] = 1LLU << 32; } -#if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) +#if DCP_FW_H17(DCP_FW_VER) iomfb_set_matrix(dcp, false, &mat, dcp_pre_swap_a428, NULL); #else iomfb_set_matrix(dcp, false, &mat, do_swap, NULL); @@ -2640,7 +2640,7 @@ static void init_3(struct apple_dcp *dcp, void *out, void *cookie) dcp_is_main_display(dcp, false, res_is_main_display, NULL); } -#if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) +#if DCP_FW_H17(DCP_FW_VER) /* * The client-open sequence H17P firmware expects before the first swap, in * H17P method numbers (dcp_push() applies the H17G aliases): @@ -2807,7 +2807,7 @@ void DCP_FW_NAME(iomfb_shutdown)(struct apple_dcp *dcp) struct dcp_wait_cookie *cookie; int ret; -#if DCP_FW_VERSION(26, 0, 0) <= DCP_FW_VER +#if DCP_FW_H17(DCP_FW_VER) if (dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17G) { iomfb_queue_stop(dcp); return; diff --git a/drivers/gpu/drm/apple/iomfb_template.h b/drivers/gpu/drm/apple/iomfb_template.h index b987ddfbd8ce7..bebb9c0f5c265 100644 --- a/drivers/gpu/drm/apple/iomfb_template.h +++ b/drivers/gpu/drm/apple/iomfb_template.h @@ -28,7 +28,7 @@ struct DCP_FW_NAME(dcp_swap) { u8 unk_v14_7[0x48]; #endif -#if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) +#if DCP_FW_H17(DCP_FW_VER) /* * H17P inserts 0x48 bytes between flags2 and the swap ID, which moves * to +0x98. Every following field shifts with it: surf_ids +0x9c, @@ -86,7 +86,7 @@ struct DCP_FW_NAME(dcp_swap) { u32 unk_flags; u32 unk_flags2; #endif -#if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) +#if DCP_FW_H17(DCP_FW_VER) /* * The H17P A408 request is 0xe9c bytes; with a 0x22c-byte surface, * SWAP_SURFACES = 4 and no surf2 block, that leaves this tail. @@ -117,7 +117,7 @@ struct DCP_FW_NAME(dcp_swap_submit_req) { * H17P carries no surf2 block: its trailing u8 null-flag run is 8 bytes * (swap_null + surf_null[4] + three tail bools). */ -#if DCP_FW_VER < DCP_FW_VERSION(26, 0, 0) +#if !DCP_FW_H17(DCP_FW_VER) struct DCP_FW_NAME(dcp_surface) surf2[5]; u64 surf2_iova[5]; #endif @@ -138,7 +138,7 @@ struct DCP_FW_NAME(dcp_swap_submit_req) { u8 swap_null; u8 surf_null[SWAP_SURFACES]; #if DCP_FW_VER >= DCP_FW_VERSION(13, 2, 0) -#if DCP_FW_VER < DCP_FW_VERSION(26, 0, 0) +#if !DCP_FW_H17(DCP_FW_VER) u8 surf2_null[5]; #endif #endif @@ -147,7 +147,7 @@ struct DCP_FW_NAME(dcp_swap_submit_req) { u8 unknown_pointer_null; u8 unknown_output_null; #endif -#if DCP_FW_VER < DCP_FW_VERSION(26, 0, 0) +#if !DCP_FW_H17(DCP_FW_VER) u8 padding[1]; #endif } __packed; @@ -167,7 +167,7 @@ struct DCP_FW_NAME(dcp_swap_submit_resp) { * swap_start response no longer echoes it. swap_start is 0x10 in / 0x8 out, * set_power_state 0x10 in. */ -#if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) +#if DCP_FW_H17(DCP_FW_VER) struct DCP_FW_NAME(dcp_swap_start_req) { u32 swap_id; u64 client; /* IOUserClient kernel VA */ @@ -219,7 +219,7 @@ struct DCP_FW_NAME(dcp_set_power_state_req) { * u32 ret }. With the old layout the DVA lands four bytes late and the * display engine DMAs to a bogus address. */ -#if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) +#if DCP_FW_H17(DCP_FW_VER) struct DCP_FW_NAME(dcp_map_buf_resp) { u32 buffer; u64 dva; @@ -237,7 +237,7 @@ struct DCP_FW_NAME(dcp_map_buf_resp) { struct DCP_FW_NAME(dc_swap_complete_resp) { u32 swap_id; /* +0x000 */ u8 unkbool; /* +0x004 */ -#if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) +#if DCP_FW_H17(DCP_FW_VER) /* * H17P: swap_complete_ap_gated(u32, bool, SwapCompleteData const *, * SwapInfoBlob const *, u32, bool) is 0x730 bytes on the wire, 89 more diff --git a/drivers/gpu/drm/apple/version_utils.h b/drivers/gpu/drm/apple/version_utils.h index 053bc0a15a844..177d8f0119201 100644 --- a/drivers/gpu/drm/apple/version_utils.h +++ b/drivers/gpu/drm/apple/version_utils.h @@ -12,6 +12,21 @@ #define DCP_FW_NAME(name) CONCATENATE(name, DCP_FW_SUFFIX) #define DCP_FW_VERSION(x, y, z) ( ((x) << 16) | ((y) << 8) | (z) ) +/* + * 26.x firmware exists in two unrelated wire lineages. The H17-generation + * firmware (A18 Pro, M5) is a new protocol; the M4-generation desktop + * firmware (T6041, the M4 Pro/Max) continues the 14.7 IOMFB. A desktop + * 26.x template instance marks its version with DCP_FW_DESKTOP, so that the + * plain ordering against older versions still holds. + */ +#define DCP_FW_DESKTOP (1 << 24) +#define DCP_FW_VERSION_DESKTOP(x, y, z) (DCP_FW_VERSION(x, y, z) | DCP_FW_DESKTOP) +#define DCP_FW_H17(ver) \ + ((ver) >= DCP_FW_VERSION(26, 0, 0) && !((ver) & DCP_FW_DESKTOP)) +#define DCP_FW_DESKTOP_26(ver) \ + (((ver) & DCP_FW_DESKTOP) && \ + ((ver) & ~DCP_FW_DESKTOP) >= DCP_FW_VERSION(26, 0, 0)) + /* * The swap record layout introduced with the 14.7 firmware. The * H17-generation firmware (26.0 and later) keeps the earlier layout and From a3c33e96fa31a05c8e17eb2baffa460ff6afc7c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Bal=C3=A1zsy?= Date: Sun, 25 Oct 2026 12:59:20 +0100 Subject: [PATCH 05/12] drm/apple: add the desktop 26.6 IOMFB template instance for T6041 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 26.6 firmware of the M4 Pro/Max DCP (T6041) continues the 14.7 IOMFB rather than the H17 protocol: most method numbers match H17P, but the records and the start, power and mode-set flows are those of 14.7 with extensions. Add it as a template instance, iomfb_v26_6_desktop.c, with firmware compatibility value DCP_FIRMWARE_V_26_6_DESKTOP. Nothing selects it yet. The instance carries this firmware's wire layout: a 0x588-byte swap record with presents_surface, has_completion and the panel level in nits as a binary64, a 0x1bd8-byte swap_submit with six surf2 entries, swap_start with an opaque client cookie, set_power_state with four transition flags, and the padded abort_swaps, read_edt_data, hotplug, swap intent and frame_sync records. DCP_FW_LAYOUT_14_7() covers the desktop 26.x versions, and static assertions pin the record sizes. Power-on is A414, first_client_open, A474, is_main_display and set_power_state(1), then set_display_device; power-off clears the swap and sets power state 0. A mode set selects the timing through get_mode_info (A364) before set_digital_out_mode. A pipe without a live mode is bootstrapped through the timing's best virtual color element, which the parser now records. GAMMA_LUT is composed with the firmware's gamma table (A420/A421). VRR timings are not offered. The per-DCP desktop state is allocated only for this firmware, so other instances keep their struct apple_dcp size and the 12.3, 13.3, 14.7 and H17P template objects are unchanged. Co-developed-by: Niklas Sheth Signed-off-by: Niklas Sheth Co-developed-by: Josh Rodd Signed-off-by: Josh Rodd Co-developed-by: Jonathan Goyvaerts Signed-off-by: Jonathan Goyvaerts Signed-off-by: František Balázsy --- drivers/gpu/drm/apple/Makefile | 1 + drivers/gpu/drm/apple/apple_drv.c | 10 +- drivers/gpu/drm/apple/av.c | 1 + drivers/gpu/drm/apple/dcp-internal.h | 31 + drivers/gpu/drm/apple/dcp.c | 37 + drivers/gpu/drm/apple/dcp.h | 1 + drivers/gpu/drm/apple/dcp_backlight.c | 35 +- drivers/gpu/drm/apple/iomfb.c | 25 + drivers/gpu/drm/apple/iomfb.h | 84 ++ drivers/gpu/drm/apple/iomfb_template.c | 817 +++++++++++++++++++- drivers/gpu/drm/apple/iomfb_template.h | 81 +- drivers/gpu/drm/apple/iomfb_v26_6_desktop.c | 166 ++++ drivers/gpu/drm/apple/iomfb_v26_6_desktop.h | 52 ++ drivers/gpu/drm/apple/parser.c | 13 +- drivers/gpu/drm/apple/parser.h | 7 + drivers/gpu/drm/apple/version_utils.h | 14 +- 16 files changed, 1333 insertions(+), 42 deletions(-) create mode 100644 drivers/gpu/drm/apple/iomfb_v26_6_desktop.c create mode 100644 drivers/gpu/drm/apple/iomfb_v26_6_desktop.h diff --git a/drivers/gpu/drm/apple/Makefile b/drivers/gpu/drm/apple/Makefile index f2a6f2c18b824..91b85f9542d8b 100644 --- a/drivers/gpu/drm/apple/Makefile +++ b/drivers/gpu/drm/apple/Makefile @@ -14,6 +14,7 @@ appledrm-y += dcpext_drm.o appledrm-y += iomfb_v12_3.o appledrm-y += iomfb_v13_3.o appledrm-y += iomfb_v14_7_tpl.o +appledrm-y += iomfb_v26_6_desktop.o appledrm-y += iomfb_v14_7.o iomfb_v14_7_link.o appledrm-$(CONFIG_DRM_APPLE_DCP_T8132) += iomfb_m4.o appledrm-y += iomfb_h17p.o diff --git a/drivers/gpu/drm/apple/apple_drv.c b/drivers/gpu/drm/apple/apple_drv.c index 33074e588dffb..d8f2fb1ed300d 100644 --- a/drivers/gpu/drm/apple/apple_drv.c +++ b/drivers/gpu/drm/apple/apple_drv.c @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -657,8 +658,13 @@ static int apple_probe_per_dcp(struct device *dev, drm_crtc_helper_add(&crtc->base, &apple_crtc_helper_funcs); /* T8132 has no qualified CTM transaction. */ - if (!iomfb_is_m4(dcp_data)) - drm_crtc_enable_color_mgmt(&crtc->base, 0, true, 0); + if (!iomfb_is_m4(dcp_data)) { + unsigned int gamma_size = dcp_gamma_lut_size(dcp); + + drm_crtc_enable_color_mgmt(&crtc->base, 0, true, gamma_size); + if (gamma_size) + drm_mode_crtc_set_gamma_size(&crtc->base, gamma_size); + } crtc->dcp = dcp; *crtc_mask = drm_crtc_mask(&crtc->base); diff --git a/drivers/gpu/drm/apple/av.c b/drivers/gpu/drm/apple/av.c index 8a19f2a295bbc..e352aaf9390d9 100644 --- a/drivers/gpu/drm/apple/av.c +++ b/drivers/gpu/drm/apple/av.c @@ -419,6 +419,7 @@ int avep_init(struct apple_dcp *dcp) break; case DCP_FIRMWARE_V_13_5: case DCP_FIRMWARE_V_14_7_TEMPLATE: + case DCP_FIRMWARE_V_26_6_DESKTOP: audiosrv_data->cmds = dcp_av_audio_cmds_v13_5; break; default: diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index 224367cbb5006..0d32888ec371d 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -4,6 +4,7 @@ #ifndef __APPLE_DCP_INTERNAL_H__ #define __APPLE_DCP_INTERNAL_H__ +#include #include #include #include @@ -33,6 +34,7 @@ #include "iomfb_v14_7.h" #include "iomfb_v14_7_tpl.h" #include "iomfb_v26_6.h" +#include "iomfb_v26_6_desktop.h" #include "epic/dpavservep.h" #include "version_utils.h" @@ -164,6 +166,8 @@ struct dcp_brightness { int nits; int scale; bool update; + /* A level was written through the backlight device (desktop 26.x). */ + bool requested; }; struct audiosrv_data; @@ -214,6 +218,30 @@ enum dcp_test_point { DCP_TEST_HOTPLUG_EVENT, /* the hotplug worker sends its event */ }; +/* + * DCP_FIRMWARE_V_26_6_DESKTOP state, see iomfb_v26_6_desktop.c; allocated + * for such a DCP only. + */ +struct dcp_desktop_state { + /* Queued swap; too large for the shared swap union. */ + struct dcp_swap_submit_req_v26_6_desktop swap; + /* Opaque client identity the firmware's IOMFB calls carry. */ + u64 client; + /* + * A mode was set since the display last went away. Until then the + * firmware's generic pipe is bootstrapped through a virtual color + * element before the real one is selected. + */ + bool mode_live; + /* + * GAMMA_LUT: the firmware's own table, read before the first change; + * the table being sent; the LUT it is composed from, if any. + */ + struct iomfb_gamma_table_req *gamma_fw, *gamma_out; + struct drm_color_lut gamma_lut[IOMFB_GAMMA_ENTRIES]; + bool gamma_fw_read, gamma_user, gamma_pending, gamma_unusable; +}; + /* TODO: move IOMFB members to its own struct */ struct dcpext_scanout; struct dcp_hdmi; @@ -638,6 +666,9 @@ struct apple_dcp { struct delayed_work hdmi_recheck_wq; /* T6030 HDMI converter service requests, see dcp-hdmi.c */ struct dcp_hdmi *hdmi; + + /* Kept last: the template instances share the layout above. */ + struct dcp_desktop_state *desktop; }; /* T8132 firmware uses its own wire codec, with the common DRM/KMS owner. */ diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 1078e5dff3345..2557bda0cafaa 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -35,6 +36,7 @@ #include #include +#include #include #include #include @@ -1001,6 +1003,11 @@ int dcp_crtc_atomic_check(struct drm_crtc *crtc, struct drm_atomic_commit *commi crtc_state->vrr_enabled) return -EINVAL; + /* DRM core only caps the blob size; the gamma update copies a full table. */ + if (crtc_state->color_mgmt_changed && crtc_state->gamma_lut && + drm_color_lut_size(crtc_state->gamma_lut) != dcp_gamma_lut_size(pdev)) + return -EINVAL; + /* * A Type-C display routed to another pipeline: its route follows * this CRTC if it can (see dcp_typec_follow_check()), and the mode is @@ -2481,6 +2488,15 @@ bool dcp_fw_compat_is_14_7(struct platform_device *pdev) dcp->fw_compat == DCP_FIRMWARE_V_26_6; } +/* GAMMA_LUT entries, 0 where the driver cannot program the gamma table */ +unsigned int dcp_gamma_lut_size(struct platform_device *pdev) +{ + struct apple_dcp *dcp = platform_get_drvdata(pdev); + + return dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP ? + IOMFB_GAMMA_ENTRIES : 0; +} + unsigned long* dcp_get_iomfb_surfaces(struct platform_device *pdev) { struct apple_dcp *dcp = platform_get_drvdata(pdev); @@ -2627,6 +2643,9 @@ static void _dcp_poweroff(struct apple_dcp *dcp) case DCP_FIRMWARE_V_14_7_TEMPLATE: iomfb_poweroff_v14_7_0(dcp); break; + case DCP_FIRMWARE_V_26_6_DESKTOP: + iomfb_poweroff_v26_6_desktop(dcp); + break; case DCP_FIRMWARE_V_26_6: iomfb_v26_6_poweroff(dcp); break; @@ -2721,6 +2740,9 @@ static void __maybe_unused dcp_sleep(struct apple_dcp *dcp) case DCP_FIRMWARE_V_14_7_TEMPLATE: iomfb_sleep_v14_7_0(dcp); break; + case DCP_FIRMWARE_V_26_6_DESKTOP: + iomfb_sleep_v26_6_desktop(dcp); + break; case DCP_FIRMWARE_V_26_6: iomfb_v26_6_poweroff(dcp); break; @@ -2878,6 +2900,9 @@ void dcp_poweron(struct platform_device *pdev) case DCP_FIRMWARE_V_14_7_TEMPLATE: iomfb_poweron_v14_7_0(dcp); break; + case DCP_FIRMWARE_V_26_6_DESKTOP: + iomfb_poweron_v26_6_desktop(dcp); + break; case DCP_FIRMWARE_V_26_6: iomfb_v26_6_poweron(dcp); break; @@ -4284,6 +4309,18 @@ static int dcp_platform_probe_pipe(struct platform_device *pdev) iomfb_queue_init(dcp); dcp->fw_compat = fw_compat; + if (fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) { + dcp->desktop = devm_kzalloc(dev, sizeof(*dcp->desktop), GFP_KERNEL); + if (!dcp->desktop) + return -ENOMEM; + /* + * The opaque client identity of the IOMFB calls. The firmware's + * own client passes a kernel pointer there; pass a random value + * of the same shape instead. + */ + dcp->desktop->client = GENMASK_ULL(63, 48) | + (get_random_u64() & GENMASK_ULL(47, 4)); + } dcp->ext_failsafe = of_device_is_compatible(dev->of_node, "apple,t8122-dcpext"); dcp->external = of_device_is_compatible(dev->of_node, "apple,t6030-dcpext"); dcp->external_native = dcp->external && dcp_t6030_ext_native(dev->of_node); diff --git a/drivers/gpu/drm/apple/dcp.h b/drivers/gpu/drm/apple/dcp.h index 33f39b7645c4e..f54bcb280cebc 100644 --- a/drivers/gpu/drm/apple/dcp.h +++ b/drivers/gpu/drm/apple/dcp.h @@ -63,6 +63,7 @@ int dcp_crtc_atomic_check(struct drm_crtc *crtc, struct drm_atomic_commit *commi int dcp_get_connector_type(struct platform_device *pdev); bool dcp_fw_compat_is_12_x(struct platform_device *pdev); bool dcp_fw_compat_is_14_7(struct platform_device *pdev); +unsigned int dcp_gamma_lut_size(struct platform_device *pdev); bool dcp_ext_unusable(struct platform_device *pdev); unsigned long* dcp_get_iomfb_surfaces(struct platform_device *pdev); int dcp_start(struct platform_device *pdev); diff --git a/drivers/gpu/drm/apple/dcp_backlight.c b/drivers/gpu/drm/apple/dcp_backlight.c index 6d56199a4fe8d..e9bc471654cde 100644 --- a/drivers/gpu/drm/apple/dcp_backlight.c +++ b/drivers/gpu/drm/apple/dcp_backlight.c @@ -22,6 +22,9 @@ #define MIN_BRIGHTNESS_PART2 103U #define MAX_BRIGHTNESS_PART2 510U +/* Initial level of a desktop 26.x panel, which reports none. */ +#define DCP_DESKTOP_DEFAULT_NITS 250U + /* * lookup for display brightness 2 to 99 nits * */ @@ -507,11 +510,24 @@ static int dcp_set_brightness(struct backlight_device *bd) return ret; } + /* + * Desktop 26.x swaps carry nits. A level of 0 must not turn the + * panel off (DPMS does that); only a blanked panel still gets 0. + */ + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && !brightness && + !bd->props.brightness) + brightness = min(bd->props.max_brightness, 2); + ret = drm_modeset_lock_single_interruptible(&dcp->crtc->base.mutex); if (ret) return ret; - dcp->brightness.dac = calculate_dac(dcp, brightness); + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) { + dcp->brightness.nits = brightness; + dcp->brightness.requested = true; + } else { + dcp->brightness.dac = calculate_dac(dcp, brightness); + } dcp->brightness.update = true; drm_modeset_unlock(&dcp->crtc->base.mutex); @@ -564,13 +580,28 @@ int dcp_backlight_register(struct apple_dcp *dcp) spin_unlock_irqrestore(&dcp->backlight.lock, flags); } + /* + * Desktop 26.x takes nits directly and publishes no initial level: + * start from a default, which is not sent until userspace writes one. + */ + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) { + if (!dcp->brightness.maximum || dcp->brightness.maximum > INT_MAX) + return -ERANGE; + props.max_brightness = dcp->brightness.maximum; + if (!props.brightness) + props.brightness = min_t(u32, DCP_DESKTOP_DEFAULT_NITS, + props.max_brightness); + dcp->brightness.nits = props.brightness; + } + bl_dev = devm_backlight_device_register(dev, "apple-panel-bl", dev, dcp, &dcp_backlight_ops, &props); if (IS_ERR(bl_dev)) return PTR_ERR(bl_dev); dcp->brightness.bl_dev = bl_dev; - if (!dcp_backlight_active(dcp)) + if (!dcp_backlight_active(dcp) && + dcp->fw_compat != DCP_FIRMWARE_V_26_6_DESKTOP) dcp->brightness.dac = calculate_dac(dcp, dcp->brightness.nits); return 0; diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index d463c85dd885f..86482c75f7e36 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -90,6 +90,17 @@ static inline u64 dcpep_ack(enum dcp_context_id id) return dcpep_msg(id, 0, 0) | IOMFB_MSG_ACK; } +/* + * Adaptive sync through IOMFBParameter 14 black-screens the J616s panel on + * the desktop 26.6 firmware: the firmware accepts the VRR mode, then no swap + * completes. Do not offer it there. + */ +static bool dcp_mode_vrr_supported(struct apple_dcp *dcp, + const struct dcp_display_mode *mode) +{ + return dcp->fw_compat != DCP_FIRMWARE_V_26_6_DESKTOP || !mode->vrr; +} + /* * A channel is busy if we have sent a message that has yet to be * acked. The driver must not sent a message to a busy channel. @@ -1934,6 +1945,8 @@ int dcp_get_modes(struct drm_connector *connector) continue; published_180hz |= modes[i].automatic_rgb8_quirk; published++; + if (!dcp_mode_vrr_supported(dcp, &modes[i])) + continue; if (modes[i].vrr) { u16 lo = modes[i].min_vrr >> 16; u16 hi = modes[i].max_vrr >> 16; @@ -2116,6 +2129,9 @@ int dcp_crtc_atomic_modeset(struct drm_crtc *crtc, case DCP_FIRMWARE_V_14_7_TEMPLATE: ret = iomfb_modeset_v14_7_0(dcp, crtc_state); break; + case DCP_FIRMWARE_V_26_6_DESKTOP: + ret = iomfb_modeset_v26_6_desktop(dcp, crtc_state); + break; case DCP_FIRMWARE_V_26_6: ret = iomfb_v26_6_modeset(dcp, crtc_state); break; @@ -2676,6 +2692,9 @@ void dcp_flush(struct drm_crtc *crtc, struct drm_atomic_commit *commit) case DCP_FIRMWARE_V_14_7_TEMPLATE: iomfb_flush_v14_7_0(dcp, crtc, commit); break; + case DCP_FIRMWARE_V_26_6_DESKTOP: + iomfb_flush_v26_6_desktop(dcp, crtc, commit); + break; case DCP_FIRMWARE_H17P: iomfb_flush_h17p(dcp, crtc, commit); break; @@ -2697,6 +2716,9 @@ static void iomfb_start(struct apple_dcp *dcp) case DCP_FIRMWARE_V_14_7_TEMPLATE: iomfb_start_v14_7_0(dcp); break; + case DCP_FIRMWARE_V_26_6_DESKTOP: + iomfb_start_v26_6_desktop(dcp); + break; case DCP_FIRMWARE_H17P: iomfb_start_h17p(dcp); break; @@ -2811,6 +2833,9 @@ void iomfb_shutdown(struct apple_dcp *dcp) case DCP_FIRMWARE_V_14_7_TEMPLATE: iomfb_shutdown_v14_7_0(dcp); break; + case DCP_FIRMWARE_V_26_6_DESKTOP: + iomfb_shutdown_v26_6_desktop(dcp); + break; case DCP_FIRMWARE_H17P: iomfb_shutdown_h17p(dcp); break; diff --git a/drivers/gpu/drm/apple/iomfb.h b/drivers/gpu/drm/apple/iomfb.h index 25c2e0254183f..7e6e16f1dddf7 100644 --- a/drivers/gpu/drm/apple/iomfb.h +++ b/drivers/gpu/drm/apple/iomfb.h @@ -406,4 +406,88 @@ struct dcpep_get_tiling_state_resp { u32 ret; } __packed; +/* + * Records of the desktop 26.6 IOMFB (T6041, DCP_FIRMWARE_V_26_6_DESKTOP) + * that differ from the shared ones above. + */ + +/* A364 get_mode_info: fills the mode record A413 then selects. */ +struct dcp_get_mode_info_req_v26_6 { + u8 mode[0x64]; + u8 mode_null; + u8 padding0[3]; + u64 opaque; + u32 timing_mode_id; + u32 color_mode_id; + u8 result_null; + u8 padding1[3]; +} __packed; + +struct dcp_get_mode_info_resp_v26_6 { + u8 opaque[0x74]; +} __packed; + +/* A469 abort_swaps_dcp takes the bare client identity. */ +struct iomfb_abort_swaps_dcp_req_v26_6 { + u64 client; +} __packed; + +struct iomfb_abort_swaps_dcp_resp_v26_6 { + u32 ret; +} __packed; + +/* D592 swap_complete_intent_gated, padded to 0x14. */ +struct dcp_swap_complete_intent_gated_v26_6 { + u32 swap_id; + u8 unkBool; + u32 unkInt; + u32 width; + u32 height; + u8 padding[3]; +} __packed; + +/* D125 read_edt_data reply, padded to 0x24. */ +struct dcp_read_edt_data_resp_v26_6 { + u32 value[8]; + u8 ret; + u8 padding[3]; +} __packed; + +/* + * D575 hotplug_gated: the connection state and a tiled-display record in, + * a 0x4c-byte record out. + */ +struct dcp_hotplug_req_v26_6 { + u64 connected; + u8 opaque[0x50]; +} __packed; + +struct dcp_hotplug_resp_v26_6 { + u8 opaque[0x4c]; +} __packed; + +/* A boolean argument, padded to four bytes. */ +struct dcp_bool_arg { + u8 value; + u8 padding[3]; +} __packed; + +/* + * A420 get_gamma_table / A421 set_gamma_table: three channels (red, green, + * blue) of 257 u32 entries followed by a null flag, 3088 bytes in and out. + */ +#define IOMFB_GAMMA_ENTRIES 257 + +struct iomfb_gamma_table_req { + __le32 table[3][IOMFB_GAMMA_ENTRIES]; + u8 table_null; + u8 padding[3]; +} __packed; + +struct iomfb_gamma_table_resp { + __le32 table[3][IOMFB_GAMMA_ENTRIES]; + u8 table_null; + u8 padding[3]; +} __packed; + #endif diff --git a/drivers/gpu/drm/apple/iomfb_template.c b/drivers/gpu/drm/apple/iomfb_template.c index fcb74e0b88dc1..da2c9eab540dc 100644 --- a/drivers/gpu/drm/apple/iomfb_template.c +++ b/drivers/gpu/drm/apple/iomfb_template.c @@ -20,6 +20,7 @@ #include #include +#include #include #include #include @@ -41,6 +42,166 @@ static_assert(offsetof(struct DCP_FW_NAME(dcp_swap), timestamp[6]) == 0x30); static_assert(offsetof(struct DCP_FW_NAME(dcp_swap), flags1) == 0x40); +/* The queued swap of this instance. */ +#if DCP_FW_DESKTOP_26(DCP_FW_VER) +#define DCP_FW_SWAP(dcp) ((dcp)->desktop->swap) +#else +#define DCP_FW_SWAP(dcp) DCP_FW_UNION((dcp)->swap) +#endif + +#if DCP_FW_DESKTOP_26(DCP_FW_VER) +enum dcp_power_transition { + DCP_POWER_TRANSITION_NONE, + DCP_POWER_TRANSITION_DISPLAY_OFF, + DCP_POWER_TRANSITION_RESTARTABLE_SLEEP, + DCP_POWER_TRANSITION_SHUTDOWN, +}; + +/* The records of this instance that differ from the shared ones. */ +#define dcp_bool_arg_t struct dcp_bool_arg +#define dcp_abort_swaps_req_t struct iomfb_abort_swaps_dcp_req_v26_6 +#define dcp_read_edt_data_resp_t struct dcp_read_edt_data_resp_v26_6 +#define dcp_swap_intent_t struct dcp_swap_complete_intent_gated_v26_6 + +/* + * set_power_state (A473) carries four independent transition flags. The + * firmware's own sleep sets the first and the last, its shutdown the second; + * both then pass no result. + */ +static struct DCP_FW_NAME(dcp_set_power_state_req) +dcp_power_state_req(u64 state, enum dcp_power_transition transition) +{ + struct DCP_FW_NAME(dcp_set_power_state_req) req = { + .unklong = state, + .padding = { 0xaa, 0xaa, 0xaa }, + }; + + switch (transition) { + case DCP_POWER_TRANSITION_RESTARTABLE_SLEEP: + req.flags[0] = 1; + req.flags[3] = 1; + req.result_null = 1; + break; + case DCP_POWER_TRANSITION_SHUTDOWN: + req.flags[1] = 1; + req.result_null = 1; + break; + case DCP_POWER_TRANSITION_NONE: + case DCP_POWER_TRANSITION_DISPLAY_OFF: + break; + } + + return req; +} + +static dcp_bool_arg_t dcp_bool_arg(bool value) +{ + return (dcp_bool_arg_t) { + .value = value, + .padding = { 0xaa, 0xaa, 0xaa }, + }; +} + +/* + * Brightness and display-off swaps carry the level in nits, as a binary64. + * Every u32 is exactly representable, so no kernel floating point is needed. + */ +static void dcp_swap_set_brightness(struct DCP_FW_NAME(dcp_swap) *swap, u32 nits) +{ + u64 bits = 0; + + if (nits) { + unsigned int exponent = fls(nits) - 1; + + bits = (u64)(exponent + 1023) << 52; + bits |= ((u64)nits << (52 - exponent)) & GENMASK_ULL(51, 0); + } + + swap->bl_update = 1; + swap->bl_flags[1] = 1; + put_unaligned_le64(bits, &swap->bl_nits); +} + +/* + * The firmware's own client passes its IOUserClient pointer as an opaque + * client identity; this one passes a cookie chosen at probe. + */ +static struct DCP_FW_NAME(dcp_swap_start_req) +dcp_swap_start_request(struct apple_dcp *dcp) +{ + return (struct DCP_FW_NAME(dcp_swap_start_req)) { + .client = dcp->desktop->client, + .padding = { 0xaa, 0xaa, 0xaa }, + }; +} + +static dcp_abort_swaps_req_t dcp_abort_swaps_request(struct apple_dcp *dcp) +{ + return (dcp_abort_swaps_req_t) { .client = dcp->desktop->client }; +} + +/* + * A414 and A474, with these arguments, bracket the display power changes. + * The request is five zero bytes followed by three bytes of 0xaa. + */ +struct dcp_wake_state_req_v26_6 { + u8 data[8]; +} __packed; + +static const struct dcp_wake_state_req_v26_6 dcp_wake_state_req = { + .data = { 0, 0, 0, 0, 0, 0xaa, 0xaa, 0xaa }, +}; + +/* Methods without a slot in the shared method table. */ +static const struct dcp_method_entry dcp_get_mode_info_method = { + .name = "dcpep_get_mode_info", + .tag = { 'A', '3', '6', '4' }, +}; + +static const struct dcp_method_entry dcp_get_gamma_table_method = { + .name = "iomfbep_get_gamma_table", + .tag = { 'A', '4', '2', '0' }, +}; + +static const struct dcp_method_entry dcp_set_gamma_table_method = { + .name = "iomfbep_set_gamma_table", + .tag = { 'A', '4', '2', '1' }, +}; + +static_assert(sizeof(struct DCP_FW_NAME(dcp_swap)) == 0x588); +static_assert(offsetof(struct DCP_FW_NAME(dcp_swap), swap_id) == 0x98); +static_assert(offsetof(struct DCP_FW_NAME(dcp_swap), bl_update) == 0x354); +static_assert(offsetof(struct DCP_FW_NAME(dcp_swap), bl_flags) == 0x358); +static_assert(offsetof(struct DCP_FW_NAME(dcp_swap), bl_nits) == 0x35e); +static_assert(offsetof(struct DCP_FW_NAME(dcp_swap), presents_surface) == 0x51e); +static_assert(offsetof(struct DCP_FW_NAME(dcp_swap), has_completion) == 0x570); +static_assert(sizeof(struct DCP_FW_NAME(dcp_surface)) == 0x22c); +static_assert(sizeof(struct DCP_FW_NAME(dcp_swap_submit_req)) == 0x1bd8); +static_assert(sizeof(struct DCP_FW_NAME(dcp_swap_submit_resp)) == 0xc); +static_assert(sizeof(struct DCP_FW_NAME(dc_swap_complete_resp)) == 0x730); +static_assert(sizeof(struct DCP_FW_NAME(dcp_map_reg_req)) == 0x10); +static_assert(sizeof(struct dcp_set_parameter_dcp) == 0x28); +static_assert(sizeof(struct dcp_get_mode_info_req_v26_6) == 0x7c); +static_assert(sizeof(struct dcp_get_mode_info_resp_v26_6) == 0x74); +static_assert(sizeof(struct iomfb_abort_swaps_dcp_req_v26_6) == 0x8); +static_assert(sizeof(struct iomfb_abort_swaps_dcp_resp_v26_6) == 0x4); +static_assert(sizeof(struct dcp_swap_complete_intent_gated_v26_6) == 0x14); +static_assert(sizeof(struct dcp_read_edt_data_resp_v26_6) == 0x24); +static_assert(sizeof(struct dcp_hotplug_req_v26_6) == 0x58); +static_assert(sizeof(struct dcp_hotplug_resp_v26_6) == 0x4c); +static_assert(sizeof(struct dcp_bool_arg) == 0x4); +static_assert(sizeof(struct dcp_wake_state_req_v26_6) == 0x8); +static_assert(sizeof(struct iomfb_gamma_table_req) == 3088); +static_assert(sizeof(struct iomfb_gamma_table_resp) == 3088); +static_assert(sizeof(struct dcp_set_frame_sync_props_req_v26_6_desktop) == 0x54); +static_assert(sizeof(struct dcp_set_frame_sync_props_resp_v26_6_desktop) == 0x50); +#else +#define dcp_bool_arg_t u32 +#define dcp_abort_swaps_req_t struct iomfb_abort_swaps_dcp_req +#define dcp_read_edt_data_resp_t struct dcp_read_edt_data_resp +#define dcp_swap_intent_t struct dcp_swap_complete_intent_gated +#endif + struct dcp_wait_cookie { struct kref refcount; struct completion done; @@ -63,7 +224,17 @@ DCP_THUNK_OUT(iomfb_a358_vi_set_temperature_hint, iomfbep_a358_vi_set_temperatur IOMFB_THUNK_INOUT(set_matrix); IOMFB_THUNK_INOUT(get_color_remap_mode); IOMFB_THUNK_INOUT(last_client_close); +#if DCP_FW_DESKTOP_26(DCP_FW_VER) +DCP_THUNK_INOUT(iomfb_abort_swaps_dcp, iomfbep_abort_swaps_dcp, + struct iomfb_abort_swaps_dcp_req_v26_6, + struct iomfb_abort_swaps_dcp_resp_v26_6); +/* The A414/A474 slots of the shared table, see dcp_wake_state_req. */ +DCP_THUNK_INOUT(dcp_prepare_wake, dcpep_pipe_cfg_414, + struct dcp_wake_state_req_v26_6, u64); +DCP_THUNK_OUT(dcp_finish_wake, dcpep_pipe_query_474, u32); +#else IOMFB_THUNK_INOUT(abort_swaps_dcp); +#endif #if DCP_FW_H17(DCP_FW_VER) static void dcp_swap_submit(struct apple_dcp *dcp, bool oob, @@ -144,11 +315,13 @@ DCP_THUNK_OUT(dcp_set_display_refresh_properties, dcpep_set_display_refresh_properties, u32); #if DCP_FW_VER >= DCP_FW_VERSION(13, 2, 0) -DCP_THUNK_INOUT(dcp_late_init_signal, dcpep_late_init_signal, u32, u32); +DCP_THUNK_INOUT(dcp_late_init_signal, dcpep_late_init_signal, dcp_bool_arg_t, + u32); #else DCP_THUNK_OUT(dcp_late_init_signal, dcpep_late_init_signal, u32); #endif -DCP_THUNK_IN(dcp_flush_supports_power, dcpep_flush_supports_power, u32); +DCP_THUNK_IN(dcp_flush_supports_power, dcpep_flush_supports_power, + dcp_bool_arg_t); DCP_THUNK_OUT(dcp_create_default_fb, dcpep_create_default_fb, u32); DCP_THUNK_OUT(dcp_start_signal, dcpep_start_signal, u32); DCP_THUNK_VOID(dcp_setup_video_limits, dcpep_setup_video_limits); @@ -470,6 +643,8 @@ static bool iomfbep_cb_match_backlight_service(struct apple_dcp *dcp, int tag, v static void iomfb_cb_pr_publish(struct apple_dcp *dcp, struct iomfb_property *prop) { switch (prop->id) { +#if !DCP_FW_DESKTOP_26(DCP_FW_VER) + /* The desktop 26.x firmware publishes no panel level here. */ case IOMFB_PROPERTY_NITS_V13_3: /* * 13.x firmware publishes another property under the 14.7 ID, with @@ -494,6 +669,7 @@ static void iomfb_cb_pr_publish(struct apple_dcp *dcp, struct iomfb_property *pr } break; } +#endif default: dev_dbg(dcp->dev, "pr_publish: id: %d = %u\n", prop->id, prop->value); } @@ -585,6 +761,12 @@ static bool dcpep_map_piodma(struct apple_dcp *dcp, .buffer = req->buffer, .dva = memdesc->dva, }; +#elif DCP_FW_DESKTOP_26(DCP_FW_VER) + /* the desktop 26.x reply echoes the buffer index too */ + *resp = (struct DCP_FW_NAME(dcp_map_buf_resp)) { + .buffer = req->buffer, + .dva = memdesc->dva, + }; #else *resp = (struct DCP_FW_NAME(dcp_map_buf_resp)) { .dva = memdesc->dva }; #endif @@ -606,6 +788,9 @@ static struct DCP_FW_NAME(dcp_map_buf_resp) dcpep_cb_map_piodma(struct apple_dcp #if DCP_FW_H17(DCP_FW_VER) resp.buffer = req->buffer; +#elif DCP_FW_DESKTOP_26(DCP_FW_VER) + resp.buffer = req->buffer; + resp.ret = EINVAL; #else resp.ret = EINVAL; #endif @@ -860,7 +1045,8 @@ static u64 dcpep_cb_get_frequency(struct apple_dcp *dcp) return clk_get_rate(dcp->clk); } -#if DCP_FW_VER >= DCP_FW_VERSION(13, 2, 0) && !DCP_FW_H17(DCP_FW_VER) +#if DCP_FW_VER >= DCP_FW_VERSION(13, 2, 0) && !DCP_FW_H17(DCP_FW_VER) && \ + !DCP_FW_DESKTOP_26(DCP_FW_VER) /* The DVA that map_reg hands out for display register range @index. */ static dma_addr_t dcpep_map_reg_dva(struct apple_dcp *dcp, u32 index, struct resource *rsrc) @@ -906,6 +1092,14 @@ static struct DCP_FW_NAME(dcp_map_reg_resp) dcpep_cb_map_reg(struct apple_dcp *d * DMA_MAPPING_ERROR as a device address. */ dma_addr_t dva = rsrc->start; +#elif DCP_FW_DESKTOP_26(DCP_FW_VER) + /* + * The desktop 26.x firmware takes the register windows' + * physical addresses as well. It reaches them on its + * DART-bypass stream; a mapping in its DMA stream would not + * affect those accesses. + */ + dma_addr_t dva = rsrc->start; #elif DCP_FW_VER >= DCP_FW_VERSION(13, 2, 0) dma_addr_t dva = dcpep_map_reg_dva(dcp, req->index, rsrc); WARN_ON(dva == DMA_MAPPING_ERROR); @@ -921,7 +1115,7 @@ static struct DCP_FW_NAME(dcp_map_reg_resp) dcpep_cb_map_reg(struct apple_dcp *d } } -static struct dcp_read_edt_data_resp +static dcp_read_edt_data_resp_t dcpep_cb_read_edt_data(struct apple_dcp *dcp, struct dcp_read_edt_data_req *req) { /* Observe boot-property requests without inventing firmware timings. */ @@ -931,7 +1125,7 @@ dcpep_cb_read_edt_data(struct apple_dcp *dcp, struct dcp_read_edt_data_req *req) (int)sizeof(req->key), req->key, req->count, req->value[0]); - return (struct dcp_read_edt_data_resp){ + return (dcp_read_edt_data_resp_t){ .value[0] = req->value[0], .ret = 0, }; @@ -1181,6 +1375,8 @@ static void boot_4b(struct apple_dcp *dcp, void *out, void *cookie) #if DCP_FW_VER >= DCP_FW_VERSION(13, 2, 0) #if DCP_FW_H17(DCP_FW_VER) u32 value = dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17G; +#elif DCP_FW_DESKTOP_26(DCP_FW_VER) + dcp_bool_arg_t value = dcp_bool_arg(true); #else u32 value = 1; #endif @@ -1202,7 +1398,11 @@ static void boot_4(struct apple_dcp *dcp, void *out, void *cookie) static void boot_3(struct apple_dcp *dcp, void *out, void *cookie) { +#if DCP_FW_DESKTOP_26(DCP_FW_VER) + dcp_bool_arg_t v_true = dcp_bool_arg(true); +#else u32 v_true = true; +#endif dcp_flush_supports_power(dcp, false, &v_true, boot_4, NULL); } @@ -1396,12 +1596,26 @@ dcpep_cb_set_frame_sync_props_h17p(struct apple_dcp *dcp, } #endif +#if DCP_FW_DESKTOP_26(DCP_FW_VER) +/* D006: echo the properties with the flag bytes 73..75 cleared. */ +static struct dcp_set_frame_sync_props_resp_v26_6_desktop +dcpep_cb_set_frame_sync_props(struct apple_dcp *dcp, + struct dcp_set_frame_sync_props_req_v26_6_desktop *req) +{ + struct dcp_set_frame_sync_props_resp_v26_6_desktop resp; + + memcpy(resp.props, req->props, sizeof(resp.props)); + memset(&resp.props[73], 0, 3); + return resp; +} +#else static struct dcp_set_frame_sync_props_resp dcpep_cb_set_frame_sync_props(struct apple_dcp *dcp, struct dcp_set_frame_sync_props_req *req) { return (struct dcp_set_frame_sync_props_resp){}; } +#endif /* Callback to get the current time as milliseconds since the UNIX epoch */ static u64 dcpep_cb_get_time(struct apple_dcp *dcp) @@ -1440,7 +1654,7 @@ static void release_swap_cookie(struct kref *ref) */ static void dcp_h17p_prepare_swap(struct apple_dcp *dcp) { - struct DCP_FW_NAME(dcp_swap_submit_req) *req = &DCP_FW_UNION(dcp->swap); + struct DCP_FW_NAME(dcp_swap_submit_req) *req = &DCP_FW_SWAP(dcp); unsigned int i; /* @@ -1551,7 +1765,7 @@ static int dcp_present_submit(struct apple_dcp *dcp, u32 swap_id, bool accepted) static void dcp_prepare_clear_swap(struct apple_dcp *dcp, void *request) { - typeof(DCP_FW_UNION(dcp->swap)) *swap = request; + typeof(DCP_FW_SWAP(dcp)) *swap = request; /* Clear surfaces. */ memset(swap, 0, sizeof(*swap)); @@ -1568,6 +1782,9 @@ static void dcp_prepare_clear_swap(struct apple_dcp *dcp, void *request) * brightness. */ if (dcp_has_panel(dcp)) { +#if DCP_FW_DESKTOP_26(DCP_FW_VER) + dcp_swap_set_brightness(&swap->swap, 0); +#else #if !DCP_FW_LAYOUT_14_7(DCP_FW_VER) swap->swap.bl_unk = 1; #else @@ -1575,6 +1792,7 @@ static void dcp_prepare_clear_swap(struct apple_dcp *dcp, void *request) #endif swap->swap.bl_value = 0; swap->swap.bl_power = 0; +#endif } /* Null all surfaces */ @@ -1582,7 +1800,7 @@ static void dcp_prepare_clear_swap(struct apple_dcp *dcp, void *request) swap->surf_null[l] = true; #if DCP_FW_VERSION(13, 2, 0) <= DCP_FW_VER #if !DCP_FW_H17(DCP_FW_VER) - for (int l = 0; l < 5; l++) + for (int l = 0; l < ARRAY_SIZE(swap->surf2_null); l++) swap->surf2_null[l] = true; #endif swap->unknown_pointer_null = true; @@ -1594,7 +1812,7 @@ static void dcp_swap_cleared(struct apple_dcp *dcp, void *data, void *cookie) { struct DCP_FW_NAME(dcp_swap_submit_resp) *resp = data; struct dcp_swap_cookie *info = cookie; - u32 swap_id = DCP_FW_UNION(dcp->swap).swap.swap_id; + u32 swap_id = DCP_FW_SWAP(dcp).swap.swap_id; u32 status = resp ? resp->ret : ~0U; int ret; @@ -1645,9 +1863,9 @@ static void dcp_swap_clear_started(struct apple_dcp *dcp, void *data, } #if DCP_FW_H17(DCP_FW_VER) if (dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17G) - dcp_prepare_clear_swap(dcp, &DCP_FW_UNION(dcp->swap)); + dcp_prepare_clear_swap(dcp, &DCP_FW_SWAP(dcp)); #endif - DCP_FW_UNION(dcp->swap).swap.swap_id = resp->swap_id; + DCP_FW_SWAP(dcp).swap.swap_id = resp->swap_id; if (dcp_present_retires(dcp)) dcp_arm_retained_framebuffers(dcp, resp->swap_id); @@ -1655,7 +1873,7 @@ static void dcp_swap_clear_started(struct apple_dcp *dcp, void *data, info->swap_id = resp->swap_id; dcp_h17p_prepare_swap(dcp); - dcp_swap_submit(dcp, false, &DCP_FW_UNION(dcp->swap), dcp_swap_cleared, cookie); + dcp_swap_submit(dcp, false, &DCP_FW_SWAP(dcp), dcp_swap_cleared, cookie); } static void dcp_on_final(struct apple_dcp *dcp, void *out, void *cookie) @@ -1674,6 +1892,61 @@ static void dcp_on_final(struct apple_dcp *dcp, void *out, void *cookie) } } +#if DCP_FW_DESKTOP_26(DCP_FW_VER) +static void dcp_on_set_parameter(struct apple_dcp *dcp, void *out, void *cookie); + +/* After set_power_state(1): select the display, then adaptive sync off. */ +static void dcp_on_set_power_state(struct apple_dcp *dcp, void *out, void *cookie) +{ + u32 handle = dcp->main_display ? 0 : 2; + + dcp_set_display_device(dcp, false, &handle, + dcp->main_display ? dcp_on_final : + dcp_on_set_parameter, + cookie); +} + +static void dcp_on_set_parameter(struct apple_dcp *dcp, void *out, void *cookie) +{ + struct dcp_set_parameter_dcp param; + + /* minRR, mediaTargetRate and fractional rate, as three u64 */ + memset(¶m, 0xaa, sizeof(param)); + param.param = IOMFBPARAM_ADAPTIVE_SYNC; + memset(param.value, 0, 3 * sizeof(u64)); + param.count = 3; + + dcp_set_parameter_dcp(dcp, false, ¶m, dcp_on_final, cookie); +} + +static void dcp_poweron_after_main_display(struct apple_dcp *dcp, void *out, + void *cookie) +{ + struct DCP_FW_NAME(dcp_set_power_state_req) req = + dcp_power_state_req(1, DCP_POWER_TRANSITION_NONE); + + dcp_set_power_state(dcp, DCP_POWER_OOB, &req, dcp_on_set_power_state, + cookie); +} + +static void dcp_poweron_after_finish_wake(struct apple_dcp *dcp, void *out, + void *cookie) +{ + dcp_is_main_display(dcp, false, dcp_poweron_after_main_display, cookie); +} + +static void dcp_poweron_after_first_client(struct apple_dcp *dcp, void *out, + void *cookie) +{ + dcp_finish_wake(dcp, false, dcp_poweron_after_finish_wake, cookie); +} + +static void dcp_poweron_after_prepare_wake(struct apple_dcp *dcp, void *out, + void *cookie) +{ + dcp_first_client_open(dcp, false, dcp_poweron_after_first_client, cookie); +} +#else static void dcp_on_set_power_state(struct apple_dcp *dcp, void *out, void *cookie) { struct DCP_FW_NAME(dcp_set_power_state_req) req = { @@ -1697,12 +1970,15 @@ static void dcp_on_set_parameter(struct apple_dcp *dcp, void *out, void *cookie) dcp_set_parameter_dcp(dcp, false, ¶m, dcp_on_set_power_state, cookie); } +#endif void DCP_FW_NAME(iomfb_poweron)(struct apple_dcp *dcp) { struct dcp_wait_cookie *cookie; int ret; +#if !DCP_FW_DESKTOP_26(DCP_FW_VER) u32 handle; +#endif dev_info(dcp->dev, "dcp_poweron() starting\n"); #if DCP_FW_H17(DCP_FW_VER) @@ -1720,6 +1996,20 @@ void DCP_FW_NAME(iomfb_poweron)(struct apple_dcp *dcp) /* increase refcount to ensure the receiver has a reference */ kref_get(&cookie->refcount); +#if DCP_FW_DESKTOP_26(DCP_FW_VER) + /* + * The firmware's own wake order: A414, first_client_open, A474, + * is_main_display, set_power_state(1), then select the display. + * Without A414/A474, or with the display selected before A473, the + * controller stays powered off and every swap is swallowed. + */ + { + struct dcp_wake_state_req_v26_6 req = dcp_wake_state_req; + + dcp_prepare_wake(dcp, false, &req, dcp_poweron_after_prepare_wake, + cookie); + } +#else if (dcp->main_display) { #if DCP_FW_H17(DCP_FW_VER) /* @@ -1739,6 +2029,7 @@ void DCP_FW_NAME(iomfb_poweron)(struct apple_dcp *dcp) dcp_set_display_device(dcp, false, &handle, dcp_on_set_parameter, cookie); } +#endif ret = wait_for_completion_timeout(&cookie->done, msecs_to_jiffies(10000)); if (ret == 0) { @@ -1784,6 +2075,23 @@ static bool dcp_power_stop_confirmed(struct dcp_wait_cookie *cookie) #endif } +#if DCP_FW_DESKTOP_26(DCP_FW_VER) +static void dcp_poweroff_after_main_display(struct apple_dcp *dcp, void *out, + void *cookie) +{ + struct DCP_FW_NAME(dcp_set_power_state_req) power_req = + dcp_power_state_req(0, DCP_POWER_TRANSITION_DISPLAY_OFF); + + dcp_set_power_state(dcp, DCP_POWER_OOB, &power_req, + complete_set_powerstate, cookie); +} + +static void dcp_poweroff_after_prepare_state(struct apple_dcp *dcp, void *out, + void *cookie) +{ + dcp_is_main_display(dcp, false, dcp_poweroff_after_main_display, cookie); +} +#else static void last_client_closed_poff(struct apple_dcp *dcp, void *out, void *cookie) { struct DCP_FW_NAME(dcp_set_power_state_req) power_req = { @@ -1804,19 +2112,25 @@ static void aborted_swaps_dcp_poff(struct apple_dcp *dcp, void *out, void *cooki last_client_closed_poff, cookie); #endif } +#endif void DCP_FW_NAME(iomfb_poweroff)(struct apple_dcp *dcp) { int ret, swap_id; - struct iomfb_abort_swaps_dcp_req abort_req = { +#if DCP_FW_DESKTOP_26(DCP_FW_VER) + struct DCP_FW_NAME(dcp_swap_start_req) swap_req = + dcp_swap_start_request(dcp); +#else + dcp_abort_swaps_req_t abort_req = { .client = { .flag2 = 1, }, }; + struct DCP_FW_NAME(dcp_swap_start_req) swap_req = { 0 }; +#endif struct dcp_swap_cookie *cookie; struct dcp_wait_cookie *poff_cookie; - struct DCP_FW_NAME(dcp_swap_start_req) swap_req = { 0 }; - typeof(DCP_FW_UNION(dcp->swap)) *swap = &DCP_FW_UNION(dcp->swap); + typeof(DCP_FW_SWAP(dcp)) *swap = &DCP_FW_SWAP(dcp); #if DCP_FW_H17(DCP_FW_VER) /* H17P has no known safe pipe-off or surface-free transition. */ @@ -1869,6 +2183,20 @@ void DCP_FW_NAME(iomfb_poweroff)(struct apple_dcp *dcp) /* increase refcount to ensure the receiver has a reference */ kref_get(&poff_cookie->refcount); +#if DCP_FW_DESKTOP_26(DCP_FW_VER) + /* + * The firmware's own display power-off goes from the clear swap + * through A414, is_main_display and set_power_state(0). It sends + * abort_swaps and last_client_close only for sleep and shutdown; + * here they could tear down state the link power-off still needs. + */ + { + struct dcp_wake_state_req_v26_6 req = dcp_wake_state_req; + + dcp_prepare_wake(dcp, false, &req, dcp_poweroff_after_prepare_state, + poff_cookie); + } +#else if (DCP_HAS_CLIENT_TEARDOWN) { iomfb_abort_swaps_dcp(dcp, false, &abort_req, aborted_swaps_dcp_poff, poff_cookie); @@ -1877,6 +2205,7 @@ void DCP_FW_NAME(iomfb_poweroff)(struct apple_dcp *dcp) "power-off: skipping abort_swaps/last_client_close\n"); last_client_closed_poff(dcp, NULL, poff_cookie); } +#endif ret = wait_for_completion_timeout(&poff_cookie->done, msecs_to_jiffies(1000)); @@ -1899,9 +2228,14 @@ void DCP_FW_NAME(iomfb_poweroff)(struct apple_dcp *dcp) static void last_client_closed_sleep(struct apple_dcp *dcp, void *out, void *cookie) { +#if DCP_FW_DESKTOP_26(DCP_FW_VER) + struct DCP_FW_NAME(dcp_set_power_state_req) power_req = + dcp_power_state_req(0, DCP_POWER_TRANSITION_RESTARTABLE_SLEEP); +#else struct DCP_FW_NAME(dcp_set_power_state_req) power_req = { .unklong = 0, }; +#endif dcp_set_power_state(dcp, DCP_POWER_OOB, &power_req, complete_set_powerstate, cookie); } @@ -1914,11 +2248,16 @@ static void aborted_swaps_dcp_sleep(struct apple_dcp *dcp, void *out, void *cook void DCP_FW_NAME(iomfb_sleep)(struct apple_dcp *dcp) { int ret; - struct iomfb_abort_swaps_dcp_req req = { +#if DCP_FW_DESKTOP_26(DCP_FW_VER) + /* abort_swaps, last_client_close, set_power_state(0) as for sleep */ + dcp_abort_swaps_req_t req = dcp_abort_swaps_request(dcp); +#else + dcp_abort_swaps_req_t req = { .client = { .flag2 = 1, }, }; +#endif struct dcp_wait_cookie *cookie; @@ -2031,9 +2370,37 @@ static void dcpep_cb_hotplug(struct apple_dcp *dcp, u64 *connected) dcp_handle_hotplug_actions(dcp, action); } +#if DCP_FW_DESKTOP_26(DCP_FW_VER) +/* + * D575 carries a tiled-display record next to the connection state and + * wants a 0x4c-byte record back. Every captured connected reply has an + * unaligned 0xffffffff at 71..74; byte 75 is padding. The disconnected + * reply holds more state, which is not decoded. + */ +static struct dcp_hotplug_resp_v26_6 +dcpep_cb_hotplug_v26_6(struct apple_dcp *dcp, struct dcp_hotplug_req_v26_6 *req) +{ + struct dcp_hotplug_resp_v26_6 resp = { 0 }; + u64 connected = req->connected; + + /* A display that went away is bootstrapped again on its next mode. */ + if (!dcp->main_display && + (!connected || (dcp->connector && + READ_ONCE(dcp->connector->connected) != !!connected))) + dcp->desktop->mode_live = false; + + dcpep_cb_hotplug(dcp, &connected); + + if (connected) + memset(&resp.opaque[0x47], 0xff, sizeof(u32)); + + return resp; +} +#endif + static void dcpep_cb_swap_complete_intent_gated(struct apple_dcp *dcp, - struct dcp_swap_complete_intent_gated *info) + dcp_swap_intent_t *info) { trace_iomfb_swap_complete_intent_gated(dcp, info->swap_id, info->width, info->height); @@ -2132,7 +2499,7 @@ TRAMPOLINE_INOUT(trampoline_map_reg, dcpep_cb_map_reg, struct DCP_FW_NAME(dcp_map_reg_req), struct DCP_FW_NAME(dcp_map_reg_resp)); TRAMPOLINE_INOUT(trampoline_read_edt_data, dcpep_cb_read_edt_data, - struct dcp_read_edt_data_req, struct dcp_read_edt_data_resp); + struct dcp_read_edt_data_req, dcp_read_edt_data_resp_t); TRAMPOLINE_INOUT(trampoline_prop_start, dcpep_cb_prop_start, u32, u8); TRAMPOLINE_INOUT(trampoline_prop_chunk, dcpep_cb_prop_chunk, struct dcp_set_dcpav_prop_chunk_req, u8); @@ -2142,9 +2509,15 @@ TRAMPOLINE_INOUT(trampoline_allocate_bandwidth, dcpep_cb_allocate_bandwidth, struct dcp_allocate_bandwidth_req, struct dcp_allocate_bandwidth_resp); TRAMPOLINE_OUT(trampoline_rt_bandwidth, dcpep_cb_rt_bandwidth, struct dcp_rt_bandwidth); +#if DCP_FW_DESKTOP_26(DCP_FW_VER) +TRAMPOLINE_INOUT(trampoline_set_frame_sync_props, dcpep_cb_set_frame_sync_props, + struct dcp_set_frame_sync_props_req_v26_6_desktop, + struct dcp_set_frame_sync_props_resp_v26_6_desktop); +#else TRAMPOLINE_INOUT(trampoline_set_frame_sync_props, dcpep_cb_set_frame_sync_props, struct dcp_set_frame_sync_props_req, struct dcp_set_frame_sync_props_resp); +#endif #if DCP_FW_H17(DCP_FW_VER) TRAMPOLINE_INOUT(trampoline_set_frame_sync_props_h17p, dcpep_cb_set_frame_sync_props_h17p, @@ -2153,10 +2526,15 @@ TRAMPOLINE_INOUT(trampoline_set_frame_sync_props_h17p, #endif TRAMPOLINE_OUT(trampoline_get_frequency, dcpep_cb_get_frequency, u64); TRAMPOLINE_OUT(trampoline_get_time, dcpep_cb_get_time, u64); +#if DCP_FW_DESKTOP_26(DCP_FW_VER) +TRAMPOLINE_INOUT(trampoline_hotplug, dcpep_cb_hotplug_v26_6, + struct dcp_hotplug_req_v26_6, struct dcp_hotplug_resp_v26_6); +#else TRAMPOLINE_IN(trampoline_hotplug, dcpep_cb_hotplug, u64); +#endif TRAMPOLINE_IN(trampoline_swap_complete_intent_gated, dcpep_cb_swap_complete_intent_gated, - struct dcp_swap_complete_intent_gated); + dcp_swap_intent_t); TRAMPOLINE_IN(trampoline_abort_swap_ap_gated, dcpep_cb_abort_swap_ap_gated, u32); TRAMPOLINE_IN(trampoline_enable_backlight_message_ap_gated, iomfbep_cb_enable_backlight_message_ap_gated, u8); @@ -2175,7 +2553,7 @@ static void dcp_swapped(struct apple_dcp *dcp, void *data, void *cookie) { int ret; struct DCP_FW_NAME(dcp_swap_submit_resp) *resp = data; - u32 swap_id = DCP_FW_UNION(dcp->swap).swap.swap_id; + u32 swap_id = DCP_FW_SWAP(dcp).swap.swap_id; u32 status = resp ? resp->ret : ~0U; ret = dcp_present_submit(dcp, swap_id, !status); @@ -2213,20 +2591,30 @@ static void dcp_swap_started(struct apple_dcp *dcp, void *data, void *cookie) dcp_drm_crtc_vblank(dcp->crtc); return; } - DCP_FW_UNION(dcp->swap).swap.swap_id = resp->swap_id; + DCP_FW_SWAP(dcp).swap.swap_id = resp->swap_id; if (dcp_present_retires(dcp)) dcp_arm_retained_framebuffers(dcp, resp->swap_id); trace_iomfb_swap_submit(dcp, resp->swap_id); dcp_h17p_prepare_swap(dcp); - dcp_swap_submit(dcp, false, &DCP_FW_UNION(dcp->swap), dcp_swapped, NULL); + dcp_swap_submit(dcp, false, &DCP_FW_SWAP(dcp), dcp_swapped, NULL); } /* Helpers to modeset and swap, used to flush */ static void do_swap(struct apple_dcp *dcp, void *data, void *cookie) { +#if DCP_FW_DESKTOP_26(DCP_FW_VER) + struct DCP_FW_NAME(dcp_swap_start_req) start_req = + dcp_swap_start_request(dcp); + + if (READ_ONCE(dcp->mode_state.valid) && dcp->connector && + READ_ONCE(dcp->connector->connected)) + dcp_swap_start(dcp, false, &start_req, dcp_swap_started, NULL); + else + dcp_drm_crtc_vblank(dcp->crtc); +#else struct DCP_FW_NAME(dcp_swap_start_req) start_req = { 0 }; - struct DCP_FW_NAME(dcp_swap_submit_req) *req = &DCP_FW_UNION(dcp->swap); + struct DCP_FW_NAME(dcp_swap_submit_req) *req = &DCP_FW_SWAP(dcp); if (READ_ONCE(dcp->mode_state.valid) && dcp->connector && READ_ONCE(dcp->connector->connected)) { @@ -2251,8 +2639,122 @@ static void do_swap(struct apple_dcp *dcp, void *data, void *cookie) } else { dcp_drm_crtc_vblank(dcp->crtc); } +#endif +} + +#if DCP_FW_DESKTOP_26(DCP_FW_VER) +/* + * GAMMA_LUT. The scale of the firmware's gamma table is not documented, so + * the LUT is applied through the firmware's own table, read once: entry i of + * the result is that table at LUT[i]. This keeps whatever the firmware's + * table does on top, and a NULL LUT restores it unchanged. + */ +static u32 dcp_gamma_lookup(const __le32 *fw, u16 v) +{ + u32 pos = v * (IOMFB_GAMMA_ENTRIES - 1), i = pos / 0xffff; + s64 a = le32_to_cpu(fw[i]); + + if (i == IOMFB_GAMMA_ENTRIES - 1) + return a; + return a + div_s64((le32_to_cpu(fw[i + 1]) - a) * (pos % 0xffff), 0xffff); +} + +static void dcp_gamma_set_done(struct apple_dcp *dcp, void *out, void *cookie) +{ + do_swap(dcp, NULL, NULL); +} + +static void dcp_gamma_set(struct apple_dcp *dcp) +{ + struct dcp_desktop_state *d = dcp->desktop; + struct iomfb_gamma_table_req *req = d->gamma_out; + u32 i; + + *req = *d->gamma_fw; + for (i = 0; d->gamma_user && i < IOMFB_GAMMA_ENTRIES; i++) { + req->table[0][i] = cpu_to_le32(dcp_gamma_lookup(d->gamma_fw->table[0], + d->gamma_lut[i].red)); + req->table[1][i] = cpu_to_le32(dcp_gamma_lookup(d->gamma_fw->table[1], + d->gamma_lut[i].green)); + req->table[2][i] = cpu_to_le32(dcp_gamma_lookup(d->gamma_fw->table[2], + d->gamma_lut[i].blue)); + } + dcp_push(dcp, false, &dcp_set_gamma_table_method, sizeof(*req), + sizeof(struct iomfb_gamma_table_resp), req, dcp_gamma_set_done, + NULL); +} + +static void dcp_gamma_read(struct apple_dcp *dcp, void *out, void *cookie) +{ + struct dcp_desktop_state *d = dcp->desktop; + struct iomfb_gamma_table_resp *resp = out; + u32 ch, i; + + /* A table to compose with rises from its first to its last entry. */ + for (ch = 0; resp && ch < 3; ch++) { + for (i = 1; i < IOMFB_GAMMA_ENTRIES; i++) + if (le32_to_cpu(resp->table[ch][i]) < + le32_to_cpu(resp->table[ch][i - 1])) + break; + if (resp->table_null || i < IOMFB_GAMMA_ENTRIES || + !le32_to_cpu(resp->table[ch][IOMFB_GAMMA_ENTRIES - 1])) + break; + } + if (!resp || ch < 3) { + dev_warn(dcp->dev, "unusable firmware gamma table, GAMMA_LUT ignored\n"); + d->gamma_unusable = true; + do_swap(dcp, NULL, NULL); + return; + } + memcpy(d->gamma_fw, resp, sizeof(*d->gamma_fw)); + d->gamma_fw_read = true; + dcp_gamma_set(dcp); +} + +/* After the colour matrix: send a changed gamma table, then the swap. */ +static void dcp_gamma_then_swap(struct apple_dcp *dcp, void *out, void *cookie) +{ + static struct iomfb_gamma_table_req empty; + struct dcp_desktop_state *d = dcp->desktop; + + if (!d->gamma_pending || d->gamma_unusable) { + do_swap(dcp, NULL, NULL); + return; + } + d->gamma_pending = false; + if (d->gamma_fw_read) + dcp_gamma_set(dcp); + else + dcp_push(dcp, false, &dcp_get_gamma_table_method, sizeof(empty), + sizeof(struct iomfb_gamma_table_resp), &empty, + dcp_gamma_read, NULL); } +/* Takes a GAMMA_LUT change from the CRTC state; false if there is nothing to send. */ +static bool dcp_gamma_update(struct apple_dcp *dcp, struct drm_crtc_state *crtc_state) +{ + struct dcp_desktop_state *d = dcp->desktop; + bool user = crtc_state->gamma_lut; + + if (!crtc_state->color_mgmt_changed || d->gamma_unusable || + (!user && !d->gamma_user)) + return false; + if (user && drm_color_lut_size(crtc_state->gamma_lut) != IOMFB_GAMMA_ENTRIES) + return false; + if (!d->gamma_fw) { + d->gamma_fw = devm_kcalloc(dcp->dev, 2, sizeof(*d->gamma_fw), + GFP_KERNEL); + if (!d->gamma_fw) + return false; + d->gamma_out = d->gamma_fw + 1; + } + if (user) + memcpy(d->gamma_lut, crtc_state->gamma_lut->data, sizeof(d->gamma_lut)); + d->gamma_user = user; + return true; +} +#endif + #if DCP_FW_H17(DCP_FW_VER) /* * H17P expects A428 between set_matrix and swap_start, with 0x10000 (1.0 in @@ -2293,6 +2795,212 @@ static void complete_set_digital_out_mode(struct apple_dcp *dcp, void *data, } } +#if DCP_FW_DESKTOP_26(DCP_FW_VER) +struct dcp_modeset_cookie { + /* Must be first: release_wait_cookie() frees this allocation. */ + struct dcp_wait_cookie wait; + struct dcp_set_digital_out_mode_req actual; + struct dcp_set_digital_out_mode_req bootstrap; + bool use_bootstrap; +}; + +/* + * get_mode_info (A364) fills the firmware's mode record for a timing and + * color pair before set_digital_out_mode (A413) selects it. The request + * bytes the firmware does not read are 0xaa, as the firmware's own client + * leaves them. + */ +static void dcp_get_mode_info(struct apple_dcp *dcp, + const struct dcp_set_digital_out_mode_req *mode, + dcp_callback_t cb, void *cookie) +{ + struct dcp_get_mode_info_req_v26_6 req; + + memset(&req, 0xaa, sizeof(req)); + memset(req.mode, 0, sizeof(req.mode)); + req.mode_null = 0; + req.timing_mode_id = mode->timing_mode_id; + req.color_mode_id = mode->color_mode_id; + req.result_null = 0; + memset(req.padding1, 0, sizeof(req.padding1)); + + dcp_push(dcp, false, &dcp_get_mode_info_method, sizeof(req), + sizeof(struct dcp_get_mode_info_resp_v26_6), &req, cb, cookie); +} + +static void dcp_modeset_after_actual_info(struct apple_dcp *dcp, void *data, + void *cookie) +{ + struct dcp_modeset_cookie *modeset = cookie; + + if (!data) { + complete_set_digital_out_mode(dcp, NULL, cookie); + return; + } + dcp_set_digital_out_mode(dcp, false, &modeset->actual, + complete_set_digital_out_mode, cookie); +} + +static void dcp_modeset_after_bootstrap(struct apple_dcp *dcp, void *data, + void *cookie) +{ + struct dcp_modeset_cookie *modeset = cookie; + + if (!data || get_unaligned_le32(data)) { + complete_set_digital_out_mode(dcp, data, cookie); + return; + } + dcp_get_mode_info(dcp, &modeset->actual, dcp_modeset_after_actual_info, + cookie); +} + +static void dcp_modeset_after_bootstrap_info(struct apple_dcp *dcp, void *data, + void *cookie) +{ + struct dcp_modeset_cookie *modeset = cookie; + + if (!data) { + complete_set_digital_out_mode(dcp, NULL, cookie); + return; + } + dcp_set_digital_out_mode(dcp, false, &modeset->bootstrap, + dcp_modeset_after_bootstrap, cookie); +} + +/* Selects the cookie's mode, through the bootstrap color element first. */ +static void dcp_modeset_select(struct apple_dcp *dcp, void *out, void *cookie) +{ + struct dcp_modeset_cookie *modeset = cookie; + + if (modeset->use_bootstrap) + dcp_get_mode_info(dcp, &modeset->bootstrap, + dcp_modeset_after_bootstrap_info, cookie); + else + dcp_get_mode_info(dcp, &modeset->actual, + dcp_modeset_after_actual_info, cookie); +} + +int DCP_FW_NAME(iomfb_modeset)(struct apple_dcp *dcp, + struct drm_crtc_state *crtc_state) +{ + struct dcp_display_mode selected, *mode = &selected; + struct dcp_modeset_cookie *modeset; + struct dcp_wait_cookie *cookie; + struct dcp_color_mode *cmode = NULL; + int ret; + + if (dcp->crashed) + return -ENODEV; + + if (!lookup_mode(dcp, &crtc_state->mode, mode)) { + dev_err(dcp->dev, "no match for " DRM_MODE_FMT "\n", + DRM_MODE_ARG(&crtc_state->mode)); + return -EIO; + } + + dev_dbg(dcp->dev, + "set_digital_out_mode(color:%d timing:%d) " DRM_MODE_FMT "\n", + mode->color_mode_id, mode->timing_mode_id, + DRM_MODE_ARG(&crtc_state->mode)); + if (mode->color_mode_id == mode->sdr_rgb.id) + cmode = &mode->sdr_rgb; + else if (mode->color_mode_id == mode->sdr_444.id) + cmode = &mode->sdr_444; + else if (mode->color_mode_id == mode->sdr.id) + cmode = &mode->sdr; + else if (mode->color_mode_id == mode->best.id) + cmode = &mode->best; + if (cmode) + dev_dbg(dcp->dev, + "color mode depth:%hhu format:%u colorimetry:%u eotf:%u range:%u vrr:%u\n", + cmode->depth, cmode->format, cmode->colorimetry, + cmode->eotf, cmode->range, mode->vrr); + + dcp->mode = (struct dcp_set_digital_out_mode_req) { + .color_mode_id = mode->color_mode_id, + .timing_mode_id = mode->timing_mode_id + }; + + /* + * Suspected VRR modes need swap timestamps, see iomfb_flush(). + * Adaptive sync itself stays off, see dcp_mode_vrr_supported(). + */ + dcp->use_timestamps = mode->vrr; + + /* + * The firmware changes a fixed panel's mode through the swap geometry, + * not through get_mode_info and set_digital_out_mode. Accept the + * single fixed timing it advertises, already matched above. + */ + if (dcp_has_panel(dcp) && dcp->main_display && + READ_ONCE(dcp->nr_modes) == 1 && !mode->vrr) { + dcp->desktop->mode_live = true; + return 0; + } + + modeset = kzalloc_obj(*modeset); + if (!modeset) + return -ENOMEM; + cookie = &modeset->wait; + modeset->actual = dcp->mode; + + cookie->status = ~0U; + init_completion(&cookie->done); + kref_init(&cookie->refcount); + /* increase refcount to ensure the receiver has a reference */ + kref_get(&cookie->refcount); + + dcp->swap_submit_timestamp = 0; + + /* A pipe without a live mode is bootstrapped first. */ + if (!dcp->desktop->mode_live && mode->has_bootstrap) { + modeset->use_bootstrap = true; + modeset->bootstrap = (struct dcp_set_digital_out_mode_req) { + .color_mode_id = mode->bootstrap_color_id, + .timing_mode_id = mode->timing_mode_id, + }; + dev_dbg(dcp->dev, + "bootstrapping the display pipe (color:%u timing:%u)\n", + modeset->bootstrap.color_mode_id, + modeset->bootstrap.timing_mode_id); + } + + dcp_modeset_select(dcp, NULL, cookie); + + /* + * The DCP firmware has an internal timeout of ~8 seconds for + * modesets. Add an extra 500ms to safe side that the modeset + * call has returned. + */ + ret = wait_for_completion_timeout(&cookie->done, + msecs_to_jiffies(8500)); + + if (ret == 0) { + dev_err(dcp->dev, "set_digital_out_mode timed out\n"); + ret = -EIO; + } else if (ret < 0) { + dev_err(dcp->dev, + "waiting on set_digital_out_mode failed:%d\n", ret); + ret = -EIO; + } else if (!READ_ONCE(cookie->replied)) { + dev_err(dcp->dev, "set_digital_out_mode got no reply\n"); + ret = -EIO; + } else if (READ_ONCE(cookie->status)) { + dev_err(dcp->dev, "set_digital_out_mode returned 0x%08x\n", + READ_ONCE(cookie->status)); + ret = -EIO; + } else { + dev_dbg(dcp->dev, + "set_digital_out_mode finished with %d to spare\n", + jiffies_to_msecs(ret)); + ret = 0; + } + kref_put(&cookie->refcount, release_wait_cookie); + + dcp->desktop->mode_live = !ret; + return ret; +} +#else /* DCP applies Adaptive Sync changes when the display mode is reselected. */ static void dcp_on_set_adaptive_sync(struct apple_dcp *dcp, void *out, void *cookie) @@ -2437,13 +3145,14 @@ int DCP_FW_NAME(iomfb_modeset)(struct apple_dcp *dcp, return 0; } +#endif void DCP_FW_NAME(iomfb_flush)(struct apple_dcp *dcp, struct drm_crtc *crtc, struct drm_atomic_commit *commit) { struct drm_plane *plane; struct drm_plane_state *new_state, *old_state; struct drm_crtc_state *crtc_state; - struct DCP_FW_NAME(dcp_swap_submit_req) *req = &DCP_FW_UNION(dcp->swap); + struct DCP_FW_NAME(dcp_swap_submit_req) *req = &DCP_FW_SWAP(dcp); int plane_idx, l; int has_surface = 0; bool update_brightness; @@ -2456,12 +3165,17 @@ void DCP_FW_NAME(iomfb_flush)(struct apple_dcp *dcp, struct drm_crtc *crtc, stru req->surf_null[l] = true; #if DCP_FW_VER >= DCP_FW_VERSION(13, 2, 0) #if !DCP_FW_H17(DCP_FW_VER) - for (l = 0; l < 5; l++) + for (l = 0; l < ARRAY_SIZE(req->surf2_null); l++) req->surf2_null[l] = true; #endif req->unknown_pointer_null = true; req->unknown_output_null = true; #endif +#if DCP_FW_DESKTOP_26(DCP_FW_VER) + /* The firmware's own client sets both on every visible swap. */ + req->swap.presents_surface = 1; + req->swap.has_completion = 1; +#endif /* * Clear all surfaces on startup. The boot framebuffer in surface 0 @@ -2493,7 +3207,7 @@ void DCP_FW_NAME(iomfb_flush)(struct apple_dcp *dcp, struct drm_crtc *crtc, stru req->swap.src_rect[l] = apple_state->src_rect; req->swap.dst_rect[l] = apple_state->dst_rect; -#if !DCP_FW_H17(DCP_FW_VER) +#if !DCP_FW_H17(DCP_FW_VER) && !DCP_FW_DESKTOP_26(DCP_FW_VER) req->swap.surf_ids[apl_plane->iomfb_surf] = plane->base.id; #endif @@ -2541,6 +3255,22 @@ void DCP_FW_NAME(iomfb_flush)(struct apple_dcp *dcp, struct drm_crtc *crtc, stru req->clear = 1; } +#if DCP_FW_DESKTOP_26(DCP_FW_VER) + if (has_surface && dcp->use_timestamps) { + /* + * Timestamp types 1, 2 and 7 hold the time the swap should be + * shown, in the firmware's clock. A target long past shows it + * at the next refresh: 120 Hz in the fixed mode, as soon as + * the panel allows with adaptive sync. The architectural + * counter must not go here: after a VRR mode set the firmware + * then never shows the swap, and it holds a swap whose target + * it reads as more than about a frame ahead. + */ + req->swap.timestamp[0] = 120; + req->swap.timestamp[1] = 120; + req->swap.timestamp[6] = 120; + } +#else if (has_surface && (dcp->use_timestamps || dcp->vrr_enabled)) { u64 submit_timestamp = dcp->swap_submit_timestamp; u64 timestamp = arch_timer_read_counter(); @@ -2562,6 +3292,7 @@ void DCP_FW_NAME(iomfb_flush)(struct apple_dcp *dcp, struct drm_crtc *crtc, stru trace_iomfb_vrr_timestamps(dcp, dcp->vrr_enabled, timestamp, submit_timestamp); } +#endif /* These fields should be set together */ req->swap.swap_completed = req->swap.swap_enabled; @@ -2572,6 +3303,22 @@ void DCP_FW_NAME(iomfb_flush)(struct apple_dcp *dcp, struct drm_crtc *crtc, stru update_brightness = update_brightness && dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17G; #endif +#if DCP_FW_DESKTOP_26(DCP_FW_VER) + /* + * The firmware publishes no initial level, so nits stays at the + * registration default until userspace writes the backlight. Sending + * that from the forced post-poweron update can turn the panel off: + * keep the level the boot loader set until one is requested. + */ + if (update_brightness && !dcp->brightness.requested) { + dcp->brightness.update = false; + update_brightness = false; + } + if (update_brightness) { + dcp_swap_set_brightness(&req->swap, dcp->brightness.nits); + dcp->brightness.update = false; + } +#else if (update_brightness) { #if DCP_FW_LAYOUT_14_7(DCP_FW_VER) req->swap.bl_update = 1; @@ -2582,6 +3329,7 @@ void DCP_FW_NAME(iomfb_flush)(struct apple_dcp *dcp, struct drm_crtc *crtc, stru req->swap.bl_power = 0x40; dcp->brightness.update = false; } +#endif if (crtc_state->color_mgmt_changed) { struct iomfb_set_matrix_req mat = { @@ -2597,6 +3345,9 @@ void DCP_FW_NAME(iomfb_flush)(struct apple_dcp *dcp, struct drm_crtc *crtc, stru #if DCP_FW_H17(DCP_FW_VER) iomfb_set_matrix(dcp, false, &mat, dcp_pre_swap_a428, NULL); +#elif DCP_FW_DESKTOP_26(DCP_FW_VER) + dcp->desktop->gamma_pending = dcp_gamma_update(dcp, crtc_state); + iomfb_set_matrix(dcp, false, &mat, dcp_gamma_then_swap, NULL); #else iomfb_set_matrix(dcp, false, &mat, do_swap, NULL); #endif @@ -2632,6 +3383,15 @@ static void res_is_main_display(struct apple_dcp *dcp, void *out, void *cookie) } dcp->active = true; +#if DCP_FW_DESKTOP_26(DCP_FW_VER) + /* + * The firmware publishes no initial panel level that would register + * the backlight (see iomfb_cb_pr_publish()); register it now. + */ + if (dcp_has_panel(dcp) && !dcp->brightness.bl_dev && + dcp->brightness.maximum > 0) + schedule_work(&dcp->bl_register_wq); +#endif complete(&dcp->start_done); } @@ -2801,9 +3561,14 @@ static void dcp_started(struct apple_dcp *dcp, void *data, void *cookie) void DCP_FW_NAME(iomfb_shutdown)(struct apple_dcp *dcp) { +#if DCP_FW_DESKTOP_26(DCP_FW_VER) + struct DCP_FW_NAME(dcp_set_power_state_req) req = + dcp_power_state_req(0, DCP_POWER_TRANSITION_SHUTDOWN); +#else struct DCP_FW_NAME(dcp_set_power_state_req) req = { /* defaults are ok */ }; +#endif struct dcp_wait_cookie *cookie; int ret; diff --git a/drivers/gpu/drm/apple/iomfb_template.h b/drivers/gpu/drm/apple/iomfb_template.h index bebb9c0f5c265..d37a013b50d90 100644 --- a/drivers/gpu/drm/apple/iomfb_template.h +++ b/drivers/gpu/drm/apple/iomfb_template.h @@ -65,6 +65,15 @@ struct DCP_FW_NAME(dcp_swap) { #else u32 bl_update; #endif +#if DCP_FW_DESKTOP_26(DCP_FW_VER) + /* + * The desktop 26.x firmware takes the panel level in nits, as an + * IEEE-754 binary64 at +0x35e (unaligned), flagged by bl_flags[1]. + */ + u8 bl_flags[6]; + __le64 bl_nits; + u8 unk_366[0x2b]; +#else #if DCP_FW_VER < DCP_FW_VERSION(13, 2, 0) u16 unk_2e2; #else @@ -78,6 +87,7 @@ struct DCP_FW_NAME(dcp_swap) { u32 bl_value; // min value is 0x10000000 u8 bl_power; // constant 0x40 for on u8 unk_2f3[0x2d]; +#endif #if DCP_FW_VER >= DCP_FW_VERSION(13, 2, 0) u8 unk_320[0x13f]; #if DCP_FW_LAYOUT_14_7(DCP_FW_VER) @@ -86,6 +96,17 @@ struct DCP_FW_NAME(dcp_swap) { u32 unk_flags; u32 unk_flags2; #endif +#if DCP_FW_DESKTOP_26(DCP_FW_VER) + /* + * Appended to the 14.7 record. The firmware expects both flags set + * on every visible swap; the other bytes are zero. + */ + u8 unk_26_6[0x16]; + u8 presents_surface; + u8 unk_26_6_17[0x51]; + u8 has_completion; + u8 unk_26_6_69[0x17]; +#endif #if DCP_FW_H17(DCP_FW_VER) /* * The H17P A408 request is 0xe9c bytes; with a 0x22c-byte surface, @@ -107,6 +128,13 @@ struct DCP_FW_NAME(dcp_surface) { /* Prototypes */ +/* The desktop 26.x request carries one more surf2 entry. */ +#if DCP_FW_DESKTOP_26(DCP_FW_VER) +#define DCP_FW_SURF2_COUNT 6 +#else +#define DCP_FW_SURF2_COUNT 5 +#endif + struct DCP_FW_NAME(dcp_swap_submit_req) { struct DCP_FW_NAME(dcp_swap) swap; struct DCP_FW_NAME(dcp_surface) surf[SWAP_SURFACES]; @@ -118,8 +146,8 @@ struct DCP_FW_NAME(dcp_swap_submit_req) { * (swap_null + surf_null[4] + three tail bools). */ #if !DCP_FW_H17(DCP_FW_VER) - struct DCP_FW_NAME(dcp_surface) surf2[5]; - u64 surf2_iova[5]; + struct DCP_FW_NAME(dcp_surface) surf2[DCP_FW_SURF2_COUNT]; + u64 surf2_iova[DCP_FW_SURF2_COUNT]; #endif #endif u8 unkbool; @@ -132,14 +160,14 @@ struct DCP_FW_NAME(dcp_swap_submit_req) { #if DCP_FW_VER >= DCP_FW_VERSION(13, 2, 0) u32 unkU32Ptr; #endif -#if DCP_FW_LAYOUT_14_7(DCP_FW_VER) +#if DCP_FW_LAYOUT_14_7(DCP_FW_VER) && !DCP_FW_DESKTOP_26(DCP_FW_VER) u8 padding_14_7[0x234]; #endif u8 swap_null; u8 surf_null[SWAP_SURFACES]; #if DCP_FW_VER >= DCP_FW_VERSION(13, 2, 0) #if !DCP_FW_H17(DCP_FW_VER) - u8 surf2_null[5]; + u8 surf2_null[DCP_FW_SURF2_COUNT]; #endif #endif u8 unkoutbool_null; @@ -147,11 +175,13 @@ struct DCP_FW_NAME(dcp_swap_submit_req) { u8 unknown_pointer_null; u8 unknown_output_null; #endif -#if !DCP_FW_H17(DCP_FW_VER) +#if !DCP_FW_H17(DCP_FW_VER) && !DCP_FW_DESKTOP_26(DCP_FW_VER) u8 padding[1]; #endif } __packed; +#undef DCP_FW_SURF2_COUNT + struct DCP_FW_NAME(dcp_swap_submit_resp) { u8 unkoutbool; #if DCP_FW_VER >= DCP_FW_VERSION(13, 2, 0) @@ -165,9 +195,33 @@ struct DCP_FW_NAME(dcp_swap_submit_resp) { * A406 swap_start / A473 set_power_state. H17P shrank both requests: the * 0x10-byte struct dcp_iouserclient became a bare u64 handle and the * swap_start response no longer echoes it. swap_start is 0x10 in / 0x8 out, - * set_power_state 0x10 in. + * set_power_state 0x10 in. The desktop 26.x firmware made the same + * swap_start change; its set_power_state carries four transition flags. */ -#if DCP_FW_H17(DCP_FW_VER) +#if DCP_FW_DESKTOP_26(DCP_FW_VER) +struct DCP_FW_NAME(dcp_swap_start_req) { + u32 swap_id; + u64 client; /* opaque client identity */ + u8 swap_id_null; + u8 padding[3]; +} __packed; + +struct DCP_FW_NAME(dcp_swap_start_resp) { + u32 swap_id; + u32 ret; +} __packed; + +struct DCP_FW_NAME(dcp_set_power_state_req) { + u64 unklong; /* the power state */ + u8 flags[4]; /* independent transition flags */ + u8 result_null; + u8 padding[3]; +} __packed; + +static_assert(sizeof(struct DCP_FW_NAME(dcp_swap_start_req)) == 0x10); +static_assert(sizeof(struct DCP_FW_NAME(dcp_swap_start_resp)) == 0x8); +static_assert(sizeof(struct DCP_FW_NAME(dcp_set_power_state_req)) == 0x10); +#elif DCP_FW_H17(DCP_FW_VER) struct DCP_FW_NAME(dcp_swap_start_req) { u32 swap_id; u64 client; /* IOUserClient kernel VA */ @@ -226,6 +280,14 @@ struct DCP_FW_NAME(dcp_map_buf_resp) { u32 unk; } __packed; static_assert(sizeof(struct DCP_FW_NAME(dcp_map_buf_resp)) == 0x10); +#elif DCP_FW_DESKTOP_26(DCP_FW_VER) +/* The desktop 26.x reply has the H17P layout, with a status. */ +struct DCP_FW_NAME(dcp_map_buf_resp) { + u32 buffer; + u64 dva; + u32 ret; +} __packed; +static_assert(sizeof(struct DCP_FW_NAME(dcp_map_buf_resp)) == 0x10); #else struct DCP_FW_NAME(dcp_map_buf_resp) { u64 vaddr; @@ -236,6 +298,10 @@ struct DCP_FW_NAME(dcp_map_buf_resp) { struct DCP_FW_NAME(dc_swap_complete_resp) { u32 swap_id; /* +0x000 */ +#if DCP_FW_DESKTOP_26(DCP_FW_VER) + /* The desktop 26.x record is 0x730 bytes; the rest is not decoded. */ + u8 opaque[0x72c]; +#else u8 unkbool; /* +0x004 */ #if DCP_FW_H17(DCP_FW_VER) /* @@ -268,6 +334,7 @@ struct DCP_FW_NAME(dc_swap_complete_resp) { u32 unkint; u8 swap_info_null; #endif +#endif } __packed; struct DCP_FW_NAME(dcp_map_reg_req) { diff --git a/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c b/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c new file mode 100644 index 0000000000000..98a3504a9a5f0 --- /dev/null +++ b/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +/* Copyright The Asahi Linux Contributors */ + +/* + * Desktop 26.6 firmware on T6041 (M4 Pro/Max) through the classic IOMFB + * template (DCP_FIRMWARE_V_26_6_DESKTOP). This firmware continues the 14.7 + * IOMFB, with its own method numbers, record layouts and power sequences. + * Not to be confused with iomfb_v26_6.c, the J613 DCPLink session, nor with + * the H17-generation 26.x firmware of iomfb_h17p.c. + */ + +/* Include all per-version headers before defining DCP_FW ourself. */ +#include "iomfb_v12_3.h" +#include "iomfb_v13_3.h" +#include "iomfb_v14_7_tpl.h" +#include "iomfb_v26_6_desktop.h" +#include "iomfb_h17p.h" +#include "version_utils.h" + +static const struct dcp_method_entry dcp_methods[dcpep_num_methods] = { + IOMFB_METHOD("A000", dcpep_late_init_signal), + IOMFB_METHOD("A029", dcpep_setup_video_limits), + IOMFB_METHOD("A131", iomfbep_a131_pmu_service_matched), + IOMFB_METHOD("A132", iomfbep_a132_backlight_service_matched), + IOMFB_METHOD("A385", dcpep_set_create_dfb), + IOMFB_METHOD("A386", iomfbep_a358_vi_set_temperature_hint), + IOMFB_METHOD("A401", dcpep_start_signal), + IOMFB_METHOD("A406", dcpep_swap_start), + IOMFB_METHOD("A407", dcpep_swap_submit), + IOMFB_METHOD("A411", dcpep_set_display_device), + IOMFB_METHOD("A412", dcpep_is_main_display), + IOMFB_METHOD("A413", dcpep_set_digital_out_mode), + /* prepare/finish around the display power changes */ + IOMFB_METHOD("A414", dcpep_pipe_cfg_414), + IOMFB_METHOD("A423", iomfbep_set_matrix), + IOMFB_METHOD("A427", iomfbep_get_color_remap_mode), + IOMFB_METHOD("A442", dcpep_set_parameter_dcp), + IOMFB_METHOD("A446", dcpep_create_default_fb), + IOMFB_METHOD("A450", dcpep_enable_disable_video_power_savings), + IOMFB_METHOD("A457", dcpep_first_client_open), + IOMFB_METHOD("A458", iomfbep_last_client_close), + IOMFB_METHOD("A465", dcpep_set_display_refresh_properties), + IOMFB_METHOD("A468", dcpep_flush_supports_power), + IOMFB_METHOD("A469", iomfbep_abort_swaps_dcp), + IOMFB_METHOD("A473", dcpep_set_power_state), + IOMFB_METHOD("A474", dcpep_pipe_query_474), + /* A364 get_mode_info and A420/A421 gamma: see iomfb_template.c */ +}; + +#define DCP_FW v26_6_desktop +#define DCP_FW_VER DCP_FW_VERSION_DESKTOP(26, 6, 0) + +#include "iomfb_template.c" + +struct dcp_dict_passthrough_req_v26_6 { + u8 key[0x40]; + u8 value[0x1000]; + u8 value_null; + u8 padding[3]; +} __packed; + +struct dcp_dict_passthrough_resp_v26_6 { + u8 value[0x1000]; + u8 ret; + u8 padding[3]; +} __packed; + +static_assert(sizeof(struct dcp_dict_passthrough_req_v26_6) == 0x1044); +static_assert(sizeof(struct dcp_dict_passthrough_resp_v26_6) == 0x1004); + +/* + * D114 periodically publishes dictionary-valued statistics. The value is an + * in/out pointer, which the firmware deserializes again: the firmware's own + * client returns the serialized buffer unchanged and reports false. An + * all-zero reply is not a harmless false. + */ +static bool trampoline_passthrough_dict_v26_6(struct apple_dcp *dcp, int tag, + void *out, void *in) +{ + const struct dcp_dict_passthrough_req_v26_6 *req = in; + struct dcp_dict_passthrough_resp_v26_6 *resp = out; + + trace_iomfb_callback(dcp, tag, "dcpep_cb_passthrough_dict"); + + if (!req->value_null) + memcpy(resp->value, req->value, sizeof(resp->value)); + + resp->ret = false; + memset(resp->padding, 0, sizeof(resp->padding)); + + return true; +} + +static const iomfb_cb_handler cb_handlers[IOMFB_MAX_CB] = { + [0] = trampoline_true, /* did_boot_signal */ + [1] = trampoline_true, /* did_power_on_signal */ + [2] = trampoline_nop, /* will_power_off_signal */ + [3] = trampoline_rt_bandwidth, + [6] = trampoline_set_frame_sync_props, + [100] = iomfbep_cb_match_pmu_service, + [101] = trampoline_zero, /* get_display_default_stride */ + [102] = trampoline_nop, /* set_number_property */ + [104] = trampoline_nop, /* set_boolean_property */ + [107] = trampoline_nop, /* remove_property */ + [108] = trampoline_true, /* create_provider_service */ + [109] = trampoline_true, /* create_product_service */ + [110] = trampoline_true, /* create_pmu_service */ + [111] = trampoline_true, /* create_iomfb_service */ + [112] = trampoline_create_backlight_service, + [113] = trampoline_true, /* create_nvram_service? */ + [114] = trampoline_passthrough_dict_v26_6, + /* set_tiling_state: the value followed by success */ + [115] = trampoline_get_tiling_state, + [121] = dcpep_cb_boot_1, /* start_hardware_boot */ + [122] = trampoline_false, /* is_dark_boot / is_waking_from_hibernate */ + [123] = trampoline_false, + [125] = trampoline_read_edt_data, + [127] = trampoline_prop_start, + [128] = trampoline_prop_chunk, + [129] = trampoline_prop_end, + [134] = trampoline_zero, + [201] = trampoline_map_piodma, + [206] = iomfbep_cb_match_pmu_service_2, + [207] = iomfbep_cb_match_backlight_service, + [208] = trampoline_nop, /* update_backlight_factor_prop */ + [209] = trampoline_get_time, + [300] = trampoline_pr_publish, + [400] = trampoline_nop, + [401] = trampoline_get_uint_prop, + [406] = trampoline_nop, /* set_fx_prop: a property notification */ + [408] = trampoline_get_frequency, + [411] = trampoline_map_reg, + [413] = trampoline_true, /* sr_set_property_dict */ + [414] = trampoline_sr_set_property_int, + [415] = trampoline_true, /* sr_set_property_bool */ + [451] = trampoline_allocate_buffer, + [552] = trampoline_true, /* set_property_dict_0 */ + [561] = trampoline_true, /* set_property_dict */ + [563] = trampoline_true, /* set_property_int */ + [565] = trampoline_true, /* set_property_bool */ + [567] = trampoline_true, /* set_property_str */ + /* a zero status for both its start and stop events */ + [572] = trampoline_zero, + [575] = trampoline_hotplug, + [576] = trampoline_nop, + [581] = trampoline_nop, + [582] = trampoline_true, /* create_default_fb_surface */ + [590] = trampoline_swap_complete, + [592] = trampoline_swap_complete_intent_gated, + [593] = trampoline_abort_swap_ap_gated, + /* a four-byte padded notification; only its first byte is read */ + [594] = trampoline_enable_backlight_message_ap_gated, + [595] = trampoline_nop, + [598] = trampoline_nop, + [599] = trampoline_nop, +}; + +void DCP_FW_NAME(iomfb_start)(struct apple_dcp *dcp) +{ + dcp->cb_handlers = cb_handlers; + + dcp_start_signal(dcp, false, dcp_started, NULL); +} + +#undef DCP_FW_VER +#undef DCP_FW diff --git a/drivers/gpu/drm/apple/iomfb_v26_6_desktop.h b/drivers/gpu/drm/apple/iomfb_v26_6_desktop.h new file mode 100644 index 0000000000000..697edc47fde3b --- /dev/null +++ b/drivers/gpu/drm/apple/iomfb_v26_6_desktop.h @@ -0,0 +1,52 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ +/* Copyright The Asahi Linux Contributors */ + +/* + * The classic IOMFB (dcpep) protocol of the desktop 26.6 firmware on T6041 + * (M4 Pro/Max), from the shared IOMFB template + * (DCP_FIRMWARE_V_26_6_DESKTOP). It continues the 14.7 IOMFB. Not to be + * confused with iomfb_v26_6.h, the J613 DCPLink session. + */ + +#ifndef __APPLE_IOMFB_V26_6_DESKTOP_H__ +#define __APPLE_IOMFB_V26_6_DESKTOP_H__ + +#include + +#include "version_utils.h" + +/* + * dcp-internal.h includes this header, also from inside another template + * instantiation: keep that one's DCP_FW. + */ +#pragma push_macro("DCP_FW") +#pragma push_macro("DCP_FW_VER") +#undef DCP_FW +#undef DCP_FW_VER + +#define DCP_FW v26_6_desktop +#define DCP_FW_VER DCP_FW_VERSION_DESKTOP(26, 6, 0) + +#include "iomfb_template.h" + +#undef DCP_FW_VER +#undef DCP_FW + +#pragma pop_macro("DCP_FW_VER") +#pragma pop_macro("DCP_FW") + +/* + * D006 set_frame_sync_props is 0x54 bytes in and 0x50 out. The firmware + * expects the first 0x50 request bytes back, with the three flag bytes at + * 73..75 cleared. + */ +struct dcp_set_frame_sync_props_req_v26_6_desktop { + u8 props[0x50]; + u8 opaque[0x4]; +} __packed; + +struct dcp_set_frame_sync_props_resp_v26_6_desktop { + u8 props[0x50]; +} __packed; + +#endif /* __APPLE_IOMFB_V26_6_DESKTOP_H__ */ diff --git a/drivers/gpu/drm/apple/parser.c b/drivers/gpu/drm/apple/parser.c index 0de67aa8fe710..58d09f97e66b1 100644 --- a/drivers/gpu/drm/apple/parser.c +++ b/drivers/gpu/drm/apple/parser.c @@ -558,12 +558,14 @@ static int parse_color_modes(struct dcp_parse_ctx *handle, struct dcp_color_mode *unsafe_rgb8) { struct iterator outer_it; + struct dcp_color_mode bootstrap = { .score = -1 }; unsigned int best_sdr_rank = 0, best_unsafe_rank = 0; int ret = 0; out->sdr_444.score = -1; out->sdr_rgb.score = -1; out->sdr.score = -1; out->best.score = -1; + out->has_bootstrap = false; ret = iterator_begin(handle, &outer_it, false); if (ret) @@ -627,7 +629,16 @@ static int parse_color_modes(struct dcp_parse_ctx *handle, return ret; } - /* Skip virtual or partial entries */ + /* + * Skip virtual or partial entries. Remember the best virtual + * one: the desktop 26.x firmware bootstraps its generic pipe + * through it. + */ + if (is_virtual && cmode.score >= 0 && cmode.id >= 0 && + !fill_color_mode(&bootstrap, &cmode) && bootstrap.score >= 0) { + out->has_bootstrap = true; + out->bootstrap_color_id = bootstrap.id; + } if (is_virtual || cmode.score < 0 || cmode.id < 0 || cmode.id > U32_MAX) continue; diff --git a/drivers/gpu/drm/apple/parser.h b/drivers/gpu/drm/apple/parser.h index 0557cc09e63e4..857b3880ac754 100644 --- a/drivers/gpu/drm/apple/parser.h +++ b/drivers/gpu/drm/apple/parser.h @@ -106,9 +106,16 @@ struct dcp_display_mode { bool preferred; bool metadata_timing_valid; bool automatic_rgb8_quirk; + /* + * The best virtual color element of the timing, which the desktop + * 26.x firmware selects first to bootstrap its generic pipe (valid if + * has_bootstrap). Both sit in what was padding. + */ + u32 bootstrap_color_id; u64 automatic_connection_sequence; u64 automatic_catalog_sequence; bool automatic_service_pending; + bool has_bootstrap; /* Immutable receipt provenance; never retag a cached exception. */ struct dcp_metadata_ticket unsafe_rgb8_ticket; u64 unsafe_rgb8_service_epoch; diff --git a/drivers/gpu/drm/apple/version_utils.h b/drivers/gpu/drm/apple/version_utils.h index 177d8f0119201..8851ee57c2daf 100644 --- a/drivers/gpu/drm/apple/version_utils.h +++ b/drivers/gpu/drm/apple/version_utils.h @@ -28,12 +28,13 @@ ((ver) & ~DCP_FW_DESKTOP) >= DCP_FW_VERSION(26, 0, 0)) /* - * The swap record layout introduced with the 14.7 firmware. The - * H17-generation firmware (26.0 and later) keeps the earlier layout and - * extends it with its own fields. + * The swap record layout introduced with the 14.7 firmware. The desktop + * 26.x firmware keeps it and appends to it. The H17-generation firmware + * (26.0 and later) keeps the earlier layout and extends it with its own + * fields. */ #define DCP_FW_LAYOUT_14_7(ver) \ - ((ver) >= DCP_FW_VERSION(14, 7, 0) && (ver) < DCP_FW_VERSION(26, 0, 0)) + ((ver) >= DCP_FW_VERSION(14, 7, 0) && !DCP_FW_H17(ver)) enum dcp_firmware_version { DCP_FIRMWARE_UNKNOWN, @@ -55,6 +56,11 @@ enum dcp_firmware_version { DCP_FIRMWARE_V_26_6, /* T8132/J713 retained EP37 session. */ DCP_FIRMWARE_M4, + /* + * T6041 (M4 Pro/Max) desktop 26.6 IOMFB through the classic template + * (iomfb_v26_6_desktop.c); not the J613 DCPLink DCP_FIRMWARE_V_26_6. + */ + DCP_FIRMWARE_V_26_6_DESKTOP, }; #endif /*__APPLE_VERSION_UTILS_H__*/ From 8e82151640fceb5c17a1707903cbfe929964f63d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Bal=C3=A1zsy?= Date: Sun, 25 Oct 2026 12:59:20 +0100 Subject: [PATCH 06/12] drm/apple: afk: speak compact EPIC on desktop 26.x endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The desktop 26.6 firmware of the T6041 DCP speaks a compact EPIC on its system and auxiliary endpoints: an 8-byte transport header (sequence, flags, interface, total length), the timestamp, an 8-byte sub-header and the payload. Messages longer than 1028 bytes arrive in fragments that repeat the header. Services announce themselves with type 0x11 and are opened with type 0x12; commands, replies and calls carry a status/length descriptor. On DCP_FIRMWARE_V_26_6_DESKTOP endpoints, switch to it on the first compact message: reassemble fragments, register and open announced services, answer calls, deliver reports and match replies to pending commands. Send commands and replies the same way, fragmented when needed, waiting for ring space with WAIT_FOR_SPACE, and answer the firmware's WAIT_FOR_SPACE after draining the receive ring. Call replies are sent from a separate workqueue so that a full transmit ring cannot stall reception. Ring pointers read from shared memory are validated before use; a send that fails part-way fails the commands in flight without shutting the endpoint down. Other firmware never takes these paths. Co-developed-by: Niklas Sheth Signed-off-by: Niklas Sheth Signed-off-by: František Balázsy --- drivers/gpu/drm/apple/afk-fragment.h | 44 ++ drivers/gpu/drm/apple/afk.c | 879 +++++++++++++++++++++++++++ drivers/gpu/drm/apple/afk.h | 50 ++ 3 files changed, 973 insertions(+) create mode 100644 drivers/gpu/drm/apple/afk-fragment.h diff --git a/drivers/gpu/drm/apple/afk-fragment.h b/drivers/gpu/drm/apple/afk-fragment.h new file mode 100644 index 0000000000000..63467b39c01de --- /dev/null +++ b/drivers/gpu/drm/apple/afk-fragment.h @@ -0,0 +1,44 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ +#ifndef __APPLE_DCP_AFK_FRAGMENT_H__ +#define __APPLE_DCP_AFK_FRAGMENT_H__ + +#include +#include + +/* Native 26.6 AV traffic: repeated 8-byte header, then up to 1028 bytes + * of the logical message (timestamp, subheader, descriptor and payload). + * The total length is repeated and the endpoint sequence advances on every + * fragment, including 255 -> 0. Messages are not interleaved on an endpoint. + */ +#define AFK_COMPACT_FRAGMENT_SIZE 1028U +#define AFK_COMPACT_MESSAGE_MAX (1024U * 1024U) + +struct afk_fragment_state { + u32 total; + u32 received; + u16 intf_id; + u8 next_seq; +}; + +/* Validate before allocating/copying. Returns 1 on completion, 0 while + * collecting, or an error. The caller discards the whole message on error. + */ +static inline int afk_fragment_accept(struct afk_fragment_state *s, + u16 intf_id, u8 seq, u32 total, u32 size) +{ + if (total < 16 || total > AFK_COMPACT_MESSAGE_MAX || !size || size > total) + return -EMSGSIZE; + if (s->received && (s->total != total || s->intf_id != intf_id || + s->next_seq != seq)) + return -EPROTO; + if (size > total - s->received) + return -EMSGSIZE; + + s->total = total; + s->intf_id = intf_id; + s->next_seq = seq + 1; + s->received += size; + return s->received == total; +} + +#endif diff --git a/drivers/gpu/drm/apple/afk.c b/drivers/gpu/drm/apple/afk.c index b97ec275e25ea..8f2bae6581523 100644 --- a/drivers/gpu/drm/apple/afk.c +++ b/drivers/gpu/drm/apple/afk.c @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -42,6 +43,8 @@ enum rbep_msg_type { RBEP_START_ACK = 0x86, RBEP_SEND = 0xa2, RBEP_RECV = 0x85, + RBEP_SPACE_AVAILABLE = 0xa7, + RBEP_WAIT_FOR_SPACE = 0xa8, RBEP_SHUTDOWN = 0xc0, RBEP_SHUTDOWN_ACK = 0xc1, }; @@ -59,6 +62,7 @@ enum rbep_msg_type { #define SEND_WPTR GENMASK(31, 0) static void afk_service_worker(struct work_struct *work); +static bool afk_compact_capable(struct apple_dcp_afkep *ep); static void afk_discard_pending_open(struct apple_epic_service *service); void afk_service_retry(struct apple_dcp_afkep *ep) @@ -110,7 +114,19 @@ struct apple_dcp_afkep *afk_init(struct apple_dcp *dcp, u32 endpoint, goto out_free_afkep; } + /* Compact EPIC replies to firmware calls go out from their own queue. */ + if (afk_compact_capable(afkep)) { + afkep->tx_wq = alloc_ordered_workqueue("apple-dcp-afkep%02x-tx", + WQ_MEM_RECLAIM, endpoint); + if (!afkep->tx_wq) { + destroy_workqueue(afkep->wq); + ret = -ENOMEM; + goto out_free_afkep; + } + } INIT_WORK(&afkep->service_work, afk_service_worker); + INIT_LIST_HEAD(&afkep->compact_cmds); + mutex_init(&afkep->compact_tx_lock); init_completion(&afkep->started); init_completion(&afkep->stopped); spin_lock_init(&afkep->lock); @@ -137,10 +153,17 @@ void afk_quiesce(struct apple_dcp_afkep *afkep) spin_lock_irqsave(&afkep->lock, flags); afkep->stopping = true; spin_unlock_irqrestore(&afkep->lock, flags); + afk_cancel_commands(afkep); if (afkep->wq) { destroy_workqueue(afkep->wq); afkep->wq = NULL; } + if (afkep->tx_wq) { + destroy_workqueue(afkep->tx_wq); + afkep->tx_wq = NULL; + } + kvfree(afkep->compact_rx_data); + afkep->compact_rx_data = NULL; for (i = 0; i < afkep->num_channels; i++) if (afkep->services[i].pending_open) afk_discard_pending_open(&afkep->services[i]); @@ -398,6 +421,822 @@ static bool afk_epic_channel_retired(struct apple_dcp_afkep *ep, u32 channel) return false; } +/* + * Compact EPIC, as the desktop 26.x firmware (T6041, M4 Pro/Max) speaks it + * on its system and auxiliary endpoints. A message is a transport header + * (sequence, flags, interface, total length) followed by the timestamp, a + * sub-header and the payload; messages longer than a fragment arrive in + * fragments that repeat the header. Services announce themselves with + * type 0x11 and are opened with type 0x12; commands, replies and calls + * carry a status/length descriptor whose status the reply echoes. + * + * Only DCP_FIRMWARE_V_26_6_DESKTOP endpoints switch to it, on the first + * compact message they receive. + */ +struct afk_compact_cmd { + struct list_head node; + struct completion done; + u16 channel; + u8 type; + bool service_call; + u16 group; + u32 command; + u32 request_status; + void *output; + size_t output_len; + size_t copied; + u32 retcode; + int status; +}; + +static bool afk_compact_capable(struct apple_dcp_afkep *ep) +{ + return ep->dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP; +} + +static u32 afk_compact_next_status(struct apple_epic_service *service) +{ + unsigned long flags; + u32 status; + + spin_lock_irqsave(&service->lock, flags); + status = (u32)service->cmd_tag++ << 8; + spin_unlock_irqrestore(&service->lock, flags); + + return status; +} + +/* The caller holds ep->lock. */ +static void afk_fail_commands_locked(struct apple_dcp_afkep *ep, int error) +{ + struct afk_compact_cmd *cmd, *tmp; + + list_for_each_entry_safe(cmd, tmp, &ep->compact_cmds, node) { + list_del_init(&cmd->node); + cmd->status = error; + complete(&cmd->done); + } +} + +void afk_cancel_commands(struct apple_dcp_afkep *ep) +{ + unsigned long flags; + + spin_lock_irqsave(&ep->lock, flags); + ep->compact_shutting_down = true; + afk_fail_commands_locked(ep, -ESHUTDOWN); + spin_unlock_irqrestore(&ep->lock, flags); +} + +/* + * A message was sent only in part: the firmware waits for the rest, and a + * new message header would be taken as its continuation. Fail what is in + * flight and refuse further sends; the endpoint stays unusable until it is + * started again. + */ +static void afk_compact_tx_desync(struct apple_dcp_afkep *ep, int error) +{ + unsigned long flags; + + spin_lock_irqsave(&ep->lock, flags); + if (!ep->compact_tx_desync) + dev_err(ep->dcp->dev, + "AFK[ep:%02x]: compact message sent in part (%d); endpoint unusable until restarted\n", + ep->endpoint, error); + ep->compact_tx_desync = true; + afk_fail_commands_locked(ep, -EIO); + spin_unlock_irqrestore(&ep->lock, flags); +} + +static int afk_send_compact_fragment(struct apple_dcp_afkep *ep, u16 intf_id, + const void *body, size_t size, u32 total) +{ + struct epic_compact_hdr *chdr; + struct afk_qe *qhdr; + unsigned long flags; + void *message; + size_t message_size, advance; + u32 rptr, wptr, old_wptr; + int ret = 0; + + if (!ep->txbfr.ready || !ep->txbfr.stride) + return -EIO; + + message_size = offsetof(struct epic_compact_hdr, timestamp) + size; + message = kzalloc(message_size, GFP_KERNEL); + if (!message) + return -ENOMEM; + + chdr = message; + chdr->intf_id = cpu_to_le16(intf_id); + chdr->length = cpu_to_le32(total); + memcpy(message + offsetof(struct epic_compact_hdr, timestamp), body, size); + + spin_lock_irqsave(&ep->lock, flags); + if (ep->compact_shutting_down || ep->stopping) { + ret = -ESHUTDOWN; + goto out_unlock; + } + if (ep->compact_tx_desync) { + ret = -EIO; + goto out_unlock; + } + + dma_rmb(); + rptr = le32_to_cpu(READ_ONCE(*ep->txbfr.rptr)); + wptr = le32_to_cpu(READ_ONCE(*ep->txbfr.wptr)); + old_wptr = wptr; + advance = ALIGN(sizeof(*qhdr) + message_size, ep->txbfr.stride); + + /* Both pointers live in shared memory: never trust them unchecked. */ + if (!afk_ring_ptrs_valid(rptr, wptr, ep->txbfr.bufsz, ep->txbfr.stride) || + ep->txbfr.bufsz - wptr < sizeof(*qhdr)) { + dev_warn_ratelimited(ep->dcp->dev, + "AFK[ep:%02x]: invalid transmit ring pointers: rptr 0x%x wptr 0x%x\n", + ep->endpoint, rptr, wptr); + ret = -EPROTO; + goto out_unlock; + } + if (advance > ep->txbfr.bufsz) { + ret = -EMSGSIZE; + goto out_unlock; + } + if (wptr < rptr && advance >= rptr - wptr) { + ret = -EAGAIN; + goto out_unlock; + } + if (wptr >= rptr) { + bool fits_above = advance < ep->txbfr.bufsz - wptr || + (advance == ep->txbfr.bufsz - wptr && rptr != 0); + + if (!fits_above && advance >= rptr) { + ret = -EAGAIN; + goto out_unlock; + } + } + + chdr->seq = ep->qe_seq++; + qhdr = ep->txbfr.buf + wptr; + qhdr->magic = cpu_to_le32(QE_MAGIC); + qhdr->size = cpu_to_le32(message_size); + qhdr->channel = cpu_to_le32(0); + qhdr->type = cpu_to_le32(0); + wptr += sizeof(*qhdr); + + /* No room for the payload: wrap, repeating the queue entry at 0. */ + if (message_size > ep->txbfr.bufsz - wptr) { + struct afk_qe *qhdr2 = ep->txbfr.buf; + + memcpy(qhdr2, qhdr, sizeof(*qhdr)); + qhdr = qhdr2; + wptr = sizeof(*qhdr); + } + + memcpy(qhdr->data, message, message_size); + wptr = ALIGN(wptr + message_size, ep->txbfr.stride); + if (wptr == ep->txbfr.bufsz) + wptr = 0; + + dma_wmb(); + WRITE_ONCE(*ep->txbfr.wptr, cpu_to_le32(wptr)); + dma_wmb(); + /* The firmware's own client rings with RECV and both pointers. */ + afk_send(ep, FIELD_PREP(RBEP_TYPE, RBEP_RECV) | + ((u64)(rptr & 0xffff) << 32) | + ((u64)(wptr & 0xffff) << 16) | (old_wptr & 0xffff)); + +out_unlock: + spin_unlock_irqrestore(&ep->lock, flags); + kfree(message); + return ret; +} + +/* + * Send one complete message. Fragments carry no offset or tag beyond the + * repeated interface/length and the per-fragment sequence number, so the + * fragments of one message must not interleave with another's. + */ +static int afk_send_compact(struct apple_dcp_afkep *ep, u16 intf_id, u8 type, + u8 category, const void *payload, size_t payload_size, + size_t wire_size) +{ + struct epic_compact_sub_hdr *sub; + size_t total, offset = 0, chunk; + unsigned long deadline; + bool waiting_for_space = false; + void *body; + int ret = 0; + + if (payload_size > wire_size || wire_size > AFK_COMPACT_MESSAGE_MAX - 16) + return -EMSGSIZE; + total = 8 + sizeof(*sub) + wire_size; + body = kvzalloc(total, GFP_KERNEL); + if (!body) + return -ENOMEM; + sub = body + 8; /* the timestamp precedes the sub-header */ + sub->type = type; + sub->category = category; + sub->unk = type == 0x12; + if (payload_size) + memcpy(sub + 1, payload, payload_size); + + mutex_lock(&ep->compact_tx_lock); + deadline = jiffies + msecs_to_jiffies(5000); + while (offset < total) { + chunk = min_t(size_t, total - offset, AFK_COMPACT_FRAGMENT_SIZE); + ret = afk_send_compact_fragment(ep, intf_id, body + offset, + chunk, total); + if (ret == -EAGAIN) { + if (!waiting_for_space) { + afk_send(ep, FIELD_PREP(RBEP_TYPE, RBEP_WAIT_FOR_SPACE) | + (chunk + offsetof(struct epic_compact_hdr, timestamp))); + waiting_for_space = true; + } + if (time_after_eq(jiffies, deadline)) { + ret = -ETIMEDOUT; + break; + } + /* The firmware advances the shared consumer pointer itself. */ + usleep_range(500, 1000); + continue; + } + if (ret) + break; + waiting_for_space = false; + offset += chunk; + } + /* Never start a new header into an unfinished remote assembly. */ + if (ret && offset) + afk_compact_tx_desync(ep, ret); + mutex_unlock(&ep->compact_tx_lock); + kvfree(body); + return ret; +} + +int afk_start_service(struct apple_epic_service *service) +{ + int ret; + + if (!service || !service->enabled) + return -EINVAL; + if (!READ_ONCE(service->ep->compact) || service->started) + return 0; + + ret = afk_send_compact(service->ep, service->channel, 0x12, 0, + NULL, 0, 0); + if (!ret) + service->started = true; + return ret; +} + +/* "prefix:name[:suffix]": match on name; "*" takes every service. */ +static const struct apple_epic_service_ops * +afk_compact_match_service(struct apple_dcp_afkep *ep, const char *name) +{ + const struct apple_epic_service_ops *ops; + const char *service_name; + size_t len; + + if (!name[0] || !ep->ops) + return NULL; + + service_name = strchr(name, ':'); + if (service_name) + service_name++; + else + service_name = name; + + for (ops = ep->ops; ops->name[0]; ops++) { + if (!strcmp(ops->name, "*")) + return ops; + + len = strlen(ops->name); + if (strncmp(ops->name, service_name, len)) + continue; + if (service_name[len] && service_name[len] != ':') + continue; + + return ops; + } + + return NULL; +} + +/* A free slot for a newly announced service, or the one it replaces. */ +static struct apple_epic_service * +afk_compact_slot(struct apple_dcp_afkep *ep, + const struct apple_epic_service_ops *ops, + const char *name, s64 unit) +{ + struct apple_epic_service *service; + unsigned long flags; + bool free; + u32 i; + + /* + * The DPDEV, DPAVSERV and AV services are announced again under a + * fresh interface after every external hotplug, without a teardown + * for the previous one: replace the same logical service in place. + * Other endpoints (DPAVCTRL) announce several services with the same + * name and no unit, so the name is no key there. + */ + if (ep->endpoint >= 0x27 && ep->endpoint <= 0x29) { + for (i = 0; i < ep->num_channels; i++) { + service = &ep->services[i]; + if (service->enabled && service->ops == ops && + service->unit == unit && !strcmp(service->name, name)) { + dev_info(ep->dcp->dev, + "AFK[ep:%02x]: replacing compact service %s interface %u\n", + ep->endpoint, name, service->channel); + afk_remove_service_debugfs(service); + afk_service_retire(service); + if (service->ops->teardown) + service->ops->teardown(service); + return service; + } + } + } + + for (i = 0; i < ep->num_channels; i++) { + service = &ep->services[i]; + spin_lock_irqsave(&service->lock, flags); + free = !service->enabled && !service->users && + bitmap_empty(service->cmd_map, MAX_PENDING_CMDS); + spin_unlock_irqrestore(&service->lock, flags); + if (free) + return service; + } + + if (ep->num_channels >= AFK_MAX_CHANNEL) + return NULL; + return &ep->services[ep->num_channels++]; +} + +static void afk_recv_handle_compact_init(struct apple_dcp_afkep *ep, + u16 channel, const void *payload, + size_t payload_size) +{ + char wire_name[33], service_name[33]; + const struct apple_epic_service_ops *ops; + struct apple_epic_service *service; + unsigned long flags; + const char *name; + char *unit_suffix; + s64 unit = -1; + int ret; + + if (payload_size < 32) { + dev_err(ep->dcp->dev, + "AFK[ep:%02x]: short compact service announcement\n", + ep->endpoint); + return; + } + if (afk_epic_find_service(ep, channel)) { + dev_warn(ep->dcp->dev, + "AFK[ep:%02x]: duplicate compact service on interface %u\n", + ep->endpoint, channel); + return; + } + memcpy(wire_name, payload, 32); + wire_name[32] = '\0'; + name = strchr(wire_name, ':'); + name = name ? name + 1 : wire_name; + strscpy(service_name, name, sizeof(service_name)); + + unit_suffix = strrchr(service_name, ':'); + if (unit_suffix && !kstrtos64(unit_suffix + 1, 10, &unit)) + *unit_suffix = '\0'; + else + unit = -1; + + ops = afk_compact_match_service(ep, wire_name); + if (!ops) { + dev_err(ep->dcp->dev, + "AFK[ep:%02x]: unable to match compact service %s on interface %u\n", + ep->endpoint, wire_name, channel); + return; + } + + service = afk_compact_slot(ep, ops, service_name, unit); + if (!service) { + dev_err(ep->dcp->dev, + "AFK[ep:%02x]: too many enabled compact services\n", + ep->endpoint); + return; + } + + spin_lock_irqsave(&service->lock, flags); + service->ops = ops; + service->ep = ep; + service->channel = channel; + service->cookie = NULL; + service->cmd_tag = 0; + strscpy(service->name, service_name, sizeof(service->name)); + service->unit = unit; + service->defer_start = false; + service->started = false; + service->torndown = false; + /* A service with an admission step is published once admitted. */ + service->enabled = !ops->prepare; + spin_unlock_irqrestore(&service->lock, flags); + + if (ops->prepare) { + ret = ops->prepare(service, service_name, service_name, unit); + if (ret) { + dev_info(ep->dcp->dev, + "AFK[ep:%02x]: compact service %s on interface %u not admitted: %d\n", + ep->endpoint, wire_name, channel, ret); + afk_service_discard(service); + return; + } + afk_service_publish(service); + } + + if (ops->init) + ops->init(service, service_name, service_name, unit); + + dev_info(ep->dcp->dev, + "AFK[ep:%02x]: new compact service %s on interface %u%s\n", + ep->endpoint, wire_name, channel, + service->defer_start ? " (deferred)" : ""); + afk_populate_service_debugfs(service); + + if (!service->defer_start) { + ret = afk_start_service(service); + if (ret) + dev_err(ep->dcp->dev, + "AFK[ep:%02x]: failed to start compact service %u: %d\n", + ep->endpoint, channel, ret); + } +} + +static void afk_recv_handle_compact_reply(struct apple_dcp_afkep *ep, + u16 channel, u8 type, + const void *payload, + size_t payload_size) +{ + const struct epic_compact_desc *desc = payload; + const struct epic_service_call *call = NULL; + struct afk_compact_cmd *pending, *match = NULL; + unsigned long flags; + size_t copy_size; + + if (payload_size < sizeof(*desc)) { + dev_err(ep->dcp->dev, + "AFK[ep:%02x]: compact reply is missing its descriptor\n", + ep->endpoint); + return; + } + + if (payload_size >= sizeof(*desc) + sizeof(*call)) { + call = (const void *)(desc + 1); + if (le32_to_cpu(call->magic) != EPIC_SERVICE_CALL_MAGIC) + call = NULL; + } + + spin_lock_irqsave(&ep->lock, flags); + list_for_each_entry(pending, &ep->compact_cmds, node) { + if (pending->channel != channel || pending->type != type) + continue; + if (pending->request_status != le32_to_cpu(desc->status)) + continue; + if (pending->service_call && + (!call || pending->group != le16_to_cpu(call->group) || + pending->command != le32_to_cpu(call->command))) + continue; + match = pending; + break; + } + if (!match) { + spin_unlock_irqrestore(&ep->lock, flags); + dev_warn(ep->dcp->dev, + "AFK[ep:%02x]: unexpected compact reply %02x on interface %u\n", + ep->endpoint, type, channel); + return; + } + + list_del_init(&match->node); + match->retcode = le32_to_cpu(desc->status); + match->status = 0; + if (call && le32_to_cpu(call->data_len) > + payload_size - sizeof(*desc) - sizeof(*call)) { + match->status = -EMSGSIZE; + } else { + copy_size = min(match->output_len, payload_size - sizeof(*desc)); + if (copy_size && match->output) + memcpy(match->output, desc + 1, copy_size); + match->copied = copy_size; + } + complete(&match->done); + spin_unlock_irqrestore(&ep->lock, flags); +} + +/* + * A reply to a firmware call, sent from the endpoint's transmit queue: a + * send may wait for ring space, which the receive worker frees by draining + * the other direction. + */ +struct afk_compact_reply { + struct work_struct work; + struct apple_dcp_afkep *ep; + u16 channel; + size_t size; + u8 data[]; +}; + +static void afk_compact_reply_worker(struct work_struct *work) +{ + struct afk_compact_reply *r = container_of(work, struct afk_compact_reply, work); + int ret; + + ret = afk_send_compact(r->ep, r->channel, EPIC_SUBTYPE_STD_SERVICE, 2, + r->data, r->size, r->size); + if (ret) + dev_err(r->ep->dcp->dev, + "AFK[ep:%02x]: failed to reply to compact service call: %d\n", + r->ep->endpoint, ret); + kvfree(r); +} + +static void afk_recv_handle_compact_call(struct apple_dcp_afkep *ep, + u16 channel, const void *payload, + size_t payload_size) +{ + const struct epic_compact_desc *request_desc = payload; + const struct epic_service_call *call; + struct epic_compact_desc *reply_desc; + struct epic_service_call *reply_call; + struct apple_epic_service *service; + size_t inner_size, call_size; + struct afk_compact_reply *r; + u16 group; + u32 command; + void *reply; + int ret; + + if (payload_size < sizeof(*request_desc) + sizeof(*call) || !ep->tx_wq) + return; + + call = payload + sizeof(*request_desc); + inner_size = payload_size - sizeof(*request_desc); + call_size = le32_to_cpu(call->data_len); + if (le32_to_cpu(call->magic) != EPIC_SERVICE_CALL_MAGIC || + inner_size < sizeof(*call) + call_size) { + dev_err(ep->dcp->dev, + "AFK[ep:%02x]: malformed compact service call on interface %u\n", + ep->endpoint, channel); + return; + } + group = le16_to_cpu(call->group); + command = le32_to_cpu(call->command); + + r = kvzalloc(struct_size(r, data, payload_size), GFP_KERNEL); + if (!r) + return; + INIT_WORK(&r->work, afk_compact_reply_worker); + r->ep = ep; + r->channel = channel; + r->size = payload_size; + reply = r->data; + reply_desc = reply; + reply_desc->status = request_desc->status; + reply_call = reply + sizeof(*reply_desc); + memcpy(reply_call, call, sizeof(*call)); + + service = afk_service_get(afk_epic_find_service(ep, channel)); + if (service && service->ops->call_v2) + ret = service->ops->call_v2(service, group, command, call + 1, + call_size, reply_call + 1, call_size); + else if (service && service->ops->call && !group) + ret = service->ops->call(service, command, call + 1, call_size, + reply_call + 1, call_size); + else + ret = -EOPNOTSUPP; + if (service) + afk_service_put(service); + if (ret == -EOPNOTSUPP) + dev_warn(ep->dcp->dev, + "AFK[ep:%02x]: no handler for compact service %u call %u:%u\n", + ep->endpoint, channel, group, command); + if (ret) + reply_desc->status = cpu_to_le32(ret); + + queue_work(ep->tx_wq, &r->work); +} + +static void afk_recv_compact_message(struct apple_dcp_afkep *ep, u16 intf_id, + const void *body, size_t size) +{ + const struct epic_compact_sub_hdr *sub = body + 8; + struct apple_epic_service *service; + const void *payload = sub + 1; + size_t payload_size = size - 8 - sizeof(*sub); + + if (sub->category == 0 && sub->type == 0x11) { + afk_recv_handle_compact_init(ep, intf_id, payload, payload_size); + return; + } + if (sub->category == 1 && sub->type == EPIC_SUBTYPE_STD_SERVICE) { + afk_recv_handle_compact_call(ep, intf_id, payload, payload_size); + return; + } + if (sub->category == 2) { + afk_recv_handle_compact_reply(ep, intf_id, sub->type, payload, + payload_size); + return; + } + if (sub->category == 0) { + /* A standard-service report carries a descriptor first. */ + if (sub->type == EPIC_SUBTYPE_STD_SERVICE && + payload_size >= sizeof(struct epic_compact_desc)) { + payload += sizeof(struct epic_compact_desc); + payload_size -= sizeof(struct epic_compact_desc); + } + service = afk_service_get(afk_epic_find_service(ep, intf_id)); + if (service && service->ops->report) + service->ops->report(service, sub->type, payload, + payload_size); + if (service) + afk_service_put(service); + return; + } + + dev_dbg(ep->dcp->dev, + "AFK[ep:%02x]: unhandled compact message %02x:%02x on interface %u\n", + ep->endpoint, sub->category, sub->type, intf_id); +} + +/* True if @data was a compact message (or fragment) and has been handled. */ +static bool afk_recv_handle_compact(struct apple_dcp_afkep *ep, u32 channel, + u32 type, const void *data, size_t data_size) +{ + const struct epic_compact_hdr *hdr = data; + const size_t header_size = offsetof(struct epic_compact_hdr, timestamp); + struct afk_fragment_state *s = &ep->compact_rx; + const struct epic_compact_sub_hdr *sub; + u32 offset, total; + u16 intf_id; + int ret; + + if (!afk_compact_capable(ep) || channel || type || + data_size < header_size) + return false; + total = le32_to_cpu(hdr->length); + intf_id = le16_to_cpu(hdr->intf_id); +retry: + offset = s->received; + /* Eight-byte empty control messages occur in the firmware's traffic. */ + if (!offset && total == 8 && data_size == header_size + 8) + return true; + /* A message starts with its timestamp and a plausible sub-header. */ + if (!offset) { + sub = data + header_size + 8; + if (data_size < header_size + 8 + sizeof(*sub) || sub->category > 2) { + if (!READ_ONCE(ep->compact)) + return false; + ret = -EPROTO; + goto discard; + } + } + ret = afk_fragment_accept(s, intf_id, hdr->seq, total, + data_size - header_size); + if (ret < 0) { + if (!READ_ONCE(ep->compact) && !offset) + return false; + /* + * A fragment that does not continue the message in assembly + * may start the next one, e.g. after a firmware restart: drop + * the partial message and take this one as a fresh start. + */ + if (offset) { + dev_err_ratelimited(ep->dcp->dev, + "AFK[ep:%02x]: compact message on interface %u abandoned at %u of %u bytes: %d\n", + ep->endpoint, s->intf_id, offset, s->total, ret); + kvfree(ep->compact_rx_data); + ep->compact_rx_data = NULL; + memset(s, 0, sizeof(*s)); + goto retry; + } + goto discard; + } + WRITE_ONCE(ep->compact, true); + if (!offset && ret == 1) { + memset(s, 0, sizeof(*s)); + afk_recv_compact_message(ep, intf_id, data + header_size, total); + return true; + } + if (!offset) { + ep->compact_rx_data = kvzalloc(total, GFP_KERNEL); + if (!ep->compact_rx_data) { + ret = -ENOMEM; + goto discard; + } + } + memcpy(ep->compact_rx_data + offset, data + header_size, + data_size - header_size); + if (ret == 1) { + void *message = ep->compact_rx_data; + + ep->compact_rx_data = NULL; + memset(s, 0, sizeof(*s)); + afk_recv_compact_message(ep, intf_id, message, total); + kvfree(message); + } + return true; + +discard: + dev_err_ratelimited(ep->dcp->dev, + "AFK[ep:%02x]: invalid compact fragment intf=%u seq=%u total=%u offset=%u: %d\n", + ep->endpoint, intf_id, hdr->seq, total, offset, ret); + kvfree(ep->compact_rx_data); + ep->compact_rx_data = NULL; + memset(s, 0, sizeof(*s)); + return true; +} + +/* + * A command on a compact endpoint: the payload behind a descriptor whose + * status the reply echoes. @retcode is the reply's status. + */ +static int afk_send_compact_command(struct apple_epic_service *service, u8 type, + const void *payload, size_t payload_len, + void *output, size_t output_len, + u32 request_status, u32 *retcode, + size_t *copied, unsigned long timeout) +{ + struct apple_dcp_afkep *ep = service->ep; + const struct epic_service_call *call = payload; + struct afk_compact_cmd pending; + struct epic_compact_desc *desc; + size_t wire_size = max(payload_len, output_len); + unsigned long waited, flags; + void *wrapped; + int ret; + + wrapped = kvzalloc(sizeof(*desc) + wire_size, GFP_KERNEL); + if (!wrapped) + return -ENOMEM; + desc = wrapped; + desc->status = cpu_to_le32(request_status); + desc->length = cpu_to_le32(wire_size); + if (payload_len) + memcpy(desc + 1, payload, payload_len); + + memset(&pending, 0, sizeof(pending)); + INIT_LIST_HEAD(&pending.node); + init_completion(&pending.done); + pending.channel = service->channel; + pending.type = type; + pending.request_status = request_status; + pending.output = output; + pending.output_len = output_len; + pending.status = -EINPROGRESS; + if (payload_len >= sizeof(*call) && + le32_to_cpu(call->magic) == EPIC_SERVICE_CALL_MAGIC) { + pending.service_call = true; + pending.group = le16_to_cpu(call->group); + pending.command = le32_to_cpu(call->command); + } + + spin_lock_irqsave(&ep->lock, flags); + if (ep->compact_shutting_down || ep->stopping) { + spin_unlock_irqrestore(&ep->lock, flags); + ret = -ESHUTDOWN; + goto out; + } + list_add_tail(&pending.node, &ep->compact_cmds); + spin_unlock_irqrestore(&ep->lock, flags); + + ret = afk_send_compact(ep, service->channel, type, 1, wrapped, + sizeof(*desc) + payload_len, + sizeof(*desc) + wire_size); + if (ret) + goto remove_pending; + + waited = wait_for_completion_timeout(&pending.done, timeout); + spin_lock_irqsave(&ep->lock, flags); + if (!waited && !list_empty(&pending.node)) { + list_del_init(&pending.node); + pending.status = -ETIMEDOUT; + } + ret = pending.status; + if (retcode) + *retcode = pending.retcode; + if (copied) + *copied = pending.copied; + spin_unlock_irqrestore(&ep->lock, flags); + goto out; + +remove_pending: + spin_lock_irqsave(&ep->lock, flags); + if (!list_empty(&pending.node)) + list_del_init(&pending.node); + spin_unlock_irqrestore(&ep->lock, flags); +out: + kvfree(wrapped); + return ret; +} + static int afk_send_epic_token(struct apple_dcp_afkep *ep, u32 channel, u16 tag, enum epic_type etype, enum epic_category ecat, u8 stype, const void *payload, size_t payload_len, u64 token); @@ -1145,6 +1984,9 @@ static void afk_recv_handle(struct apple_dcp_afkep *ep, u32 channel, u32 type, u8 *payload; size_t payload_size; + if (afk_recv_handle_compact(ep, channel, type, data, data_size)) + return; + if (ep->wire_v2) { afk_v2_receive(ep, channel, type, data, data_size); return; @@ -1423,7 +2265,26 @@ static void afk_receive_message_worker(struct work_struct *work_) afk_recv_drain(work->ep); break; + case RBEP_SPACE_AVAILABLE: + /* The compact sender polls the shared consumer pointer. */ + if (!afk_compact_capable(work->ep)) + goto unknown; + break; + + case RBEP_WAIT_FOR_SPACE: + /* + * Compact endpoints ask for a wakeup when their ring fills: + * drain and publish rptr before acknowledging. + */ + if (!afk_compact_capable(work->ep)) + goto unknown; + afk_recv_drain(work->ep); + dma_wmb(); + afk_send(work->ep, FIELD_PREP(RBEP_TYPE, RBEP_SPACE_AVAILABLE)); + break; + default: +unknown: dev_err(work->ep->dcp->dev, "Received unknown AFK message type: 0x%x\n", type); } @@ -1793,6 +2654,24 @@ static int afk_send_command_common(struct apple_epic_service *service, u8 type, if (!service) return -ENODEV; ep = service->ep; + if (READ_ONCE(ep->compact)) { + u32 request_status = afk_compact_next_status(service); + size_t copied = 0; + u32 status = 0; + + ret = afk_send_compact_command(service, type, payload, payload_len, + output, output_len, request_status, + &status, &copied, + msecs_to_jiffies(timeout_ms)); + /* Success is the reply echoing the request's status. */ + if (!ret && retcode) + *retcode = status == request_status ? 0 : (status ?: ~0U); + if (!ret && received) + *received = copied; + if (!ret && reply_len) + *reply_len = copied; + goto err_put_service; + } /* AFKv2 replies report their length through reply_length instead. */ if (reply_len && ep->wire_v2) { ret = -EOPNOTSUPP; diff --git a/drivers/gpu/drm/apple/afk.h b/drivers/gpu/drm/apple/afk.h index 2b6609abe361a..291ee12402292 100644 --- a/drivers/gpu/drm/apple/afk.h +++ b/drivers/gpu/drm/apple/afk.h @@ -13,6 +13,7 @@ #include #include #include +#include #include #include "dcp.h" @@ -72,6 +73,12 @@ struct apple_epic_service { const char *pending_name, *pending_class; s64 pending_unit; + /* Compact EPIC (desktop 26.x): the announced identity and OPEN state. */ + char name[33]; + s64 unit; + bool defer_start; + bool started; + struct { struct dentry *entry; u8 *scratch; @@ -379,6 +386,35 @@ struct epic_sub_hdr_h17p { #define EPIC_SUBTYPE_ANNOUNCE_H17P 0x11 +/* + * Compact EPIC, as the desktop 26.x firmware (T6041) speaks it on its + * auxiliary endpoints: an 8-byte transport header, the 8-byte timestamp, + * an 8-byte sub-header and, for commands and replies, a status/length + * descriptor. Ring entries carry channel and type 0. + */ +struct epic_compact_hdr { + u8 seq; + u8 flags; + __le16 intf_id; + __le32 length; + __le64 timestamp; +} __packed; + +struct epic_compact_sub_hdr { + u8 type; + u8 category; + u8 unk; + u8 flags; + __le32 reserved; +} __packed; + +struct epic_compact_desc { + __le32 status; + __le32 length; +} __packed; + +#include "afk-fragment.h" + struct epic_cmd { __le32 retcode; __le64 rxbuf; @@ -460,6 +496,18 @@ struct apple_dcp_afkep { u16 qe_seq; bool stopping; /* lock: no new receive work after shutdown. */ + /* Compact EPIC, detected from the first compact message (desktop 26.x). */ + bool compact; + struct list_head compact_cmds; /* lock */ + bool compact_shutting_down; /* lock */ + /* A message went out in part; no further sends (lock). */ + bool compact_tx_desync; + /* Replies to firmware calls, see afk_compact_reply_worker(). */ + struct workqueue_struct *tx_wq; + struct mutex compact_tx_lock; + struct afk_fragment_state compact_rx; + void *compact_rx_data; + const struct apple_epic_service_ops *ops; struct apple_epic_service services[AFK_MAX_CHANNEL]; u32 num_channels; @@ -474,6 +522,8 @@ struct apple_dcp_afkep *afk_init(struct apple_dcp *dcp, u32 endpoint, int afk_v2_copy_owned(struct apple_epic_service *service, u64 dva, void *buffer, size_t size, bool to_peer); int afk_start(struct apple_dcp_afkep *ep); +int afk_start_service(struct apple_epic_service *service); +void afk_cancel_commands(struct apple_dcp_afkep *ep); void afk_shutdown(struct apple_dcp_afkep *ep); void afk_quiesce(struct apple_dcp_afkep *ep); int afk_receive_message(struct apple_dcp_afkep *ep, u64 message); From c7a3e09a51df3868db6ba5168ba8f121e3990f0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Bal=C3=A1zsy?= Date: Sun, 25 Oct 2026 12:59:20 +0100 Subject: [PATCH 07/12] drm/apple: drive the T6041 internal panel on desktop 26.6 firmware MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Match apple,t6041-dcp and apple,t6041-dcpext and drive the internal DCP of the M4 Pro/Max (J616s) through the desktop 26.6 IOMFB instance. Probe accepts a T6041 processor only with apple,firmware-version and apple,firmware-compat both 26.6.2, and the internal one only if its register windows, iommus, mboxes and piodma match the ADT. The clock is optional there, and without apple,max-brightness the backlight maximum defaults to the panel's 600 nits. The firmware does not tolerate being left running with its endpoints unserviced, so bind sets the coprocessor running and dcp_start() wakes RTKit once every handler is in place. It starts the system endpoint, the IOMFB transport and the auxiliary endpoints the firmware advertises, routes their messages, and binds the IOMFB shared memory last. An auxiliary endpoint that fails to start is skipped. Before the boot framebuffer is evicted, the panel goes through a takeover trial: DCP ready, modes known, panel connected, power-on and one mode set, boot-like mode first. If it fails, the DCP is left running, the DRM device is kept, shutdown and resume leave the DCP alone and suspend is refused, and the boot framebuffer keeps the screen. The T6041 external processors stay out of the DRM device unless t6041_dcpext=1, and then as unusable pipes. Co-developed-by: Cody Ho Signed-off-by: Cody Ho Co-developed-by: Jonathan Goyvaerts Signed-off-by: Jonathan Goyvaerts Signed-off-by: František Balázsy --- drivers/gpu/drm/apple/Kconfig | 2 + drivers/gpu/drm/apple/apple_drv.c | 57 ++- drivers/gpu/drm/apple/dcp-internal.h | 25 ++ drivers/gpu/drm/apple/dcp.c | 548 ++++++++++++++++++++++++- drivers/gpu/drm/apple/dcp.h | 5 + drivers/gpu/drm/apple/iomfb.c | 158 ++++++- drivers/gpu/drm/apple/iomfb_template.c | 13 +- 7 files changed, 787 insertions(+), 21 deletions(-) diff --git a/drivers/gpu/drm/apple/Kconfig b/drivers/gpu/drm/apple/Kconfig index 94e0066cb5bb1..9685682f2a87e 100644 --- a/drivers/gpu/drm/apple/Kconfig +++ b/drivers/gpu/drm/apple/Kconfig @@ -14,6 +14,8 @@ config DRM_APPLE select DRM_GEM_SHMEM_HELPER select DRM_GEM_DMA_HELPER select VIDEOMODE_HELPERS + # write-combined maps of firmware-placed carveouts (desktop 26.x) + select VMAP_PFN depends on BACKLIGHT_CLASS_DEVICE select MULTIPLEXER select DRM_APPLE_FABRIC_CORE diff --git a/drivers/gpu/drm/apple/apple_drv.c b/drivers/gpu/drm/apple/apple_drv.c index d8f2fb1ed300d..5c3a4ab02f1e9 100644 --- a/drivers/gpu/drm/apple/apple_drv.c +++ b/drivers/gpu/drm/apple/apple_drv.c @@ -54,6 +54,11 @@ struct apple_drm_private { struct drm_device drm; + /* + * A T6041 panel refused the takeover trial: the boot framebuffer keeps + * the screen, the DRM device stays unregistered and its DCPs running. + */ + bool aborted; }; DEFINE_DRM_GEM_DMA_FOPS(apple_fops); @@ -898,7 +903,10 @@ static bool apple_dcp_usable(struct device_node *np) (!of_device_is_compatible(np, "apple,t6030-dcpext") || dcp_t6030_ext_native(np)) && (!of_device_is_compatible(np, "apple,t8140-dcpext") || - dcp_h17p_external_supported(np)); + dcp_h17p_external_supported(np)) && + /* Not driven yet; never let it hold up the panel's device. */ + (!of_device_is_compatible(np, "apple,t6041-dcpext") || + dcp_t6041_ext_enabled()); } static void apple_drm_quiesce_connectors(struct drm_device *drm) @@ -998,9 +1006,14 @@ static int apple_drm_init_dcp(struct device *dev) } /* - * Starting DPTX might take some time. + * Starting DPTX might take some time. The T6041 panel runs its + * client-open, main-display and power chain inside the start: give it + * as long as that takes. */ timeout = get_jiffies_64() + msecs_to_jiffies(3000); + for (i = 0; i < num_dcp; i++) + if (of_device_is_compatible(dcp[i]->dev.of_node, "apple,t6041-dcp")) + timeout = get_jiffies_64() + msecs_to_jiffies(10000); for (i = 0; i < num_dcp; ++i) { u64 jiffies = get_jiffies_64(); @@ -1119,6 +1132,34 @@ static int apple_drm_init(struct device *dev) * swap. Hold the modeset locks across activation so an early userspace * commit cannot race the retained startup surface. */ + /* + * T6041 panel: everything that can still be refused is tried while + * the boot framebuffer owns the screen; past the eviction there is no + * way back. On refusal leave the DCP running (its shutdown sequence + * can crash it and blank the panel), pin the module and keep the boot + * framebuffer. + */ + drm_for_each_crtc(crtc, &apple->drm) { + const char *reason = "none"; + + ret = dcp_takeover_trial(to_apple_crtc(crtc)->dcp, &reason); + if (!ret) + continue; + dev_err(dev, "display takeover refused (%s: %d); the boot framebuffer keeps the screen\n", + reason, ret); + /* + * The DCPs keep running and keep their pipelines: pin the + * module and the DRM device their CRTCs and connectors live + * in, and keep them out of shutdown and sleep. + */ + __module_get(THIS_MODULE); + drm_dev_get(&apple->drm); + drm_for_each_crtc(crtc, &apple->drm) + dcp_takeover_aborted(to_apple_crtc(crtc)->dcp); + apple->aborted = true; + return 0; + } + if (retained_modeset) drm_modeset_lock_all(&apple->drm); fb_size = fb_r.end - fb_r.start + 1; @@ -1148,6 +1189,10 @@ static int apple_drm_init(struct device *dev) /* the fabric keeps order only once registered: catch up on probe */ dcp_typec_reorder(); + /* A panel that publishes no level registers its backlight now. */ + drm_for_each_crtc(crtc, &apple->drm) + dcp_backlight_takeover_registered(to_apple_crtc(crtc)->dcp); + drm_client_setup_with_fourcc(&apple->drm, DRM_FORMAT_XRGB8888); return 0; @@ -1166,6 +1211,10 @@ static void apple_drm_uninit(struct device *dev) { struct apple_drm_private *apple = dev_get_drvdata(dev); + /* An aborted takeover leaves its DCPs running until the reboot. */ + if (apple->aborted) + return; + apple_drm_quiesce_connectors(&apple->drm); drm_dev_unplug(&apple->drm); drm_atomic_helper_shutdown(&apple->drm); @@ -1275,7 +1324,7 @@ static int apple_platform_suspend(struct device *dev) { struct apple_drm_private *apple = dev_get_drvdata(dev); - if (apple) + if (apple && !apple->aborted) return drm_mode_config_helper_suspend(&apple->drm); return 0; @@ -1303,7 +1352,7 @@ static int apple_platform_resume(struct device *dev) struct apple_drm_private *apple = dev_get_drvdata(dev); u32 dropped = 0; - if (apple) { + if (apple && !apple->aborted) { if (apple->drm.mode_config.suspend_state) dropped = apple_drm_drop_stale_crtcs(apple->drm.mode_config.suspend_state); /* Have userspace set the mode of a CRTC resumed disabled again. */ diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index 0d32888ec371d..2973f679db3c7 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -87,6 +87,11 @@ enum { * publishes the port service used by DPTX 0x2a. */ DPAV_CTRL_ENDPOINT = 0x24, + /* Desktop 26.x (T6041) auxiliary endpoints, see dcp_start_desktop(). */ + EPIC25_ENDPOINT = 0x25, + DPSAC_ENDPOINT = 0x26, + DPDEV_ENDPOINT = 0x27, + EPIC2C_ENDPOINT = 0x2c, DPAVSERV_ENDPOINT = 0x28, DPAVSERV_ENDPOINT_V2 = 0x2a, AV_ENDPOINT = 0x29, @@ -240,6 +245,26 @@ struct dcp_desktop_state { struct iomfb_gamma_table_req *gamma_fw, *gamma_out; struct drm_color_lut gamma_lut[IOMFB_GAMMA_ENTRIES]; bool gamma_fw_read, gamma_user, gamma_pending, gamma_unusable; + /* + * Endpoints 0x20..0x3f this processor's start brought up beside the + * system endpoint (bit = endpoint - 0x20), and those it skipped. + */ + struct apple_dcp_afkep *graph[0x20]; + u32 graph_started, graph_skipped; + /* DPAVCTRL (0x24): its two AV controllers and the deferred service. */ + struct apple_epic_service *dpavctrl_av_controller[2]; + struct apple_epic_service *dpavctrl_late_service; + struct completion dpavctrl_ready; + /* + * Takeover trial: power-on and a mode set ran before the boot + * framebuffer was evicted; the first atomic enable reuses them. + */ + bool trial_powered, trial_modeset_ok; + u32 trial_timing_mode_id, trial_color_mode_id; + /* Result of the last power-on: 0 ok, 1 timeout, 2 failed. */ + u8 poweron_result; + /* The takeover was refused: leave the DCP alone until the reboot. */ + bool takeover_aborted; }; /* TODO: move IOMFB members to its own struct */ diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 2557bda0cafaa..859ac7fc6c155 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -34,6 +34,7 @@ #include #include #include +#include #include #include @@ -214,6 +215,191 @@ static bool unstable_edid = true; module_param(unstable_edid, bool, 0644); MODULE_PARM_DESC(unstable_edid, "Enable unstable EDID retrival support"); +/* + * T6041 (M4 Pro/Max) external display processors are not driven yet. They + * stay out of the display's DRM device unless asked for; see + * dcp_t6041_ext_enabled(). + */ +static bool t6041_dcpext; +module_param(t6041_dcpext, bool, 0444); +MODULE_PARM_DESC(t6041_dcpext, + "T6041: let the external display processors join the DRM device (default 0; not driven yet, development only)"); + +bool dcp_t6041_ext_enabled(void) +{ + return t6041_dcpext; +} + +/* + * The backlight maximum of a desktop 26.x panel whose node has no + * apple,max-brightness: the J616s panel's user-accessible SDR maximum (ADT + * user-accessible-max-nits, 0x02580000 in 16.16). + */ +#define DCP_DESKTOP_MAX_NITS 600 + +static bool dcp_desktop_external(struct apple_dcp *dcp) +{ + return of_device_is_compatible(dcp->dev->of_node, "apple,t6041-dcpext"); +} + +/* + * The register map of the T6041 internal DCP, from the macOS ADT capture + * (/arm-io/dcp reg[0], /arm-io/disp0 reg[0..3]). The firmware maps these + * windows by index, so the device tree must describe exactly these. + */ +static const struct { + const char *name; + u64 start; + u64 size; +} t6041_dcp_regs[] = { + { "coproc", 0x402e00000ULL, 0x4000 }, + { "disp-0", 0x400000000ULL, 0x4000000 }, + { "disp-1", 0x402240000ULL, 0x10000 }, + { "disp-2", 0x4023a8000ULL, 0x4000 }, + { "disp-3", 0x402800000ULL, 0x800000 }, +}; + +/* Refuse to touch the coprocessor on any mismatch with the captured ADT. */ +static int dcp_validate_t6041_dt(struct device *dev) +{ + struct platform_device *pdev = to_platform_device(dev); + struct device_node *piodma __free(device_node) = + of_get_child_by_name(dev->of_node, "piodma"); + int i; + + if (!of_device_is_compatible(dev->of_node, "apple,t6041-dcp")) + return 0; + + if (pdev->num_resources != ARRAY_SIZE(t6041_dcp_regs)) + return dev_err_probe(dev, -ENODEV, + "T6041 DCP: %u register windows, the ADT has %zu\n", + pdev->num_resources, ARRAY_SIZE(t6041_dcp_regs)); + + for (i = 0; i < ARRAY_SIZE(t6041_dcp_regs); i++) { + struct resource *r = platform_get_resource(pdev, IORESOURCE_MEM, i); + + if (!r || r->start != t6041_dcp_regs[i].start || + resource_size(r) != t6041_dcp_regs[i].size || + of_property_match_string(dev->of_node, "reg-names", + t6041_dcp_regs[i].name) != i) + return dev_err_probe(dev, -ENODEV, + "T6041 DCP: register window %d (%s) differs from the ADT\n", + i, t6041_dcp_regs[i].name); + } + + if (!of_property_present(dev->of_node, "iommus") || + !of_property_present(dev->of_node, "mboxes") || !piodma) + return dev_err_probe(dev, -ENODEV, + "T6041 DCP: iommus, mboxes or piodma missing\n"); + + return 0; +} + +/* + * Map a buffer the desktop 26.x firmware placed itself write-combined. + * It may be accessed through a realtime path, and a write-back alias + * creates AMCC directory state that can fault when the DCP then accesses + * the same memory. The buffers may lie in RAM or in carveouts without + * struct pages. + */ +static int dcp_vmap_wc(phys_addr_t phys, size_t size, + void __iomem **iomem, void **map_base) +{ + unsigned long offset = offset_in_page(phys); + unsigned long first_pfn = PHYS_PFN(phys); + phys_addr_t last; + size_t span, nr_pages; + unsigned int count, i; + void *base; + + if (!size) + return -EINVAL; + if (check_add_overflow(phys, (phys_addr_t)size - 1, &last) || + check_add_overflow(size, (size_t)offset, &span)) + return -EOVERFLOW; + if (last > PHYS_MASK) + return -EOVERFLOW; + nr_pages = (span - 1) / PAGE_SIZE + 1; + if (nr_pages > UINT_MAX) + return -E2BIG; + count = nr_pages; + + if (pfn_valid(first_pfn)) { + struct page **pages = kmalloc_array(count, sizeof(*pages), GFP_KERNEL); + + if (!pages) + return -ENOMEM; + for (i = 0; i < count; i++) { + if (!pfn_valid(first_pfn + i)) { + kfree(pages); + return -EINVAL; + } + pages[i] = pfn_to_page(first_pfn + i); + } + base = vmap(pages, count, VM_MAP, pgprot_writecombine(PAGE_KERNEL)); + kfree(pages); + } else { + unsigned long *pfns = kmalloc_array(count, sizeof(*pfns), GFP_KERNEL); + + if (!pfns) + return -ENOMEM; + for (i = 0; i < count; i++) { + if (pfn_valid(first_pfn + i)) { + kfree(pfns); + return -EINVAL; + } + pfns[i] = first_pfn + i; + } + base = vmap_pfn(pfns, count, pgprot_writecombine(PAGE_KERNEL)); + kfree(pfns); + } + if (!base) + return -ENOMEM; + + *map_base = base; + *iomem = (void __iomem *)(base + offset); + return 0; +} + +static int dcp_check_iova_contiguous(struct iommu_domain *domain, + dma_addr_t iova, size_t size, + phys_addr_t phys); + +static int dcp_desktop_shmem_setup(struct apple_dcp *dcp, + struct apple_rtkit_shmem *bfr) +{ + struct iommu_domain *domain = iommu_get_domain_for_dev(dcp->dev); + phys_addr_t phy_addr; + int ret; + + if (!domain) + return -ENODEV; + if (!bfr->size) + return -EINVAL; + if (bfr->size - 1 > (dma_addr_t)-1 - bfr->iova) + return -EOVERFLOW; + phy_addr = iommu_iova_to_phys(domain, bfr->iova); + if (!phy_addr) + return -EINVAL; + if (offset_in_page(phy_addr) != offset_in_page(bfr->iova)) + return -EINVAL; + if (phy_addr > PHYS_MASK || bfr->size - 1 > PHYS_MASK - phy_addr) + return -EOVERFLOW; + /* The CPU alias is contiguous: check the whole mapping. */ + ret = dcp_check_iova_contiguous(domain, bfr->iova, bfr->size, phy_addr); + if (ret) + return ret; + + ret = dcp_vmap_wc(phy_addr, bfr->size, &bfr->iomem, &bfr->private); + if (ret) + return ret; + + bfr->is_mapped = true; + dev_dbg(dcp->dev, "shmem_setup: iova %pad -> pa %pa\n", &bfr->iova, + &phy_addr); + return 0; +} + static void disconnected_hpd_event(struct apple_connector *connector); bool dcp_is_external_h17p(struct apple_dcp *dcp) @@ -677,6 +863,15 @@ static void dcp_delayed_vblank(struct work_struct *work) static struct apple_dcp_afkep *dcp_afkep(struct apple_dcp *dcp, u8 endpoint) { + /* The auxiliary endpoints of a desktop 26.x processor. */ + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && + endpoint >= 0x20 && endpoint < 0x40) { + struct apple_dcp_afkep *ep = + READ_ONCE(dcp->desktop->graph[endpoint - 0x20]); + + if (ep) + return ep; + } /* DPAV 0x2a on H17P overlaps the legacy DPTX endpoint number. */ if (dcp->dcpavservep && endpoint == dcp->dcpavservep->endpoint) return dcp->dcpavservep; @@ -862,6 +1057,9 @@ static int dcp_rtk_shmem_setup(void *cookie, struct apple_rtkit_shmem *bfr) struct apple_dcp *dcp = cookie; int ret; + if (bfr->iova && dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) + return dcp_desktop_shmem_setup(dcp, bfr); + if (bfr->iova) { struct iommu_domain *domain = iommu_get_domain_for_dev(dcp->dev); @@ -912,7 +1110,9 @@ static void dcp_rtk_shmem_destroy(void *cookie, struct apple_rtkit_shmem *bfr) { struct apple_dcp *dcp = cookie; - if (bfr->is_mapped) + if (bfr->is_mapped && bfr->private) + vunmap(bfr->private); + else if (bfr->is_mapped) memunmap(bfr->buffer); else dma_free_coherent(dcp->dev, bfr->size, bfr->buffer, bfr->iova); @@ -2512,6 +2712,211 @@ int dcp_activate(struct platform_device *pdev) return iomfb_is_m4(dcp) ? iomfb_m4_activate(dcp) : 0; } +/* + * Desktop 26.x (T6041) start. The firmware does not tolerate being left + * running with its endpoints unserviced, so RTKit is woken only here, once + * every handler is in place, not at component bind. The internal panel's + * processor then starts the system endpoint, the IOMFB transport and the + * auxiliary endpoints the firmware advertises, all speaking compact EPIC, + * and binds the IOMFB shared memory last. An auxiliary endpoint that fails + * to start is skipped: IOMFB can run without it. + */ +static void dcp_compact_aux_init(struct apple_epic_service *service, + const char *name, const char *class, s64 unit) +{ + dev_dbg(service->ep->dcp->dev, + "AFK[ep:%02x]: compact auxiliary service %s (unit %lld)\n", + service->ep->endpoint, name ?: "", unit); +} + +/* + * These endpoints are only serviced: no call of theirs was ever seen. A + * call is answered with -EOPNOTSUPP and logged by the AFK core. + */ +static const struct apple_epic_service_ops dcp_compact_aux_ops[] = { + { + .name = "*", + .init = dcp_compact_aux_init, + }, + {}, +}; + +static void dcp_dpavctrl_service_init(struct apple_epic_service *service, + const char *name, const char *class, + s64 unit) +{ + struct apple_dcp *dcp = service->ep->dcp; + struct dcp_desktop_state *d = dcp->desktop; + const char *service_class = class ?: name; + + if (service_class && strstr(service_class, "dcpav-controller-epic")) + d->dpavctrl_av_controller[!!(service->channel & BIT(2))] = service; + + /* Interface 7 is opened only once the AV controllers are. */ + if ((service->channel & 7) == 7) { + service->defer_start = true; + d->dpavctrl_late_service = service; + } + + /* + * The internal panel announces only the AV controller on interface 5 + * and the deferred DP controller on interface 7. + */ + if (dcp_has_panel(dcp)) { + if (d->dpavctrl_av_controller[1] && d->dpavctrl_late_service) + complete_all(&d->dpavctrl_ready); + } else if (service->ep->num_channels >= 4) { + complete_all(&d->dpavctrl_ready); + } +} + +static const struct apple_epic_service_ops dcp_dpavctrl_ops[] = { + { + .name = "AppleDCPDPTXController", + .init = dcp_dpavctrl_service_init, + }, + { + .name = "dcpav-controller-epic", + .init = dcp_dpavctrl_service_init, + }, + { + .name = "dcpdp-controller-epic", + .init = dcp_dpavctrl_service_init, + }, + {}, +}; + +static int dcp_desktop_start_ep(struct apple_dcp *dcp, u32 endpoint, + const struct apple_epic_service_ops *ops) +{ + struct apple_dcp_afkep *ep; + int ret; + + ep = afk_init(dcp, endpoint, ops); + if (IS_ERR(ep)) + return PTR_ERR(ep); + + /* + * Route its messages before the start: afk_start() waits for the + * firmware's reply on this endpoint. The route stays if the start + * times out, the firmware may still talk. + */ + WRITE_ONCE(dcp->desktop->graph[endpoint - 0x20], ep); + ret = afk_start(ep); + return ret; +} + +static int dcp_desktop_start_dpavctrl(struct apple_dcp *dcp) +{ + int ret; + + init_completion(&dcp->desktop->dpavctrl_ready); + ret = dcp_desktop_start_ep(dcp, DPAV_CTRL_ENDPOINT, dcp_dpavctrl_ops); + if (ret) + return ret; + + if (!wait_for_completion_timeout(&dcp->desktop->dpavctrl_ready, + msecs_to_jiffies(1000))) + return -ETIMEDOUT; + return 0; +} + +/* The auxiliary endpoints of the panel's processor, started if advertised. */ +static const struct { + u32 endpoint; + const char *name; +} dcp_desktop_panel_eps[] = { + { DPAV_CTRL_ENDPOINT, "DPAVCTRL" }, + { EPIC25_ENDPOINT, "EPIC25" }, + { DPSAC_ENDPOINT, "DPSAC" }, + { DPDEV_ENDPOINT, "DPDEV" }, + { DPAVSERV_ENDPOINT, "DPAVSERV" }, + { AV_ENDPOINT, "AV" }, + { DPTX_ENDPOINT, "DPTX" }, + { HDCP_ENDPOINT, "HDCP" }, + { EPIC2C_ENDPOINT, "EPIC2C" }, + { REMOTE_ALLOC_ENDPOINT, "REMOTEALLOC" }, +}; + +static void dcp_desktop_graph_skip(struct apple_dcp *dcp, u32 endpoint, + const char *name, const char *reason, int rc) +{ + dcp->desktop->graph_skipped |= BIT(endpoint - 0x20); + if (rc) + dev_warn(dcp->dev, "endpoint %#04x (%s) not started: %s (%d)\n", + endpoint, name, reason, rc); + else + dev_dbg(dcp->dev, "endpoint %#04x (%s) not started: %s\n", + endpoint, name, reason); +} + +static int dcp_start_desktop(struct apple_dcp *dcp) +{ + struct dcp_desktop_state *d = dcp->desktop; + u32 map = 0; + int i, ep, ret; + + if (dcp_desktop_external(dcp)) + return -EOPNOTSUPP; + if (IS_ERR_OR_NULL(dcp->rtk)) + return -ENODEV; + + ret = apple_rtkit_wake(dcp->rtk); + if (ret) + return dev_err_probe(dcp->dev, ret, "Failed to boot RTKit: %d\n", + ret); + + for (ep = 0x20; ep < 0x40; ep++) + if (apple_rtkit_has_endpoint(dcp->rtk, ep)) + map |= BIT(ep - 0x20); + dev_dbg(dcp->dev, "firmware endpoints 0x20+: %#010x\n", map); + + ret = systemep_init(dcp); + if (ret) + dev_warn(dcp->dev, "Failed to start system endpoint: %d\n", ret); + + /* + * The IOMFB transport starts before the auxiliary endpoints; its + * shared memory is bound only after them (iomfb_start_rtkit()). + */ + ret = apple_rtkit_start_ep(dcp->rtk, IOMFB_ENDPOINT); + if (ret) + return dev_err_probe(dcp->dev, ret, + "Failed to start IOMFB transport endpoint\n"); + + for (i = 0; i < ARRAY_SIZE(dcp_desktop_panel_eps); i++) { + u32 endpoint = dcp_desktop_panel_eps[i].endpoint; + const char *name = dcp_desktop_panel_eps[i].name; + + if (!(map & BIT(endpoint - 0x20))) { + dcp_desktop_graph_skip(dcp, endpoint, name, "not advertised", 0); + continue; + } + + if (endpoint == DPAV_CTRL_ENDPOINT) + ret = dcp_desktop_start_dpavctrl(dcp); +#if IS_ENABLED(CONFIG_DRM_APPLE_AUDIO) + else if (endpoint == AV_ENDPOINT && hdmi_audio) + ret = avep_init(dcp); +#endif + else + ret = dcp_desktop_start_ep(dcp, endpoint, + dcp_compact_aux_ops); + if (ret) { + dcp_desktop_graph_skip(dcp, endpoint, name, "start failed", ret); + continue; + } + d->graph_started |= BIT(endpoint - 0x20); + } + dev_info(dcp->dev, "endpoints started %#010x, skipped %#010x\n", + d->graph_started, d->graph_skipped); + + ret = iomfb_start_rtkit(dcp); + if (ret) + dev_err(dcp->dev, "Failed to start IOMFB endpoint: %d\n", ret); + return ret; +} + int dcp_start(struct platform_device *pdev) { struct apple_dcp *dcp = platform_get_drvdata(pdev); @@ -2527,6 +2932,8 @@ int dcp_start(struct platform_device *pdev) init_completion(&dcp->start_done); if (dcp->fw_compat == DCP_FIRMWARE_V_26_6) return iomfb_v26_6_start(dcp); + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) + return dcp_start_desktop(dcp); /* * The T6030 firmware session is adopted for the internal panel. @@ -2796,6 +3203,13 @@ void dcp_poweron(struct platform_device *pdev) unsigned long remaining; int ret; + /* The takeover trial powered it on before the eviction. */ + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && + dcp->desktop->trial_powered) { + dcp->desktop->trial_powered = false; + return; + } + if (iomfb_is_m4(dcp)) { ret = dcp_backlight_dpms(dcp, true); if (ret) @@ -3049,6 +3463,17 @@ void dcp_poweroff(struct platform_device *pdev) } } +/* Desktop 26.x: the DRM device exists now, register the panel backlight. */ +void dcp_backlight_takeover_registered(struct platform_device *pdev) +{ + struct apple_dcp *dcp = platform_get_drvdata(pdev); + + if (!dcp || dcp->fw_compat != DCP_FIRMWARE_V_26_6_DESKTOP || + !dcp_has_panel(dcp) || !dcp->brightness.maximum) + return; + schedule_work(&dcp->bl_register_wq); +} + static void dcp_work_register_backlight(struct work_struct *work) { int ret; @@ -3498,7 +3923,26 @@ static enum dcp_firmware_version dcp_check_firmware_version(struct device *dev) return hw->firmware_compat; /* firmware version is just informative */ - dcp_read_fw_version(dev, "apple,firmware-version", fw_str); + ret = dcp_read_fw_version(dev, "apple,firmware-version", fw_str); + + /* + * T6041 (M4 Pro/Max) is driven only with the 26.6.2 firmware its + * desktop IOMFB instance was written for: both the boot firmware and + * its compatibility version must be exactly that release. + */ + if (of_device_is_compatible(dev->of_node, "apple,t6041-dcp") || + of_device_is_compatible(dev->of_node, "apple,t6041-dcpext")) { + int compat_ret = dcp_read_fw_version(dev, "apple,firmware-compat", + compat_str); + + if (ret >= 0 && compat_ret >= 0 && !strcmp(fw_str, "26.6.2") && + !strcmp(compat_str, "26.6.2")) + return DCP_FIRMWARE_V_26_6_DESKTOP; + dev_err(dev, "T6041 display not started: DCP firmware %s (compat %s) is not 26.6.2\n", + ret >= 0 ? fw_str : "missing", + compat_ret >= 0 ? compat_str : "missing"); + return DCP_FIRMWARE_UNKNOWN; + } ret = dcp_read_fw_version(dev, "apple,firmware-compat", compat_str); if (ret < 0) { @@ -3788,6 +4232,16 @@ static int dcp_comp_bind_pipe(struct device *dev, struct device *main, void *dat if (dcp->external_prepared) return 0; + /* + * The T6041 external processors' start is not implemented yet. The + * failsafe keeps such a processor out of the DRM device without + * touching it. + */ + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && + dcp_desktop_external(dcp)) + return dev_err_probe(dev, -EOPNOTSUPP, + "T6041 external display processor start is not implemented\n"); + /* A timed-out prior session may still scan these mappings. */ mutex_lock(&dcp->swapped_out_fbs_lock); ret = list_empty(&dcp->swapped_out_fbs) ? 0 : -EBUSY; @@ -3852,7 +4306,17 @@ static int dcp_comp_bind_pipe(struct device *dev, struct device *main, void *dat if (panel_np) { const char height_prop[2][16] = { "adj-height-mm", "height-mm" }; - if (of_device_is_available(panel_np)) { + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) { + /* + * Desktop 26.x swaps carry nits, and the panel node + * describes the panel whether or not the boot loader + * enabled it. Without apple,max-brightness take the + * J616s panel's user-accessible SDR maximum. + */ + if (of_property_read_u32(panel_np, "apple,max-brightness", + &dcp->brightness.maximum)) + dcp->brightness.maximum = DCP_DESKTOP_MAX_NITS; + } else if (of_device_is_available(panel_np)) { ret = of_property_read_u32(panel_np, "apple,max-brightness", &dcp->brightness.maximum); if (ret) @@ -3914,7 +4378,15 @@ static int dcp_comp_bind_pipe(struct device *dev, struct device *main, void *dat goto err_piodma; } - dcp->clk = devm_clk_get(dev, NULL); + /* + * T6041: the device tree carries no DCP clock, and the boot loader + * left the coprocessor clocked; D408 then reports 0 Hz, as the + * driver this instance was ported from did. + */ + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) + dcp->clk = devm_clk_get_optional(dev, NULL); + else + dcp->clk = devm_clk_get(dev, NULL); if (IS_ERR(dcp->clk)) { ret = dev_err_probe(dev, PTR_ERR(dcp->clk), "Unable to find clock\n"); @@ -3991,6 +4463,8 @@ static int dcp_comp_bind_pipe(struct device *dev, struct device *main, void *dat if (dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL) ret = apple_rtkit_wake_early_ap(dcp->rtk); + else if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) + ret = 0; /* woken in dcp_start_desktop() */ else ret = apple_rtkit_wake(dcp->rtk); if (ret) { @@ -4169,6 +4643,19 @@ VISIBLE_IF_KUNIT void dcp_comp_unbind(struct device *dev, struct device *main, v dcp->systemep = NULL; } + /* + * The auxiliary endpoints a desktop 26.x start brought up, still + * routed while their shutdown is acknowledged. + */ + for (int i = 0; dcp->desktop && i < ARRAY_SIZE(dcp->desktop->graph); i++) { + struct apple_dcp_afkep *ep = dcp->desktop->graph[i]; + + if (ep) { + afk_shutdown(ep); + WRITE_ONCE(dcp->desktop->graph[i], NULL); + } + } + if (dcp->dcpavservep) { afk_shutdown(dcp->dcpavservep); dpavservep_detach(dcp); @@ -4281,6 +4768,11 @@ static int dcp_platform_probe_pipe(struct platform_device *pdev) fw_compat = dcp_check_firmware_version(dev); if (fw_compat == DCP_FIRMWARE_UNKNOWN) return -ENODEV; + if (fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) { + ret = dcp_validate_t6041_dt(dev); + if (ret) + return ret; + } /* Check for "apple,bw-scratch" to avoid probing appledrm with outdated * device trees. This prevents replacing simpledrm and ending up without @@ -4321,7 +4813,8 @@ static int dcp_platform_probe_pipe(struct platform_device *pdev) dcp->desktop->client = GENMASK_ULL(63, 48) | (get_random_u64() & GENMASK_ULL(47, 4)); } - dcp->ext_failsafe = of_device_is_compatible(dev->of_node, "apple,t8122-dcpext"); + dcp->ext_failsafe = of_device_is_compatible(dev->of_node, "apple,t8122-dcpext") || + of_device_is_compatible(dev->of_node, "apple,t6041-dcpext"); dcp->external = of_device_is_compatible(dev->of_node, "apple,t6030-dcpext"); dcp->external_native = dcp->external && dcp_t6030_ext_native(dev->of_node); dcp->dev = dev; @@ -4718,7 +5211,15 @@ static int dcp_platform_probe(struct platform_device *pdev) void *group; int ret; - if (!of_device_is_compatible(dev->of_node, "apple,t8122-dcpext")) + /* Not driven yet: stay off the hardware unless asked for. */ + if (of_device_is_compatible(dev->of_node, "apple,t6041-dcpext") && + !t6041_dcpext) { + dev_info(dev, "T6041 external display processor left alone (appledrm.t6041_dcpext=0)\n"); + return -ENODEV; + } + + if (!of_device_is_compatible(dev->of_node, "apple,t8122-dcpext") && + !of_device_is_compatible(dev->of_node, "apple,t6041-dcpext")) return dcp_platform_probe_pipe(pdev); ret = dcp_ext_route_suppliers(dev); @@ -4779,10 +5280,27 @@ static void dcp_platform_remove(struct platform_device *pdev) iomfb_v14_7_remove(dcp); } +/* A refused takeover left the DCP running untouched, see apple_drv.c. */ +static bool dcp_takeover_was_aborted(struct apple_dcp *dcp) +{ + return dcp && dcp->desktop && READ_ONCE(dcp->desktop->takeover_aborted); +} + +void dcp_takeover_aborted(struct platform_device *pdev) +{ + struct apple_dcp *dcp = platform_get_drvdata(pdev); + + if (dcp && dcp->desktop) + WRITE_ONCE(dcp->desktop->takeover_aborted, true); +} + VISIBLE_IF_KUNIT void dcp_platform_shutdown(struct platform_device *pdev) { struct apple_dcp *dcp = platform_get_drvdata(pdev); + if (dcp_takeover_was_aborted(dcp)) + return; + if (dcp && iomfb_is_m4(dcp)) { dcp_comp_unbind(&pdev->dev, NULL, NULL); return; @@ -4972,6 +5490,10 @@ static int dcp_platform_suspend(struct device *dev) { struct apple_dcp *dcp = dev_get_drvdata(dev); + /* Its power sequences may crash the DCP and blank the panel. */ + if (dcp_takeover_was_aborted(dcp)) + return -EBUSY; + /* The retained T8132 session has no qualified system-sleep handshake. */ if (iomfb_is_m4(dcp)) return -EBUSY; @@ -5048,6 +5570,9 @@ static int dcp_platform_resume(struct device *dev) { struct apple_dcp *dcp = dev_get_drvdata(dev); + if (dcp_takeover_was_aborted(dcp)) + return 0; + if (iomfb_is_m4(dcp)) return iomfb_m4_health(dcp); if (READ_ONCE(dcp->ext_dead)) @@ -5163,7 +5688,18 @@ static const struct apple_dcp_hw_data apple_dcp_hw_t8132 = { }; #endif +/* T6041 (M4 Pro/Max), desktop 26.6 firmware */ +static const struct apple_dcp_hw_data apple_dcp_hw_t6041 = { + .num_dptx_ports = 2, +}; + +static const struct apple_dcp_hw_data apple_dcp_hw_t6041_dcpext = { + .num_dptx_ports = 2, +}; + static const struct of_device_id of_match[] = { + { .compatible = "apple,t6041-dcp", .data = &apple_dcp_hw_t6041, }, + { .compatible = "apple,t6041-dcpext", .data = &apple_dcp_hw_t6041_dcpext, }, { .compatible = "apple,t6020-dcp", .data = &apple_dcp_hw_t6020, }, { .compatible = "apple,t6020-dcpext", .data = &apple_dcp_hw_t6020_dcpext, }, { .compatible = "apple,t8112-dcp", .data = &apple_dcp_hw_t8112, }, diff --git a/drivers/gpu/drm/apple/dcp.h b/drivers/gpu/drm/apple/dcp.h index f54bcb280cebc..c04c312a09a92 100644 --- a/drivers/gpu/drm/apple/dcp.h +++ b/drivers/gpu/drm/apple/dcp.h @@ -64,6 +64,11 @@ int dcp_get_connector_type(struct platform_device *pdev); bool dcp_fw_compat_is_12_x(struct platform_device *pdev); bool dcp_fw_compat_is_14_7(struct platform_device *pdev); unsigned int dcp_gamma_lut_size(struct platform_device *pdev); +/* T6041 external display processors may join the DRM device. */ +bool dcp_t6041_ext_enabled(void); +void dcp_backlight_takeover_registered(struct platform_device *pdev); +int dcp_takeover_trial(struct platform_device *pdev, const char **reason); +void dcp_takeover_aborted(struct platform_device *pdev); bool dcp_ext_unusable(struct platform_device *pdev); unsigned long* dcp_get_iomfb_surfaces(struct platform_device *pdev); int dcp_start(struct platform_device *pdev); diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index 86482c75f7e36..c6bf3b8d35fe6 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -1983,6 +1983,153 @@ int dcp_get_modes(struct drm_connector *connector) return published; } +/* The pixel clock of the J616s boot loader's panel mode. */ +#define DCP_DESKTOP_BOOT_PIXEL_CLOCK_KHZ 521065 +#define DCP_DESKTOP_TRIAL_MAX_CAND 2 + +/* Reuses the mode the takeover trial set, once; see dcp_takeover_trial(). */ +static int dcp_desktop_modeset(struct apple_dcp *dcp, + struct drm_crtc_state *crtc_state) +{ + struct dcp_desktop_state *d = dcp->desktop; + struct dcp_display_mode matched; + + if (d->trial_modeset_ok) { + d->trial_modeset_ok = false; + if (d->mode_live && lookup_mode(dcp, &crtc_state->mode, &matched) && + matched.timing_mode_id == d->trial_timing_mode_id && + matched.color_mode_id == d->trial_color_mode_id) { + dev_dbg(dcp->dev, "keeping the takeover mode " DRM_MODE_FMT "\n", + DRM_MODE_ARG(&crtc_state->mode)); + return 0; + } + } + return iomfb_modeset_v26_6_desktop(dcp, crtc_state); +} + +/* + * Takeover gate of a desktop 26.x panel, called after the DCP started and + * before the boot framebuffer is evicted. Everything that can still be + * refused is tried while the boot framebuffer owns the screen: DCP ready, + * modes known, panel connected, power on, one mode set. Returns 0 when it + * is safe to evict, otherwise a negative errno and a short reason. A + * failure leaves the DCP running: its shutdown sequence can crash it. + */ +int dcp_takeover_trial(struct platform_device *pdev, const char **reason) +{ + struct apple_dcp *dcp = platform_get_drvdata(pdev); + struct drm_display_mode cand[DCP_DESKTOP_TRIAL_MAX_CAND]; + unsigned int i, k, n = 0, pref = UINT_MAX, boot = UINT_MAX; + u32 best = U32_MAX; + struct dcp_desktop_state *d; + struct drm_crtc_state *st; + int win = -1, ret = -EIO; + + *reason = "none"; + if (!dcp || dcp->fw_compat != DCP_FIRMWARE_V_26_6_DESKTOP || + !dcp_has_panel(dcp)) + return 0; + d = dcp->desktop; + if (dcp->crashed) { + *reason = "dcp-crashed"; + return -ENODEV; + } + if (!dcp->active) { + *reason = "dcp-not-ready"; + return -ETIMEDOUT; + } + if (!READ_ONCE(dcp->nr_modes)) { + *reason = "no-modes"; + return -ENODEV; + } + if (!dcp->connector || !dcp->connector->connected) { + *reason = "not-connected"; + return -ENODEV; + } + /* The boot-like mode first, then the preferred one. */ + mutex_lock(&dcp->modes_lock); + for (i = 0; i < dcp->nr_modes; i++) { + const struct dcp_display_mode *m = &dcp->modes[i]; + + if ((m->mode.type & DRM_MODE_TYPE_PREFERRED) && pref == UINT_MAX) + pref = i; + if (!m->vrr) { + u32 delta = abs((int)m->mode.clock - + DCP_DESKTOP_BOOT_PIXEL_CLOCK_KHZ); + + if (delta < best) { + best = delta; + boot = i; + } + } + } + if (pref == UINT_MAX) + pref = 0; + /* Only a mode within 1% of the boot loader's clock counts as boot-like. */ + if (boot != UINT_MAX && best <= DCP_DESKTOP_BOOT_PIXEL_CLOCK_KHZ / 100) + cand[n++] = dcp->modes[boot].mode; + else + boot = UINT_MAX; + if (pref != boot) + cand[n++] = dcp->modes[pref].mode; + mutex_unlock(&dcp->modes_lock); + + st = kzalloc_obj(*st); + if (!st) { + *reason = "no-memory"; + return -ENOMEM; + } + + dcp_poweron(pdev); + if (dcp->crashed || d->poweron_result) { + *reason = dcp->crashed ? "dcp-crashed-in-poweron" : "poweron-failed"; + goto out; + } + + for (k = 0; k < n; k++) { + int r; + + st->mode = cand[k]; + dev_dbg(dcp->dev, "takeover trial mode set " DRM_MODE_FMT "\n", + DRM_MODE_ARG(&st->mode)); + r = iomfb_modeset_v26_6_desktop(dcp, st); + if (!r && d->mode_live) { + win = k; + break; + } + if (dcp->crashed) + break; + } + if (win < 0) { + *reason = dcp->crashed ? "dcp-crashed-in-modeset" : "modeset-failed"; + goto out; + } + + d->trial_timing_mode_id = dcp->mode.timing_mode_id; + d->trial_color_mode_id = dcp->mode.color_mode_id; + d->trial_powered = true; + d->trial_modeset_ok = true; + + /* Make the mode that worked the one the console client picks. */ + mutex_lock(&dcp->modes_lock); + for (i = 0; i < dcp->nr_modes; i++) { + struct dcp_display_mode *m = &dcp->modes[i]; + + if (m->timing_mode_id == d->trial_timing_mode_id && + m->color_mode_id == d->trial_color_mode_id) + m->mode.type |= DRM_MODE_TYPE_PREFERRED; + else + m->mode.type &= ~DRM_MODE_TYPE_PREFERRED; + } + mutex_unlock(&dcp->modes_lock); + dev_info(dcp->dev, "takeover trial mode " DRM_MODE_FMT " accepted\n", + DRM_MODE_ARG(&cand[win])); + ret = 0; +out: + kfree(st); + return ret; +} + /* The user may own drm_display_mode, so we need to search for our copy */ bool lookup_mode(struct apple_dcp *dcp, const struct drm_display_mode *mode, struct dcp_display_mode *out) @@ -2130,7 +2277,7 @@ int dcp_crtc_atomic_modeset(struct drm_crtc *crtc, ret = iomfb_modeset_v14_7_0(dcp, crtc_state); break; case DCP_FIRMWARE_V_26_6_DESKTOP: - ret = iomfb_modeset_v26_6_desktop(dcp, crtc_state); + ret = dcp_desktop_modeset(dcp, crtc_state); break; case DCP_FIRMWARE_V_26_6: ret = iomfb_v26_6_modeset(dcp, crtc_state); @@ -2803,9 +2950,12 @@ int iomfb_start_rtkit(struct apple_dcp *dcp) if (ret) return ret; } - ret = apple_rtkit_start_ep(dcp->rtk, IOMFB_ENDPOINT); - if (ret) - return ret; + /* Desktop 26.x started the transport with its auxiliary endpoints. */ + if (dcp->fw_compat != DCP_FIRMWARE_V_26_6_DESKTOP) { + ret = apple_rtkit_start_ep(dcp->rtk, IOMFB_ENDPOINT); + if (ret) + return ret; + } dcp->shmem = dma_alloc_coherent(dcp->dev, DCP_SHMEM_SIZE, &shmem_iova, GFP_KERNEL); diff --git a/drivers/gpu/drm/apple/iomfb_template.c b/drivers/gpu/drm/apple/iomfb_template.c index da2c9eab540dc..693dca78f831b 100644 --- a/drivers/gpu/drm/apple/iomfb_template.c +++ b/drivers/gpu/drm/apple/iomfb_template.c @@ -2031,6 +2031,9 @@ void DCP_FW_NAME(iomfb_poweron)(struct apple_dcp *dcp) } #endif ret = wait_for_completion_timeout(&cookie->done, msecs_to_jiffies(10000)); +#if DCP_FW_DESKTOP_26(DCP_FW_VER) + dcp->desktop->poweron_result = ret > 0 ? 0 : ret == 0 ? 1 : 2; +#endif if (ret == 0) { dev_warn(dcp->dev, "wait for power timed out, connector will be broken\n"); @@ -3383,15 +3386,11 @@ static void res_is_main_display(struct apple_dcp *dcp, void *out, void *cookie) } dcp->active = true; -#if DCP_FW_DESKTOP_26(DCP_FW_VER) /* - * The firmware publishes no initial panel level that would register - * the backlight (see iomfb_cb_pr_publish()); register it now. + * Desktop 26.x publishes no initial panel level that would register + * the backlight (see iomfb_cb_pr_publish()): the DRM master registers + * it once its device is, see dcp_backlight_takeover_registered(). */ - if (dcp_has_panel(dcp) && !dcp->brightness.bl_dev && - dcp->brightness.maximum > 0) - schedule_work(&dcp->bl_register_wq); -#endif complete(&dcp->start_done); } From 99870822214e82bc5f7bcd44fe1e5ccff422ec68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Bal=C3=A1zsy?= Date: Sun, 25 Oct 2026 12:59:20 +0100 Subject: [PATCH 08/12] drm/apple: compact DPTX and DPAVSERV for desktop 26.x external displays MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The T6041 external display processors publish their DPTX port services as dcpdptx-port-epic:0 and :1 in compact EPIC on EPIC2C (0x2c). Bind them by unit, open port 0 last, and follow the firmware's own client where the compact ABI differs: - set_hpd returns an IOReturn status in its first word; - GET_SUPPORTS_HPD and GET_VIRTUAL_DEVICE_MODE are answered with 0; - GET_MAX_LINK_RATE offers HBR3; - the crossbar link is taken down before a link configuration change and brought up after it; - group 1 calls 3 and 4 keep the first 32 bytes of their record and clear the result word. The DPAVSERV EDID request asks for 2 KiB there, as a 32 KiB inline request does not fit the compact ring, and dcpdp-service-epic is not opened. Co-developed-by: Niklas Sheth Signed-off-by: Niklas Sheth Signed-off-by: František Balázsy --- drivers/gpu/drm/apple/dcp.h | 2 + drivers/gpu/drm/apple/dptxep.c | 137 +++++++++++++++++++++++- drivers/gpu/drm/apple/epic/dpavservep.c | 22 +++- 3 files changed, 155 insertions(+), 6 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp.h b/drivers/gpu/drm/apple/dcp.h index c04c312a09a92..30b728820e2c7 100644 --- a/drivers/gpu/drm/apple/dcp.h +++ b/drivers/gpu/drm/apple/dcp.h @@ -63,6 +63,7 @@ int dcp_crtc_atomic_check(struct drm_crtc *crtc, struct drm_atomic_commit *commi int dcp_get_connector_type(struct platform_device *pdev); bool dcp_fw_compat_is_12_x(struct platform_device *pdev); bool dcp_fw_compat_is_14_7(struct platform_device *pdev); +bool dcp_fw_compat_is_desktop(struct platform_device *pdev); unsigned int dcp_gamma_lut_size(struct platform_device *pdev); /* T6041 external display processors may join the DRM device. */ bool dcp_t6041_ext_enabled(void); @@ -124,6 +125,7 @@ void iomfb_recv_msg(struct apple_dcp *dcp, u64 message); int systemep_init(struct apple_dcp *dcp); int dptxep_init(struct apple_dcp *dcp); +int dptxep_init_desktop(struct apple_dcp *dcp, u32 endpoint); int dptx_controller_init(struct apple_dcp *dcp); int ibootep_init(struct apple_dcp *dcp); int dpavservep_init(struct apple_dcp *dcp); diff --git a/drivers/gpu/drm/apple/dptxep.c b/drivers/gpu/drm/apple/dptxep.c index d61b28e04546a..92525cb250d15 100644 --- a/drivers/gpu/drm/apple/dptxep.c +++ b/drivers/gpu/drm/apple/dptxep.c @@ -220,6 +220,15 @@ int dptxport_set_hpd_timeout(struct apple_epic_service *service, bool hpd, &received); if (ret) return ret; + /* Compact EPIC returns an IOReturn status in the first word instead. */ + if (READ_ONCE(service->ep->compact)) { + u32 status = get_unaligned_le32(resp._pad0); + + if (status) + dev_warn(service->ep->dcp->dev, + "set_hpd(%d): firmware status %#x\n", hpd, status); + return status ? -EIO : 0; + } /* Past the end of a short reply there is only zero fill, not an echo. */ if (received < sizeof(resp)) { dev_warn(service->ep->dcp->dev, @@ -469,6 +478,24 @@ static int dptxport_call_get_link_rate(struct apple_epic_service *service, return 0; } +/* + * Compact DPTX (T6041): the direct DP PHY's pixel clock is the crossbar's + * PCLK1. Take the crossbar link down before a link configuration change + * and bring it up after it, the PHY rate having been set in between. + */ +static int dptxport_compact_xbar_link(struct apple_epic_service *service, bool up) +{ + struct apple_dcp *dcp = service->ep->dcp; + int ret; + + if (!READ_ONCE(service->ep->compact) || !dcp->xbar) + return 0; + ret = up ? apple_dpxbar_link_up(dcp->xbar) : + apple_dpxbar_link_down(dcp->xbar); + dev_dbg(dcp->dev, "DPTXPort: crossbar link %s: %d\n", up ? "up" : "down", ret); + return ret; +} + static int dptxport_call_will_change_link_config(struct apple_epic_service *service) { @@ -479,6 +506,7 @@ dptxport_call_will_change_link_config(struct apple_epic_service *service) dptx->phy_ops.dp.set_rate = 0; dptx->phy_ops.dp.set_voltages = 0; + dptxport_compact_xbar_link(service, false); return 0; } @@ -493,6 +521,7 @@ dptxport_call_did_change_link_config(struct apple_epic_service *service) /* assume the link config did change and wait a little bit */ mdelay(10); + dptxport_compact_xbar_link(service, true); return 0; } @@ -600,7 +629,8 @@ static int dptxport_call_get_supports_hpd(struct apple_epic_service *service, * that path instead of waiting for a PHY start. */ reply->supported = cpu_to_le32(dcp_is_typec_output(dcp) && - !dcp_ext_dptx_607(dcp) ? 1 : 0); + !dcp_ext_dptx_607(dcp) && + !READ_ONCE(service->ep->compact) ? 1 : 0); dev_dbg(dcp->dev, "DPTXPort: GET_SUPPORTS_HPD %u usb4=%d\n", le32_to_cpu(reply->supported), dcp_is_usb4_output(dcp)); return 0; @@ -912,6 +942,19 @@ static int dptxport_prepare(struct apple_epic_service *service, const char *name * T6030 14.7 also announces the low-power port service, and may leave * its class empty. */ + /* + * Desktop 26.x announces dcpdptx-port-epic:0 and :1 in compact EPIC, + * without a class. Unit 0 is opened last, see dptxep_init_desktop(). + */ + if (READ_ONCE(service->ep->compact)) { + if (unit != 0 && unit != 1) + return -EINVAL; + if (!unit) + service->defer_start = true; + trace_dptxport_init(dcp, unit); + return dptx_service_bind(service, &dcp->dptxport[unit], unit); + } + if (dcp_fw_is_14_7(dcp) && name && (!strcmp(name, "dcpdptx-port-epic") || !strcmp(name, "dcp-lpdptx-port-epic")) && @@ -1071,6 +1114,98 @@ static const struct apple_epic_service_ops dptxep_ops[] = { {} }; +/* + * Group 1 calls 3 and 4 of compact DPTX carry an 80-byte record: keep the + * first 32 bytes, clear the result word (at 0 for 3, at 16 for 4) and never + * echo the pointer-bearing tail. + */ +static int dptxport_compact_group1(u32 command, const void *data, size_t size, + void *reply, size_t room) +{ + size_t preserved = min_t(size_t, 32, min(size, room)); + size_t result = command == 3 ? 0 : 16; + + if (command != 3 && command != 4) + return -EOPNOTSUPP; + memcpy(reply, data, preserved); + if (room > preserved) + memset(reply + preserved, 0, room - preserved); + if (room < result + sizeof(__le32)) + return -EINVAL; + memset(reply + result, 0, sizeof(__le32)); + return 0; +} + +static int dptxport_compact_call(struct apple_epic_service *service, u16 group, + u32 command, const void *data, size_t size, + void *reply, size_t room) +{ + if (group == 1) + return dptxport_compact_group1(command, data, size, reply, room); + if (group) + return -EOPNOTSUPP; + /* Asked on every connect; no virtual device is supported. */ + if (command == DPTX_APCALL_GET_VIRTUAL_DEVICE_MODE) { + if (room < sizeof(struct dptxport_apcall_get_support)) + return -EINVAL; + memset(reply, 0, room); + return 0; + } + return dptxport_call(service, command, data, size, reply, room); +} + +static const struct apple_epic_service_ops dptxep_compact_ops[] = { + { + .name = "dcpdptx-port-epic", + .prepare = dptxport_prepare, + .teardown = dptxport_teardown, + .init = dptxport_init, + .call_v2 = dptxport_compact_call, + }, + {} +}; + +/* + * The DPTX port services of a desktop 26.x external processor (T6041 + * dcpext), on EPIC2C (0x2c): wait for both ports, then open port 0. + */ +int dptxep_init_desktop(struct apple_dcp *dcp, u32 endpoint) +{ + struct apple_dcp_afkep *ep; + unsigned long timeout = msecs_to_jiffies(1000); + u32 port; + int ret; + + init_completion(&dcp->dptxport[0].enable_completion); + init_completion(&dcp->dptxport[1].enable_completion); + init_completion(&dcp->dptxport[0].linkcfg_completion); + init_completion(&dcp->dptxport[1].linkcfg_completion); + + ep = afk_init(dcp, endpoint, dptxep_compact_ops); + if (IS_ERR(ep)) + return PTR_ERR(ep); + /* Routed by its endpoint number: set before the start replies arrive. */ + dcp->dptxep = ep; + + ret = afk_start(ep); + if (ret) + return ret; + + for (port = 0; port < dcp->hw.num_dptx_ports; port++) { + ret = wait_for_completion_timeout(&dcp->dptxport[port].enable_completion, + timeout); + if (!ret) { + dev_warn(dcp->dev, "DPTX port %u service not announced\n", port); + return -ETIMEDOUT; + } + timeout = ret; + } + + if (READ_ONCE(ep->compact) && dcp->dptxport[0].service) + return afk_start_service(dcp->dptxport[0].service); + return 0; +} + static void dptx_controller_service_init(struct apple_epic_service *service, const char *name, const char *class, s64 unit) { diff --git a/drivers/gpu/drm/apple/epic/dpavservep.c b/drivers/gpu/drm/apple/epic/dpavservep.c index bec8f711fbd14..fc6d7ed5d570a 100644 --- a/drivers/gpu/drm/apple/epic/dpavservep.c +++ b/drivers/gpu/drm/apple/epic/dpavservep.c @@ -209,6 +209,9 @@ static int dcpdpserv_prepare(struct apple_epic_service *service, const char *nam static void dcpdpserv_init(struct apple_epic_service *service, const char *name, const char *class, s64 unit) { + /* Compact EPIC (desktop 26.x): the firmware's own client never opens it. */ + if (READ_ONCE(service->ep->compact)) + service->defer_start = true; } static void dcpdpserv_teardown(struct apple_epic_service *service) @@ -310,36 +313,45 @@ static int dcpdpserv_report(struct apple_epic_service *service, return parse_report(service, type, data, data_size); } +/* + * A compact EPIC call goes inline through the endpoint's ring, which a + * 32 KiB EDID request can never fit: ask for 2 KiB there, enough for 15 + * extension blocks. + */ +#define EDID_COMPACT_BUF_SIZE (EDID_LEADING_DATA_SIZE + SZ_2K) + static const struct drm_edid *dcpavserv_read_edid_legacy(struct apple_epic_service *service) { struct dpavserv_copy_edid_cmd cmd; struct dpavserv_copy_edid_resp *resp __free(kfree) = NULL; - size_t resp_size = sizeof(*resp) + EDID_BUF_SIZE; + size_t edid_buf_size = READ_ONCE(service->ep->compact) ? EDID_COMPACT_BUF_SIZE : + EDID_BUF_SIZE; + size_t resp_size = sizeof(*resp) + edid_buf_size; size_t reply_size; int num_blocks; u64 data_size; int ret; memset(&cmd, 0, sizeof(cmd)); - cmd.max_size = cpu_to_le64(EDID_BUF_SIZE); + cmd.max_size = cpu_to_le64(edid_buf_size); resp = kzalloc(resp_size, GFP_KERNEL); if (!resp) return ERR_PTR(-ENOMEM); ret = afk_service_call_with_reply_len(service, 1, 7, &cmd, sizeof(cmd), - EDID_BUF_SIZE, resp, resp_size, 0, + edid_buf_size, resp, resp_size, 0, &reply_size); if (ret < 0) return ERR_PTR(ret); if (reply_size < sizeof(*resp) || - le64_to_cpu(resp->max_size) != EDID_BUF_SIZE) + le64_to_cpu(resp->max_size) != edid_buf_size) return ERR_PTR(-EIO); /* Only complete EDID blocks within the received reply may be copied. */ data_size = le64_to_cpu(resp->used_size); if (data_size < EDID_LEADING_DATA_SIZE + EDID_BLOCK_SIZE || - data_size > EDID_BUF_SIZE || data_size > reply_size - sizeof(*resp)) + data_size > edid_buf_size || data_size > reply_size - sizeof(*resp)) return ERR_PTR(-EIO); /* From 2a2a77531715a5650bf2ce5338ea051146de1a6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Bal=C3=A1zsy?= Date: Sun, 25 Oct 2026 12:59:21 +0100 Subject: [PATCH 09/12] drm/apple: drive the T6041 external displays on desktop 26.6 firmware MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The two external display processors of the M4 Pro/Max (J616s) join the panel's DRM device as further pipelines; t6041_dcpext=0 leaves them alone. dcpext0 drives the HDMI port through its fixed route (atcphy3 and its crossbar), dcpext1 the USB-C ports through a route chosen at connect from the PHY that entered DP alt mode. - probe powers the fixed route's DP PHY and lists the dynamic routes (dp-phyN, dp-xbarN, apple,dptx-phys, apple,dptx-mux-index); - start brings up the advertised auxiliary endpoints, the DPTX port services, DPAVSERV, EXPERT and EPIC25, and connects DPTX port 0; - a connect asserts HPD after the display request and does not fail when no display is attached; - an HDMI hotplug on a connected port pulses HPD, so the firmware exports the modes of a display plugged in after boot; - a USB-C hotplug selects and releases the route through the connector's out-of-band hotplug under hpd_mutex; a failed connect and unbind release it; - a CRTC kept active across a replug is powered on and set up again on its next flush. A dcpext that is absent or fails to probe or bind is kept as an unusable pipe and never holds up the panel. apple_t6040_phy_dp_alt() gets a stub, and DRM_APPLE cannot be built in with the T6040 USB2 PHY as a module. Signed-off-by: František Balázsy --- drivers/gpu/drm/apple/Kconfig | 1 + drivers/gpu/drm/apple/apple_drv.c | 6 +- drivers/gpu/drm/apple/dcp-fabric.c | 9 + drivers/gpu/drm/apple/dcp-internal.h | 17 + drivers/gpu/drm/apple/dcp.c | 436 +++++++++++++++++++++++-- drivers/gpu/drm/apple/iomfb.c | 19 ++ drivers/gpu/drm/apple/iomfb_template.c | 3 + include/linux/soc/apple/dp-tunnel.h | 7 + 8 files changed, 477 insertions(+), 21 deletions(-) diff --git a/drivers/gpu/drm/apple/Kconfig b/drivers/gpu/drm/apple/Kconfig index 9685682f2a87e..d7837413e770f 100644 --- a/drivers/gpu/drm/apple/Kconfig +++ b/drivers/gpu/drm/apple/Kconfig @@ -5,6 +5,7 @@ config DRM_APPLE depends on ARCH_APPLE || COMPILE_TEST depends on APPLE_RTKIT depends on TYPEC || !TYPEC + depends on PHY_APPLE_T6040_USB2 || !PHY_APPLE_T6040_USB2 depends on OF_ADDRESS select APPLE_PMP_REPORT select OF_DYNAMIC diff --git a/drivers/gpu/drm/apple/apple_drv.c b/drivers/gpu/drm/apple/apple_drv.c index 5c3a4ab02f1e9..dac96df0316c7 100644 --- a/drivers/gpu/drm/apple/apple_drv.c +++ b/drivers/gpu/drm/apple/apple_drv.c @@ -683,7 +683,9 @@ static int apple_probe_per_dcp(struct device *dev, * and no encoder either, because the port's encoder spans every * pipeline that can drive it. */ - if (connector_type == DRM_MODE_CONNECTOR_USB) { + /* Desktop 26.x drives its Type-C route itself, as a pipeline output. */ + if (connector_type == DRM_MODE_CONNECTOR_USB && + !dcp_fw_compat_is_desktop(dcp)) { dcp_link(dcp, crtc, NULL); *encoder = NULL; return 0; @@ -904,7 +906,7 @@ static bool apple_dcp_usable(struct device_node *np) dcp_t6030_ext_native(np)) && (!of_device_is_compatible(np, "apple,t8140-dcpext") || dcp_h17p_external_supported(np)) && - /* Not driven yet; never let it hold up the panel's device. */ + /* t6041_dcpext=0 keeps the T6041 external processors out. */ (!of_device_is_compatible(np, "apple,t6041-dcpext") || dcp_t6041_ext_enabled()); } diff --git a/drivers/gpu/drm/apple/dcp-fabric.c b/drivers/gpu/drm/apple/dcp-fabric.c index 972f2641e1a9b..976830cef7935 100644 --- a/drivers/gpu/drm/apple/dcp-fabric.c +++ b/drivers/gpu/drm/apple/dcp-fabric.c @@ -102,6 +102,9 @@ static bool dcp_hdmi_borrowed(void *ctx); bool dcp_is_typec_output(struct apple_dcp *dcp) { + /* Desktop 26.x keeps its own Type-C route, outside this fabric. */ + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) + return false; return dcp->active_typec_route || dcp->fixed_connector_type == DRM_MODE_CONNECTOR_USB; } @@ -3655,6 +3658,9 @@ irqreturn_t dcp_dp2hdmi_hpd_edge(int irq, void *data) { struct apple_dcp *dcp = data; + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) + return IRQ_WAKE_THREAD; + WRITE_ONCE(dcp->hdmi_edge_jiffies, jiffies); WRITE_ONCE(dcp->hdmi_edge_seen, true); dcp_hdmi_hpd_edge(data); @@ -3976,6 +3982,9 @@ irqreturn_t dcp_dp2hdmi_hpd(int irq, void *data) { struct apple_dcp *dcp = data; + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) + return dcp_desktop_dp2hdmi_hpd(dcp); + guard(mutex)(&dcp_typec_fabric_lock); dcp_hdmi_update_locked(dcp); /* The GPIO edge detector is off throughout an oneshot handler. */ diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index 2973f679db3c7..bbc4c8453c536 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -265,6 +265,22 @@ struct dcp_desktop_state { u8 poweron_result; /* The takeover was refused: leave the DCP alone until the reboot. */ bool takeover_aborted; + /* + * External processors: the firmware powered the pipe off with the + * display (unplug); the next swap needs a power-on and a mode set. + */ + bool ext_pipe_off; + /* + * Dynamic Type-C DP route (T6041 dcpext1): the DP PHYs and crossbars + * of every Type-C port, the one in use (or -1), the DPTX target PHY + * of each and the crossbar state selecting this processor. + */ + unsigned int ndyn; + int dyn_active; + struct phy *dyn_phy[3]; + struct mux_control *dyn_xbar[3]; + u32 dyn_dptx_phy[3]; + u32 dyn_mux_index; }; /* TODO: move IOMFB members to its own struct */ @@ -759,6 +775,7 @@ void dcp_announce_arm(struct apple_dcp *dcp); void dcp_announce_work(struct work_struct *work); void dcp_external_crtc_released(struct apple_dcp *dcp); void dcp_external_link_fault(struct apple_dcp *dcp); +irqreturn_t dcp_desktop_dp2hdmi_hpd(struct apple_dcp *dcp); /* A KUnit test can wait here for the work it races with. */ static inline void dcp_test_point(struct apple_dcp *dcp, enum dcp_test_point point) diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 859ac7fc6c155..3494c47de9bca 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -216,14 +216,14 @@ module_param(unstable_edid, bool, 0644); MODULE_PARM_DESC(unstable_edid, "Enable unstable EDID retrival support"); /* - * T6041 (M4 Pro/Max) external display processors are not driven yet. They - * stay out of the display's DRM device unless asked for; see - * dcp_t6041_ext_enabled(). + * T6041 (M4 Pro/Max) external display processors, whose nodes the boot + * environment enables before the module loads. t6041_dcpext=0 keeps them + * out of the display's DRM device and off the hardware. */ -static bool t6041_dcpext; +static bool t6041_dcpext = true; module_param(t6041_dcpext, bool, 0444); MODULE_PARM_DESC(t6041_dcpext, - "T6041: let the external display processors join the DRM device (default 0; not driven yet, development only)"); + "T6041: drive the external display processors (default 1; 0 leaves them alone)"); bool dcp_t6041_ext_enabled(void) { @@ -1891,11 +1891,223 @@ static int dcp_dptx_connect_tile(struct apple_dcp *dcp) return ret; } +/* + * Desktop 26.x external processors (T6041 dcpext0 HDMI, dcpext1 USB-C). + * They keep the classic external model of the driver this was ported from: + * no Type-C display fabric, the DPTX link set up on HPD (HDMI) or on the + * DP alt mode hotplug of the port (USB-C), and the Type-C port chosen + * dynamically from the PHY that entered DP alt mode. + */ +/* The caller holds hpd_mutex, which also orders dcp->phy against a connect. */ +static int dcp_desktop_dyn_route_select_locked(struct apple_dcp *dcp) +{ + struct dcp_desktop_state *d = dcp->desktop; + unsigned int i; + int ret; + + lockdep_assert_held(&dcp->hpd_mutex); + if (!d->ndyn || d->dyn_active >= 0) + return 0; + for (i = 0; i < d->ndyn; i++) + if (apple_t6040_phy_dp_alt(d->dyn_phy[i])) + break; + if (i == d->ndyn) { + dev_dbg(dcp->dev, "no Type-C port in DP alt mode\n"); + return -ENODEV; + } + ret = mux_control_select(d->dyn_xbar[i], d->dyn_mux_index); + if (ret) { + dev_warn(dcp->dev, "Type-C port %u: crossbar select: %d\n", i, ret); + return ret; + } + ret = phy_init(d->dyn_phy[i]); + if (!ret) { + ret = phy_power_on(d->dyn_phy[i]); + if (ret) + phy_exit(d->dyn_phy[i]); + } + if (ret) { + dev_warn(dcp->dev, "Type-C port %u: DP PHY init/power: %d\n", i, ret); + mux_control_deselect(d->dyn_xbar[i]); + return ret; + } + msleep(25); + dcp->phy = d->dyn_phy[i]; + dcp->xbar = d->dyn_xbar[i]; + dcp->dptx_phy = d->dyn_dptx_phy[i]; + d->dyn_active = i; + dev_dbg(dcp->dev, "Type-C port %u selected (DPTX target PHY %u, crossbar state %u)\n", + i, dcp->dptx_phy, d->dyn_mux_index); + return 0; +} + +static void dcp_desktop_dyn_route_release_locked(struct apple_dcp *dcp) +{ + struct dcp_desktop_state *d = dcp->desktop; + int i = d->dyn_active; + + lockdep_assert_held(&dcp->hpd_mutex); + if (!d->ndyn || i < 0) + return; + d->dyn_active = -1; + dcp->phy = NULL; + dcp->xbar = NULL; + phy_power_off(d->dyn_phy[i]); + phy_exit(d->dyn_phy[i]); + mux_control_deselect(d->dyn_xbar[i]); + dev_dbg(dcp->dev, "Type-C port %u released\n", i); +} + +/* Unbind: a selected route holds its crossbar and its powered PHY. */ +static void dcp_desktop_dyn_route_release(struct apple_dcp *dcp) +{ + if (!dcp->desktop) + return; + guard(mutex)(&dcp->hpd_mutex); + dcp_desktop_dyn_route_release_locked(dcp); +} + +static int dcp_desktop_dptx_connect(struct apple_dcp *dcp, u32 port) +{ + struct dptx_port *dptx = &dcp->dptxport[port]; + unsigned long left; + bool selected; + int ret = 0; + + mutex_lock(&dcp->hpd_mutex); + /* A route selected here is released again if the connect fails. */ + selected = !dcp->phy && dcp->desktop->ndyn; + if (selected) { + ret = dcp_desktop_dyn_route_select_locked(dcp); + if (ret) + goto out_unlock; + } + if (!dcp->phy) { + dev_warn(dcp->dev, "dcp_dptx_connect: missing phy\n"); + ret = -ENODEV; + goto out_unlock; + } + if (!dptx_service_available(dptx)) { + dev_warn(dcp->dev, "dcp_dptx_connect: dptx service for port %u not enabled\n", + port); + ret = -ENODEV; + goto out_release; + } + if (dptx->connected) + goto out_unlock; + + reinit_completion(&dptx->linkcfg_completion); + dptx->atcphy = dcp->phy; + ret = dptxport_connect(dptx->service, 0, dcp->dptx_phy, dcp->dptx_die, true); + if (ret) { + dev_warn(dcp->dev, "DPTX port %u: connect failed: %d\n", port, ret); + goto out_release; + } + ret = dptxport_request_display(dptx->service); + if (ret) { + dev_warn(dcp->dev, "DPTX port %u: display request failed: %d\n", + port, ret); + goto out_release; + } + dptx->connected = true; + + /* + * The firmware does not take HPD from the DP2HDMI converter on its + * own ("HPD not present, skipping mode export"): assert it right + * after the display request. + */ + if (READ_ONCE(dptx->service->ep->compact)) { + ret = dptxport_set_hpd(dptx->service, true); + if (ret) + dev_warn(dcp->dev, "DPTX port %u: set_hpd(1): %d\n", port, ret); + ret = 0; + } + mutex_unlock(&dcp->hpd_mutex); + + /* Without a sink the link is configured once one arrives. */ + left = wait_for_completion_timeout(&dptx->linkcfg_completion, + DPTX_CONNECT_TIMEOUT); + if (!left) + dev_dbg(dcp->dev, "DPTX port %u: no link configuration yet\n", port); + + usleep_range(5, 10); + + if (dcp->avep) + av_service_connect(dcp); + return 0; + +out_release: + if (selected) + dcp_desktop_dyn_route_release_locked(dcp); +out_unlock: + mutex_unlock(&dcp->hpd_mutex); + return ret; +} + +static int dcp_desktop_dptx_disconnect_oob(struct apple_dcp *dcp, u32 port) +{ + int ret; + + disconnected_hpd_event(dcp->connector); + if (dcp->avep) + av_service_disconnect(dcp); + scoped_guard(mutex, &dcp->hpd_mutex) { + if (dptx_service_available(&dcp->dptxport[port])) + dptxport_set_hpd(dcp->dptxport[port].service, false); + } + ret = dcp_dptx_disconnect(dcp, port); + scoped_guard(mutex, &dcp->hpd_mutex) + dcp_desktop_dyn_route_release_locked(dcp); + if (!dcp->main_display) + dcp->desktop->ext_pipe_off = true; + return ret; +} + +/* + * HDMI HPD of a desktop 26.x external processor. A disconnect is left to + * the firmware, which detects it itself. A cold plug after boot finds the + * port connected without a sink: the firmware never saw this HPD, so pulse + * the AP-asserted HPD to make it export the modes now. + */ +irqreturn_t dcp_desktop_dp2hdmi_hpd(struct apple_dcp *dcp) +{ + struct dptx_port *dptx = &dcp->dptxport[0]; + bool connected = gpiod_get_value_cansleep(dcp->hdmi_hpd); + int ret; + + if (connected) { + msleep(500); + connected = gpiod_get_value_cansleep(dcp->hdmi_hpd); + } + dev_dbg(dcp->dev, "DP2HDMI HPD irq, connected:%d after 500 ms\n", connected); + if (!connected) + return IRQ_HANDLED; + + mutex_lock(&dcp->hpd_mutex); + if (dptx_service_available(dptx) && dptx->connected && + READ_ONCE(dptx->service->ep->compact)) { + ret = dptxport_set_hpd(dptx->service, false); + msleep(100); + ret = dptxport_set_hpd(dptx->service, true) ?: ret; + mutex_unlock(&dcp->hpd_mutex); + dev_dbg(dcp->dev, "HDMI cold plug: HPD pulse on the connected port: %d\n", + ret); + return IRQ_HANDLED; + } + mutex_unlock(&dcp->hpd_mutex); + + dcp_dptx_connect(dcp, 0); + return IRQ_HANDLED; +} + int dcp_dptx_connect(struct apple_dcp *dcp, u32 port) { struct dcp_fabric_session session = {}; int ret; + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) + return dcp_desktop_dptx_connect(dcp, port); + if (!dcp_is_external_h17p(dcp)) { if (dcp_typec_sessionless(dcp)) { ret = dcp_dptx_connect_raw(dcp, port, NULL, NULL, false, false); @@ -2480,6 +2692,9 @@ int dcp_dptx_connect_oob(struct platform_device *pdev, u32 port) { struct apple_dcp *dcp = platform_get_drvdata(pdev); + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) + return dcp_desktop_dptx_connect(dcp, port); + if (dcp_is_external_h17p(dcp)) { bool new_cable; @@ -2521,6 +2736,9 @@ int dcp_dptx_disconnect_oob(struct platform_device *pdev, u32 port) { struct apple_dcp *dcp = platform_get_drvdata(pdev); + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) + return dcp_desktop_dptx_disconnect_oob(dcp, port); + if (dcp_is_external_h17p(dcp)) return dcp_external_disconnect(dcp, true); @@ -2680,6 +2898,13 @@ bool dcp_ext_unusable(struct platform_device *pdev) return !dcp || READ_ONCE(dcp->ext_dead); } +bool dcp_fw_compat_is_desktop(struct platform_device *pdev) +{ + struct apple_dcp *dcp = platform_get_drvdata(pdev); + + return dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP; +} + bool dcp_fw_compat_is_14_7(struct platform_device *pdev) { struct apple_dcp *dcp = platform_get_drvdata(pdev); @@ -2850,14 +3075,101 @@ static void dcp_desktop_graph_skip(struct apple_dcp *dcp, u32 endpoint, endpoint, name, reason); } +/* + * The external processors (T6041 dcpext): DPDEV, DPAVCTRL (all four + * controller interfaces), DPSAC, AV and REMOTEALLOC as advertised, then + * with a DP PHY or a Type-C route iBoot, the DPTX port services on EPIC2C, + * DPAVSERV for the EDID, EXPERT and EPIC25, and the DPTX connect of port 0. + * A failure fails this processor's start; the panel is not affected. + */ +static int dcp_desktop_start_ext_ep(struct apple_dcp *dcp, u32 map, u32 endpoint, + const char *name) +{ + int ret; + + if (!(map & BIT(endpoint - 0x20))) { + dcp_desktop_graph_skip(dcp, endpoint, name, "not advertised", 0); + return 0; + } + ret = dcp_desktop_start_ep(dcp, endpoint, dcp_compact_aux_ops); + if (ret) + return dev_err_probe(dcp->dev, ret, "Failed to start %s endpoint\n", + name); + dcp->desktop->graph_started |= BIT(endpoint - 0x20); + return 0; +} + +static int dcp_start_desktop_external(struct apple_dcp *dcp, u32 map) +{ + struct dcp_desktop_state *d = dcp->desktop; + int ret, dptx_ret; + + ret = dcp_desktop_start_ext_ep(dcp, map, DPDEV_ENDPOINT, "DPDEV"); + if (ret) + return ret; + ret = dcp_desktop_start_dpavctrl(dcp); + if (ret) + return dev_err_probe(dcp->dev, ret, + "Failed to initialize DPAVCTRL endpoint\n"); + d->graph_started |= BIT(DPAV_CTRL_ENDPOINT - 0x20); + ret = dcp_desktop_start_ext_ep(dcp, map, DPSAC_ENDPOINT, "DPSAC"); + if (ret) + return ret; +#if IS_ENABLED(CONFIG_DRM_APPLE_AUDIO) + if (hdmi_audio) + ret = avep_init(dcp); + else +#endif + ret = dcp_desktop_start_ext_ep(dcp, map, AV_ENDPOINT, "AV"); + if (ret) + return ret; + ret = dcp_desktop_start_ext_ep(dcp, map, REMOTE_ALLOC_ENDPOINT, + "REMOTEALLOC"); + if (ret) + return ret; + + if (!dcp->phy && !d->ndyn) { + dev_warn(dcp->dev, "no DP PHY and no Type-C route: no display link\n"); + return 0; + } + + ret = ibootep_init(dcp); + if (ret) + dev_warn(dcp->dev, "Failed to start IBOOT endpoint: %d\n", ret); + + /* The DPTX port services live on EPIC2C here. */ + dptx_ret = dptxep_init_desktop(dcp, EPIC2C_ENDPOINT); + if (dptx_ret) + dev_warn(dcp->dev, "Failed to start DPTX port services: %d\n", + dptx_ret); + + if (unstable_edid) { + ret = dpavservep_init(dcp); + if (ret) + return dev_err_probe(dcp->dev, ret, + "Failed to start DPAVSERV endpoint\n"); + } + ret = dcp_desktop_start_ext_ep(dcp, map, DCP_EXPERT_ENDPOINT, "EXPERT"); + if (ret) + return ret; + ret = dcp_desktop_start_ext_ep(dcp, map, EPIC25_ENDPOINT, "EPIC25"); + if (ret) + return ret; + + if (!dptx_ret && dptx_service_available(&dcp->dptxport[0])) { + ret = dcp_dptx_connect(dcp, 0); + if (ret) + dev_info(dcp->dev, "no DPTX link at startup: %d\n", ret); + } + return 0; +} + static int dcp_start_desktop(struct apple_dcp *dcp) { struct dcp_desktop_state *d = dcp->desktop; u32 map = 0; int i, ep, ret; - if (dcp_desktop_external(dcp)) - return -EOPNOTSUPP; if (IS_ERR_OR_NULL(dcp->rtk)) return -ENODEV; @@ -2884,6 +3196,13 @@ static int dcp_start_desktop(struct apple_dcp *dcp) return dev_err_probe(dcp->dev, ret, "Failed to start IOMFB transport endpoint\n"); + if (dcp_desktop_external(dcp)) { + ret = dcp_start_desktop_external(dcp, map); + if (ret) + return ret; + goto start_iomfb; + } + for (i = 0; i < ARRAY_SIZE(dcp_desktop_panel_eps); i++) { u32 endpoint = dcp_desktop_panel_eps[i].endpoint; const char *name = dcp_desktop_panel_eps[i].name; @@ -2908,6 +3227,7 @@ static int dcp_start_desktop(struct apple_dcp *dcp) } d->graph_started |= BIT(endpoint - 0x20); } +start_iomfb: dev_info(dcp->dev, "endpoints started %#010x, skipped %#010x\n", d->graph_started, d->graph_skipped); @@ -3209,6 +3529,8 @@ void dcp_poweron(struct platform_device *pdev) dcp->desktop->trial_powered = false; return; } + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) + dcp->desktop->ext_pipe_off = false; if (iomfb_is_m4(dcp)) { ret = dcp_backlight_dpms(dcp, true); @@ -4232,16 +4554,6 @@ static int dcp_comp_bind_pipe(struct device *dev, struct device *main, void *dat if (dcp->external_prepared) return 0; - /* - * The T6041 external processors' start is not implemented yet. The - * failsafe keeps such a processor out of the DRM device without - * touching it. - */ - if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && - dcp_desktop_external(dcp)) - return dev_err_probe(dev, -EOPNOTSUPP, - "T6041 external display processor start is not implemented\n"); - /* A timed-out prior session may still scan these mappings. */ mutex_lock(&dcp->swapped_out_fbs_lock); ret = list_empty(&dcp->swapped_out_fbs) ? 0 : -EBUSY; @@ -4262,6 +4574,12 @@ static int dcp_comp_bind_pipe(struct device *dev, struct device *main, void *dat return PTR_ERR(dcp->coproc_reg); } + /* Record the state the boot loader handed the coprocessor over in. */ + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) + dev_info(dev, "coprocessor handoff: CPU_CONTROL %#x CPU_STATUS %#x\n", + readl_relaxed(dcp->coproc_reg + APPLE_DCP_COPROC_CPU_CONTROL), + readl_relaxed(dcp->coproc_reg + APPLE_DCP_COPROC_CPU_STATUS)); + /* * Display coprocessors running H17-generation firmware are left running * by the bootloader with their firmware mapped through a locked DART. @@ -4643,6 +4961,8 @@ VISIBLE_IF_KUNIT void dcp_comp_unbind(struct device *dev, struct device *main, v dcp->systemep = NULL; } + dcp_desktop_dyn_route_release(dcp); + /* * The auxiliary endpoints a desktop 26.x start brought up, still * routed while their shutdown is acknowledged. @@ -4744,6 +5064,74 @@ static int dcp_native_routes_error(struct apple_dcp *dcp, int err) return 0; } +static void dcp_desktop_phy_off(void *data) +{ + struct phy *phy = data; + + phy_power_off(phy); + phy_exit(phy); +} + +/* + * The outputs of a desktop 26.x external processor (T6041 dcpext): the + * fixed HDMI route's DP PHY is powered here, as the boot loader's working + * sequence does; a dynamic Type-C route lists the DP PHY and crossbar of + * every port (dp-phyN, dp-xbarN, apple,dptx-phys, apple,dptx-mux-index), + * and the one in DP alt mode is claimed at connect. + */ +static int dcp_desktop_probe_routes(struct apple_dcp *dcp) +{ + struct dcp_desktop_state *d = dcp->desktop; + struct device *dev = dcp->dev; + unsigned int i; + int nd, ret; + + d->dyn_active = -1; + if (!dcp_desktop_external(dcp)) + return 0; + + if (dcp->phy) { + ret = phy_init(dcp->phy); + if (ret) + return dev_err_probe(dev, ret, "Failed to initialize dp-phy\n"); + ret = phy_power_on(dcp->phy); + if (ret) { + phy_exit(dcp->phy); + return dev_err_probe(dev, ret, "Failed to power on dp-phy\n"); + } + ret = devm_add_action_or_reset(dev, dcp_desktop_phy_off, dcp->phy); + if (ret) + return ret; + msleep(25); + } + + nd = of_property_count_u32_elems(dev->of_node, "apple,dptx-phys"); + if (nd <= 0) + return 0; + if (nd > ARRAY_SIZE(d->dyn_phy)) + return dev_err_probe(dev, -EINVAL, "too many Type-C routes: %d\n", nd); + + for (i = 0; i < nd; i++) { + char name[16]; + + snprintf(name, sizeof(name), "dp-phy%u", i); + d->dyn_phy[i] = devm_phy_get(dev, name); + if (IS_ERR(d->dyn_phy[i])) + return dev_err_probe(dev, PTR_ERR(d->dyn_phy[i]), "%s\n", name); + snprintf(name, sizeof(name), "dp-xbar%u", i); + d->dyn_xbar[i] = devm_mux_control_get(dev, name); + if (IS_ERR(d->dyn_xbar[i])) + return dev_err_probe(dev, PTR_ERR(d->dyn_xbar[i]), "%s\n", name); + of_property_read_u32_index(dev->of_node, "apple,dptx-phys", i, + &d->dyn_dptx_phy[i]); + } + of_property_read_u32(dev->of_node, "apple,dptx-mux-index", &d->dyn_mux_index); + d->ndyn = nd; + dev_info(dev, "%u Type-C DP routes, crossbar state %u\n", d->ndyn, + d->dyn_mux_index); + return 0; +} + static int dcp_platform_probe_pipe(struct platform_device *pdev) { const struct apple_dcp_hw_data *hw = of_device_get_match_data(&pdev->dev); @@ -4928,6 +5316,12 @@ static int dcp_platform_probe_pipe(struct platform_device *pdev) } dcp->fixed_phy = dcp->phy; + if (fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) { + ret = dcp_desktop_probe_routes(dcp); + if (ret) + return ret; + } + bitmap_zero(dcp->iomfb_surfaces, DCP_MAX_PLANES); if (!of_property_present(dev->of_node, "apple,iomfb-surfaces")) num_surfs = 0; @@ -5038,6 +5432,10 @@ static int dcp_platform_probe_pipe(struct platform_device *pdev) } if (IS_ERR(dcp->dp2hdmi_pwren)) return PTR_ERR(dcp->dp2hdmi_pwren); + /* The converter rails must be stable before DCP requests the link. */ + if (fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && + (dcp->hdmi_pwren || dcp->dp2hdmi_pwren)) + msleep(100); /* * A DCP may have both a fixed HDMI/DP route and allocatable Type-C @@ -5211,10 +5609,10 @@ static int dcp_platform_probe(struct platform_device *pdev) void *group; int ret; - /* Not driven yet: stay off the hardware unless asked for. */ + /* t6041_dcpext=0: stay off the external processors' hardware. */ if (of_device_is_compatible(dev->of_node, "apple,t6041-dcpext") && !t6041_dcpext) { - dev_info(dev, "T6041 external display processor left alone (appledrm.t6041_dcpext=0)\n"); + dev_info(dev, "T6041 external display processor left alone (t6041_dcpext=0)\n"); return -ENODEV; } diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index c6bf3b8d35fe6..df8a70b6f2a20 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -2788,6 +2788,25 @@ void dcp_flush(struct drm_crtc *crtc, struct drm_atomic_commit *commit) iomfb_v26_6_flush(dcp, crtc, commit); return; } + + /* + * Desktop 26.x external display replugged while the compositor kept + * its CRTC active: no atomic enable follows, and the firmware, which + * powered the pipe off with the unplug, swallows every swap. Run the + * power-on and mode set the enable would have run, once. + */ + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && + dcp->desktop->ext_pipe_off && !dcp->main_display && + !READ_ONCE(dcp->mode_state.valid) && dcp->connector && + READ_ONCE(dcp->connector->connected)) { + struct drm_crtc_state *cs = drm_atomic_get_new_crtc_state(commit, crtc); + + if (cs && cs->active && cs->mode.hdisplay) { + dev_dbg(dcp->dev, "re-powering the external pipe of an active CRTC\n"); + dcp_poweron(pdev); + dcp_crtc_atomic_modeset(crtc, commit); + } + } if (dcp->fw_compat == DCP_FIRMWARE_V_14_7) { iomfb_v14_7_flush(dcp, crtc, commit); return; diff --git a/drivers/gpu/drm/apple/iomfb_template.c b/drivers/gpu/drm/apple/iomfb_template.c index 693dca78f831b..246498a8e843c 100644 --- a/drivers/gpu/drm/apple/iomfb_template.c +++ b/drivers/gpu/drm/apple/iomfb_template.c @@ -2391,6 +2391,9 @@ dcpep_cb_hotplug_v26_6(struct apple_dcp *dcp, struct dcp_hotplug_req_v26_6 *req) (!connected || (dcp->connector && READ_ONCE(dcp->connector->connected) != !!connected))) dcp->desktop->mode_live = false; + /* The firmware shuts an external pipe down with its display. */ + if (!dcp->main_display && !connected) + dcp->desktop->ext_pipe_off = true; dcpep_cb_hotplug(dcp, &connected); diff --git a/include/linux/soc/apple/dp-tunnel.h b/include/linux/soc/apple/dp-tunnel.h index 194a238927190..0732ca5aabdf4 100644 --- a/include/linux/soc/apple/dp-tunnel.h +++ b/include/linux/soc/apple/dp-tunnel.h @@ -57,7 +57,14 @@ int apple_atc_dp_tunnel_open(struct phy *phy); * appledrm can route a DCP that lists several Type-C DP PHYs to the one that * has a DisplayPort sink plugged. */ +#if IS_ENABLED(CONFIG_PHY_APPLE_T6040_USB2) bool apple_t6040_phy_dp_alt(struct phy *phy); +#else +static inline bool apple_t6040_phy_dp_alt(struct phy *phy) +{ + return false; +} +#endif /* * Display crossbar: take the connection of an already selected output down or From 8406d1e7a10ea8dbac0b4f35d16c14e9a2955d09 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Sun, 25 Oct 2026 23:51:14 +1100 Subject: [PATCH 10/12] drm/apple: look up the crossbar link and T6040 DP alt helpers at runtime The desktop 26.x DPTX path called apple_dpxbar_link_up/down() and apple_t6040_phy_dp_alt() directly. That made appledrm.ko depend on the crossbar and T6040 PHY modules on every SoC, and DRM_APPLE=y with the crossbar built as a module failed to link. The PHY dependency was also expressed as "depends on PHY_APPLE_T6040_USB2 || !PHY_APPLE_T6040_USB2", which silently demotes DRM_APPLE=y to m wherever the PHY is a module. Use the existing symbol_get() based dcp_dpxbar_link() for the crossbar, look the PHY helper up the same way, and drop the Kconfig dependency. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/Kconfig | 1 - drivers/gpu/drm/apple/dcp-fabric.c | 2 +- drivers/gpu/drm/apple/dcp-fabric.h | 1 + drivers/gpu/drm/apple/dcp.c | 16 +++++++++++++++- drivers/gpu/drm/apple/dptxep.c | 3 +-- 5 files changed, 18 insertions(+), 5 deletions(-) diff --git a/drivers/gpu/drm/apple/Kconfig b/drivers/gpu/drm/apple/Kconfig index d7837413e770f..9685682f2a87e 100644 --- a/drivers/gpu/drm/apple/Kconfig +++ b/drivers/gpu/drm/apple/Kconfig @@ -5,7 +5,6 @@ config DRM_APPLE depends on ARCH_APPLE || COMPILE_TEST depends on APPLE_RTKIT depends on TYPEC || !TYPEC - depends on PHY_APPLE_T6040_USB2 || !PHY_APPLE_T6040_USB2 depends on OF_ADDRESS select APPLE_PMP_REPORT select OF_DYNAMIC diff --git a/drivers/gpu/drm/apple/dcp-fabric.c b/drivers/gpu/drm/apple/dcp-fabric.c index 976830cef7935..eb4d0363afb6b 100644 --- a/drivers/gpu/drm/apple/dcp-fabric.c +++ b/drivers/gpu/drm/apple/dcp-fabric.c @@ -1576,7 +1576,7 @@ static bool dcp_t602x_tunnel_route(struct apple_dcp_typec_route *route) * Crossbar connection up/down, looked up at runtime so appledrm does not * require the crossbar driver to be built. */ -static int dcp_dpxbar_link(struct mux_control *mux, bool up) +int dcp_dpxbar_link(struct mux_control *mux, bool up) { typeof(&apple_dpxbar_link_up) fn; int ret; diff --git a/drivers/gpu/drm/apple/dcp-fabric.h b/drivers/gpu/drm/apple/dcp-fabric.h index 62b1a72d5946a..2647ddff4653d 100644 --- a/drivers/gpu/drm/apple/dcp-fabric.h +++ b/drivers/gpu/drm/apple/dcp-fabric.h @@ -90,6 +90,7 @@ void dcp_unlink(struct drm_device *drm); int dcp_tunnel_crossbar_up(struct apple_dcp *dcp, u32 unit); int dcp_tunnel_crossbar_down(struct apple_dcp *dcp, u32 unit); int dcp_direct_crossbar_link(struct apple_dcp *dcp, bool up); +int dcp_dpxbar_link(struct mux_control *mux, bool up); /* * T6030 ATC PHY: keep the source owning the pixel clock across rate changes * and stop (linux/soc/apple/dp-tunnel.h, 170-usb). Declared here until that diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 3494c47de9bca..3b3ea4abac3b1 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -1902,15 +1902,29 @@ static int dcp_dptx_connect_tile(struct apple_dcp *dcp) static int dcp_desktop_dyn_route_select_locked(struct apple_dcp *dcp) { struct dcp_desktop_state *d = dcp->desktop; + typeof(&apple_t6040_phy_dp_alt) dp_alt; unsigned int i; int ret; lockdep_assert_held(&dcp->hpd_mutex); if (!d->ndyn || d->dyn_active >= 0) return 0; + /* Looked up at runtime so appledrm does not require the T6040 PHY driver. */ +#if IS_ENABLED(CONFIG_PHY_APPLE_T6040_USB2) + dp_alt = symbol_get(apple_t6040_phy_dp_alt); +#else + dp_alt = NULL; +#endif + if (!dp_alt) { + dev_dbg(dcp->dev, "T6040 PHY driver not loaded\n"); + return -ENODEV; + } for (i = 0; i < d->ndyn; i++) - if (apple_t6040_phy_dp_alt(d->dyn_phy[i])) + if (dp_alt(d->dyn_phy[i])) break; +#if IS_ENABLED(CONFIG_PHY_APPLE_T6040_USB2) + symbol_put(apple_t6040_phy_dp_alt); +#endif if (i == d->ndyn) { dev_dbg(dcp->dev, "no Type-C port in DP alt mode\n"); return -ENODEV; diff --git a/drivers/gpu/drm/apple/dptxep.c b/drivers/gpu/drm/apple/dptxep.c index 92525cb250d15..79b26f7caeb44 100644 --- a/drivers/gpu/drm/apple/dptxep.c +++ b/drivers/gpu/drm/apple/dptxep.c @@ -490,8 +490,7 @@ static int dptxport_compact_xbar_link(struct apple_epic_service *service, bool u if (!READ_ONCE(service->ep->compact) || !dcp->xbar) return 0; - ret = up ? apple_dpxbar_link_up(dcp->xbar) : - apple_dpxbar_link_down(dcp->xbar); + ret = dcp_dpxbar_link(dcp->xbar, up); dev_dbg(dcp->dev, "DPTXPort: crossbar link %s: %d\n", up ? "up" : "down", ret); return ret; } From 9c1406a9537403957bba140930d50116a0f4d1eb Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Sun, 25 Oct 2026 23:51:14 +1100 Subject: [PATCH 11/12] phy: apple: t6040: never leave an error pointer in the tunables When a USB3 tunable fails to parse, t6040_usb3_probe() falls back to USB2 only but leaves the ERR_PTR in tphy->tunables[]. The node is then treated as DP only, and t6040_dp_probe() skips an absent property without touching that slot. A later DP power on hands the error pointer to apple_tunable_apply(), which only checks for NULL. Clear the slot on the USB3 failure path and on the DP skip path, so an unparsed tunable is always NULL and simply not applied. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/phy/apple/t6040-usb2.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/drivers/phy/apple/t6040-usb2.c b/drivers/phy/apple/t6040-usb2.c index 9ff40f1c11019..868d4304b2395 100644 --- a/drivers/phy/apple/t6040-usb2.c +++ b/drivers/phy/apple/t6040-usb2.c @@ -1557,6 +1557,7 @@ static int t6040_dp_probe(struct t6040_usb2_phy *tphy) if (!of_property_present(dev->of_node, prop) || !tphy->res[t6040_usb3_tunable_list[i].region]) { dev_warn(dev, "DP: no %s\n", prop); + tphy->tunables[i] = NULL; continue; } struct resource *res = tphy->res[t6040_usb3_tunable_list[i].region]; @@ -1725,6 +1726,7 @@ static int t6040_usb3_probe(struct t6040_usb2_phy *tphy, struct platform_device if (IS_ERR(tphy->tunables[i])) { dev_warn(dev, "USB3: %s: %pe, USB2 only\n", t6040_usb3_tunable_list[i].prop, tphy->tunables[i]); + tphy->tunables[i] = NULL; return 0; } } From 7efa9c4fa0c1611ef3a2d5b7e78b6b07691f5173 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 00:27:50 +1100 Subject: [PATCH 12/12] selftests: apple-dcp: follow the desktop 26.6 firmware path The T6041 display series added DCP_FIRMWARE_V_26_6_DESKTOP, the fw_compat and desktop checks it implies, the DCP_FW_H17() lineage test, compact EPIC in the AFK sender, receive worker and quiesce, a VRR filter and a gamma size check in the extracted functions. The fixtures lacked the matching mock fields, defines and helpers, and the lookup_mode() extraction matched a new call site instead of the definition, so nine units no longer compiled. Add them so every scenario stays off the desktop path (fw_compat not desktop, desktop NULL, compact false, no colour-management change), with stubs that assert if it is reached, so the existing paths are checked exactly as before. Assisted-by: LLM Signed-off-by: Ryan Murray --- .../selftests/drivers/gpu/apple-dcp/common.py | 12 +++++++++ .../selftests/drivers/gpu/apple-dcp/edid.py | 10 ++++++-- .../drivers/gpu/apple-dcp/lifecycle.py | 24 +++++++++++++++--- .../selftests/drivers/gpu/apple-dcp/rate.py | 8 +++--- .../drivers/gpu/apple-dcp/recovery.py | 25 +++++++++++++++++++ 5 files changed, 71 insertions(+), 8 deletions(-) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/common.py b/tools/testing/selftests/drivers/gpu/apple-dcp/common.py index 91ccd2a84123d..849a166c88fb8 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/common.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/common.py @@ -194,6 +194,18 @@ def apple_function(root, name): return '' +def firmware_lineage(root): + """The 26.x lineage tests of version_utils.h (DCP_FW_H17() and friends). + + Units define their own DCP_FW_VERSION(); a plain 26 is the H17 lineage, + since only a desktop template instance carries DCP_FW_DESKTOP. + """ + text = (root / 'drivers/gpu/drm/apple/version_utils.h').read_text() + names = ('DCP_FW_DESKTOP', 'DCP_FW_VERSION_DESKTOP', 'DCP_FW_H17', 'DCP_FW_DESKTOP_26') + return ''.join(d + '\n' for d in source_defines(text) + if re.match(r'#define (\w+)', d).group(1) in names) + + def internal_inline(root, name): """A static inline helper from dcp-internal.h, marked unused; '' if absent. diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/edid.py b/tools/testing/selftests/drivers/gpu/apple-dcp/edid.py index dfd6d61bcf74f..f20891027a2ab 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/edid.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/edid.py @@ -2,7 +2,7 @@ # Copyright 2026 Ryan Murray """Actual native EDID admission, service epochs, stale replies and RX progress.""" from pathlib import Path -from common import block, source_defines, apple_function, internal_inline +from common import block, source_defines, apple_function, internal_inline, firmware_lineage def rebind(root): base = root / 'drivers/gpu/drm/apple' @@ -595,6 +595,8 @@ def function(file, signature): #define DCP_FIRMWARE_H17P 2 /* The 14.7 open-before-EDID handshake is for T6030 external processors only. */ #define DCP_FIRMWARE_V_14_7 (DCP_FIRMWARE_H17P + 1) +/* Nor is any processor here a desktop 26.6 one: it has no auxiliary graph. */ +#define DCP_FIRMWARE_V_26_6_DESKTOP (DCP_FIRMWARE_H17P + 3) #define AV_ENDPOINT 0x29 #define SYSTEM_ENDPOINT 0x20 #define DISP0_ENDPOINT 0x23 @@ -617,6 +619,7 @@ def function(file, signature): u64 typec_generation; int fw_compat; void *dev; + struct dcp_desktop_state { struct apple_dcp_afkep *graph[0x20]; } *desktop; }; struct drm_edid { int identity; }; static unsigned int reads, allocations, legacy_reads, receives; @@ -793,6 +796,8 @@ def function(text, signature): #define dev_dbg_ratelimited(...) ((void)0) #define trace_dcp_recv_msg(...) ((void)0) #define DCP_FIRMWARE_H17P 2 +/* The H17P external processor is not a desktop 26.6 one. */ +#define DCP_FIRMWARE_V_26_6_DESKTOP (DCP_FIRMWARE_H17P + 3) #define IOMFB_ENDPOINT 0x37 #define AV_ENDPOINT 0x29 #define SYSTEM_ENDPOINT 0x20 @@ -836,6 +841,7 @@ def function(text, signature): struct apple_connector *connector; struct apple_dcp_afkep *dcpavservep,*dptxep,*avep,*systemep,*ibootep,*dptx_controller_ep; struct { struct mutex lock; bool stopped; void *owner; } iomfb; + struct dcp_desktop_state { struct apple_dcp_afkep *graph[0x20]; } *desktop; }; struct afk_receive_message_work { struct apple_dcp_afkep *ep; u64 message; struct work_struct work; }; struct apple_rtkit_ops { void (*recv_message)(void *,u8,u64); }; @@ -994,7 +1000,7 @@ def function(text, signature): old_join=''' assert(!pthread_join(rx,NULL) && !pthread_join(afk,NULL) && !pthread_join(dimensions,NULL));''' assert old_join in suffix suffix=suffix.replace(old_join,old_join+'\n assert(!pthread_join(hotplug,NULL));',1) - return prefix + '\n'.join(parts) + suffix + return prefix + firmware_lineage(root) + '\n'.join(parts) + suffix def startup(root): diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py index e604cf378e221..6b088b1be6ccd 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py @@ -3,7 +3,7 @@ """Actual disconnect authority, DWC3 teardown and firmware resource admission.""" from pathlib import Path import re -from common import block, apple_function, internal_inline, atc_mux_without_pm, source_defines, ATC_POST_TUNABLE, tipd_pm_header +from common import block, apple_function, internal_inline, atc_mux_without_pm, source_defines, ATC_POST_TUNABLE, tipd_pm_header, firmware_lineage def disconnect(root): # Copyright 2026 Ryan Murray @@ -459,6 +459,15 @@ def function(file, name): static void __attribute__((unused)) dcp_relink_link_up(int *s, u64 generation, unsigned long now) { (void)s; (void)generation; (void)now; assert(!"legacy relink timestamp in an H17P scenario"); } """ + # The desktop 26.6 (T6041) connect has its own path; an H17P display is + # never on that firmware. + if 'dcp_desktop_dptx_connect(' in funcs: + old = ' struct dcp_announce announce;\n' + assert prefix.count(old) == 1 + prefix = prefix.replace(old, old + ' int fw_compat; /* zero: not the desktop 26.6 firmware */\n') + prefix += ('#define DCP_FIRMWARE_V_26_6_DESKTOP 100\n' + 'static int __attribute__((unused)) dcp_desktop_dptx_connect(struct apple_dcp *d, u32 port)\n' + '{ (void)d; (void)port; assert(!"desktop 26.6 connect in an H17P scenario"); return -EINVAL; }\n') # How long a held mux has been retried is measured on the driver's clock. if 'jiffies_to_msecs(jiffies)' in funcs: shim += ('\nstatic unsigned long jiffies;\n' @@ -492,7 +501,8 @@ def swap(old, new): ' void *phy, *dev, *active_typec_route;\n' ' int dptx_die, dptx_phy, dptx_dfp_port, connector_type;\n' ' bool external, external_link_ready, crashed;\n' - ' bool typec_work_enabled; int fw_compat; struct { bool fw_14_7_endpoints; } hw;\n' + ' bool typec_work_enabled;' + ('' if '\n int fw_compat;' in unit else ' int fw_compat;') + + ' struct { bool fw_14_7_endpoints; } hw;\n' ' struct { bool active; } split;') old_raw = unit[unit.index('static int dcp_dptx_connect_raw('):] old_raw = old_raw[:old_raw.index('return raw_error; }') + len('return raw_error; }')] @@ -1387,6 +1397,9 @@ def function(source, marker): { (void)n; assert(!"T6030 dcpext in a J700 scenario"); return false; } static int __attribute__((unused)) iomfb_v14_7_external_prepare(struct apple_dcp *d) { (void)d; assert(!"T6030 external processor in a J700 scenario"); return -ENODEV; } +/* Nor a T6041 dcpext. */ +static bool __attribute__((unused)) dcp_t6041_ext_enabled(void) +{ assert(!"T6041 dcpext in a J700 scenario"); return false; } static void __attribute__((unused)) iomfb_v14_7_external_refuse(struct apple_dcp *d, const char *why) { (void)d; (void)why; assert(!"T6030 external processor in a J700 scenario"); } struct drm_device { int unused; }; @@ -1660,7 +1673,7 @@ def vrr(root): struct platform_device { struct apple_dcp *data; }; struct drm_crtc { struct platform_device *dcp; void *primary; }; struct drm_display_mode { int unused; }; -struct drm_crtc_state { bool enable, vrr_enabled, modeset; struct drm_display_mode mode; void *ctm, *degamma_lut, *gamma_lut; unsigned plane_mask; }; +struct drm_crtc_state { bool enable, vrr_enabled, modeset, color_mgmt_changed; struct drm_display_mode mode; void *ctm, *degamma_lut, *gamma_lut; unsigned plane_mask; }; /* mainline 7.2 renamed struct drm_atomic_state to struct drm_atomic_commit */ struct drm_atomic_commit { struct drm_crtc_state crtc; }; #define to_apple_crtc(x) (x) @@ -1683,6 +1696,11 @@ def vrr(root): static unsigned drm_plane_mask(void *p) { (void)p; assert(false); return 0; } static bool drm_mode_equal(const struct drm_display_mode *a,const struct drm_display_mode *b) { (void)a; (void)b; assert(false); return false; } +/* These commits change no colour management, so no gamma table is sized. */ +static int __attribute__((unused)) drm_color_lut_size(const void *blob) +{ (void)blob; assert(!"gamma LUT without a colour-management change"); return 0; } +static unsigned int __attribute__((unused)) dcp_gamma_lut_size(struct platform_device *p) +{ (void)p; assert(!"gamma LUT without a colour-management change"); return 0; } /* Route-follow is for Type-C displays; this DCP has no Type-C routes. */ static bool __attribute__((unused)) typec_follow_crtc = true; static int __attribute__((unused)) dcp_typec_follow_check(struct apple_dcp *d, struct drm_crtc *c, diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py b/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py index b7b816e07a976..797452f3fc226 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py @@ -3,7 +3,7 @@ """Actual parser, bounded rate admission, catalog replacement and HPD.""" from pathlib import Path import copy, re, struct -from common import block, source_defines, apple_function, internal_inline +from common import block, source_defines, apple_function, internal_inline, firmware_lineage def parser_fixture(root): @@ -671,7 +671,9 @@ def publication(root): #define DCP_FIRMWARE_V_14_7 (DCP_FIRMWARE_H17P + 1) #define DCP_FIRMWARE_V_14_7_TEMPLATE (DCP_FIRMWARE_H17P + 2) #endif -'''+internal_inline(root,'dcp_fw_is_14_7')+r'''/* Off T6030 machines the direct crossbar output is not clocked separately. */ +/* Nor the desktop 26.6 firmware, the one that withholds VRR modes. */ +#define DCP_FIRMWARE_V_26_6_DESKTOP (DCP_FIRMWARE_H17P + 3) +'''+firmware_lineage(root)+internal_inline(root,'dcp_fw_is_14_7')+r'''/* Off T6030 machines the direct crossbar output is not clocked separately. */ static int __attribute__((unused)) dcp_direct_crossbar_link(struct apple_dcp *d,bool up) { (void)d;(void)up;return 0; } ''' transport = '\n'.join(block(av,marker) for marker in [ @@ -692,7 +694,7 @@ def publication(root): + ''.join(apple_function(root, name) for name in ('dcp_queue_hotplug', 'dcp_modes_for_connector', 'apple_connector_invalidate_edid', 'apple_connector_edid_set_live')) + mode_helpers) - publication = block(iomfb,'static void dcp_clear_connector_edid(')+'\n'+block(iomfb,'static void dcp_consume_edid_invalidation(')+mode_helpers+block(iomfb,'int dcp_get_modes(')+block(iomfb,re.search(r'^[^\n;]*\blookup_mode\(',iomfb,re.M).start()) + publication = block(iomfb,'static void dcp_clear_connector_edid(')+'\n'+block(iomfb,'static void dcp_consume_edid_invalidation(')+mode_helpers+block(iomfb,'static bool dcp_mode_vrr_supported(')+'\n'+block(iomfb,'int dcp_get_modes(')+block(iomfb,re.search(r'^[^\n;(]*\blookup_mode\(',iomfb,re.M).start()) # Drive malformed replacements through the same chunk/end ingress as firmware. # The dictionary cache is a boundary substitute and takes ownership of the blob. receipt = r''' diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/recovery.py b/tools/testing/selftests/drivers/gpu/apple-dcp/recovery.py index 5fa3a4900ce18..7b51603ae2139 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/recovery.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/recovery.py @@ -258,6 +258,18 @@ def extract(text,signature): static bool apple_rtkit_is_running(void *r) { (void)r;assert(!"T6030 dcpext diagnostic on H17P");return false; } static bool apple_rtkit_is_crashed(void *r) { (void)r;assert(!"T6030 dcpext diagnostic on H17P");return false; } '''+sender + if 'afk_send_compact_command(' in sender: + # Compact EPIC is the desktop 26.6 (T6041) wire; this H17P endpoint + # never switches to it, so the classic sender runs. + unit=unit.replace('struct apple_epic_service services[AFK_MAX_CHANNEL]; };', + 'struct apple_epic_service services[AFK_MAX_CHANNEL];bool compact; };',1) + sender='''#ifndef DCP_FIRMWARE_V_26_6_DESKTOP + #define DCP_FIRMWARE_V_26_6_DESKTOP (DCP_FIRMWARE_H17P + 3) + #endif + static u32 afk_compact_next_status(struct apple_epic_service *s) { (void)s;assert(!"compact EPIC on H17P");return 0; } + static int afk_send_compact_command(struct apple_epic_service *s,u8 type,const void *payload,size_t payload_len,void *output,size_t output_len,u32 request_status,u32 *retcode,size_t *copied,unsigned long timeout) + { (void)s;(void)type;(void)payload;(void)payload_len;(void)output;(void)output_len;(void)request_status;(void)retcode;(void)copied;(void)timeout;assert(!"compact EPIC on H17P");return -EIO; } + '''+sender unit+=sender+'\n'+tests out.mkdir(parents=True,exist_ok=True) @@ -343,6 +355,19 @@ def run(name,text,cases): } ''' af=(root/'drivers/gpu/drm/apple/afk.c').read_text() + if 'static bool afk_compact_capable(' in af: + # The desktop 26.x ring messages and compact state; the actual + # capability check keeps this H17P endpoint on the classic path. + import re + queued=queued.replace('bool compact; };','bool compact;void *tx_wq,*compact_rx_data; };',1) + for message in ('RBEP_SPACE_AVAILABLE','RBEP_WAIT_FOR_SPACE'): + queued+='#define %s %s\n'%(message,re.search(r'\b'+message+r'\s*=\s*(\w+)',af).group(1)) + queued+=r''' + #define FIELD_PREP(mask,val) ((val)&(mask)) + #define dma_wmb() ((void)0) + static void afk_send(struct apple_dcp_afkep *e,u64 m) { (void)e;(void)m;assert(!"compact ring message on H17P"); } + static void afk_cancel_commands(struct apple_dcp_afkep *e) { assert(e && !e->compact); } + '''+extract(af,'static bool afk_compact_capable(')+'\n' for signature in ['static inline unsigned int afk_ring_drain_budget(', 'static inline unsigned int afk_ring_drain(']: queued+=extract(afh,signature)+'\n' for signature in ['static bool afk_recv_entry(', 'static void afk_recv_drain(',