From a8dd093ae7eaf129037658339b43bb3cf7b25e67 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sat, 31 Oct 2026 11:54:45 +0100 Subject: [PATCH 01/17] drm/asahi: g18: start stage 10 without a G18GFX01 capture Stage 10 (the DRM render node) loaded asahi/g18-26a428-color-a-v2.bin, a private capture of a macOS render job, only to build the H1 job it then never runs: every application frame clears the encoded command and rebuilds it from Mesa's parameters ("no capture bytes execute"). When the file is absent at stage 10, map the colour-a page layout with zeroed pages (output pages poisoned as before) and skip the H1 encode. The capture is still required by stage 9 and still preferred when present. Tested on the J873g: firmware live, G18_DRM_READY, GLES clear and triangle read back exactly at 64, 256 and 1920 square; sway, alacritty and mpv render on the GPU. Signed-off-by: Thomas Glanzmann (cherry picked from commit 150ba9aa9ce8109a6005a61c3078c3d4fc6e20fe) --- .../gpu/drm/asahi/g18/fw/graphics_payload.rs | 7 ++ drivers/gpu/drm/asahi/g18/initdata/mod.rs | 3 +- .../gpu/drm/asahi/g18/runtime/graphics_job.rs | 1 + .../drm/asahi/g18/runtime/graphics_memory.rs | 74 +++++++++++++------ 4 files changed, 63 insertions(+), 22 deletions(-) diff --git a/drivers/gpu/drm/asahi/g18/fw/graphics_payload.rs b/drivers/gpu/drm/asahi/g18/fw/graphics_payload.rs index 71a82541685e0..5e60e8bf37b84 100644 --- a/drivers/gpu/drm/asahi/g18/fw/graphics_payload.rs +++ b/drivers/gpu/drm/asahi/g18/fw/graphics_payload.rs @@ -78,6 +78,13 @@ const WITNESS_RANGES: [(u64, usize, u32); 25] = [ (0x100_002d_8000, 0xc0000, 4), ]; +/// Every colour-a page (address, captured usage) in file order, for the +/// stage-10 layout that is mapped without a capture file. +pub(crate) fn color_a_pages() -> impl Iterator { + RANGES.iter().flat_map(|&(base, length, usage)| + (0..length).step_by(PAGE_BYTES).map(move |offset| (base + offset as u64, usage))) +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum Capture { ColorA = 1, ColorB = 2, GearsG1 = 3, FragmentWitness = 4 } diff --git a/drivers/gpu/drm/asahi/g18/initdata/mod.rs b/drivers/gpu/drm/asahi/g18/initdata/mod.rs index 05c0d1c17633e..50b8901e252a4 100644 --- a/drivers/gpu/drm/asahi/g18/initdata/mod.rs +++ b/drivers/gpu/drm/asahi/g18/initdata/mod.rs @@ -424,7 +424,8 @@ impl G18FirmwareMemory { let pool = self.uma_pool.prepare_binding()?; self.uma_pool.load_hardware_descriptor(dev, &mut self.uma, pool, j873_sma_device_words(2, 6)?)?; job.parameter_buffer().load_row(&mut self.pbdesc)?; - job.encode_h1(dev)?; + // Without a capture (stage 10 only) the first application frame encodes. + if !job.layout_only_payload() { job.encode_h1(dev)?; } Ok(pool) } diff --git a/drivers/gpu/drm/asahi/g18/runtime/graphics_job.rs b/drivers/gpu/drm/asahi/g18/runtime/graphics_job.rs index de7e2cd51f5d2..6080cd3bf4b05 100644 --- a/drivers/gpu/drm/asahi/g18/runtime/graphics_job.rs +++ b/drivers/gpu/drm/asahi/g18/runtime/graphics_job.rs @@ -136,6 +136,7 @@ impl G18GraphicsJob { } pub(crate) fn vm(&self) -> &mmu::Vm { self.program.vm() } + pub(crate) fn layout_only_payload(&self) -> bool { self.program.layout_only_payload() } pub(crate) fn context_id(&self) -> u8 { self.program.context_id() } pub(crate) fn scheduler_record(&self) -> Result { Ok(self.scheduler.binding(0)?.record) } pub(crate) fn queues(&mut self) -> &mut G18GraphicsQueues { &mut self.queues } diff --git a/drivers/gpu/drm/asahi/g18/runtime/graphics_memory.rs b/drivers/gpu/drm/asahi/g18/runtime/graphics_memory.rs index 0a9a9504925b2..10c9573166088 100644 --- a/drivers/gpu/drm/asahi/g18/runtime/graphics_memory.rs +++ b/drivers/gpu/drm/asahi/g18/runtime/graphics_memory.rs @@ -30,49 +30,81 @@ impl G18GraphicsMemory { _ => return Err(EINVAL), }; if !uat.is_t8152_geometry() || mmu::UAT_PGSZ != payload::PAGE_BYTES { return Err(ENODEV); } - let firmware = Firmware::request(name, dev.as_ref().as_ref())?; + let firmware = match Firmware::request(name, dev.as_ref().as_ref()) { + Ok(firmware) => firmware, + // Stage 10 executes no capture bytes: every application command + // is rebuilt from Mesa's parameters. It needs only the colour-a + // page layout, which is mapped here with zeroed contents. + Err(e) if e == ENOENT && profile == 1 + && *crate::module_parameters::g18_initdata_test.value() == 10 => + return Self::layout_only(dev, uat), + Err(e) => return Err(e), + }; let input = payload::GraphicsPayload::parse(firmware.data()).map_err(|_| EINVAL)?; if input.capture() as u32 != profile { return Err(EINVAL); } + let (context, aliases, pages) = Self::map_pages(dev, uat, + (0..input.page_count()).map(|index| input.page(index).map(|page| (page.address, Some(page.bytes)))))?; + let mut command = KVec::new(); + command.extend_from_slice(input.command(), GFP_KERNEL)?; + let owner = Self { context, aliases, vdm_alias: None, ppp_aliased: false, pages, capture: input.capture(), command, + body_digest: input.body_digest(), command_digest: payload::fnv64(input.command()) }; + fence(Ordering::SeqCst); + dev_info!(dev.as_ref(), "G18_RENDER_PAYLOAD profile={} pages={} low_aliases={} body_fnv={:#018x} command_fnv={:#018x} context={} roots={:x?}; unpublished\n", + profile, owner.pages.len(), owner.aliases.len(), owner.body_digest, owner.command_digest, + owner.context_id(), owner.roots()); + Ok(owner) + } + + /// The colour-a VM layout without its capture: zeroed pages, no command. + fn layout_only(dev: &AsahiDrmDevice, uat: &mmu::Uat) -> Result { + let (context, aliases, pages) = Self::map_pages(dev, uat, + payload::color_a_pages().map(|(address, _)| Some((address, None))))?; + let owner = Self { context, aliases, vdm_alias: None, ppp_aliased: false, pages, + capture: payload::Capture::ColorA, command: KVec::new(), body_digest: 0, command_digest: 0 }; + fence(Ordering::SeqCst); + dev_info!(dev.as_ref(), "G18_RENDER_PAYLOAD layout-only colour-a pages={} low_aliases={} context={} roots={:x?}; no capture, stage 10 encodes from Mesa only\n", + owner.pages.len(), owner.aliases.len(), owner.context_id(), owner.roots()); + Ok(owner) + } + + /// True when no capture was loaded: there is no H1 command to encode. + pub(crate) fn layout_only_payload(&self) -> bool { self.command.is_empty() } + + fn map_pages<'a>(dev: &AsahiDrmDevice, uat: &mmu::Uat, + sources: impl Iterator)>>) + -> Result<(mmu::G18ExecutionContext, KVec, KVec)> { let vm = uat.new_t8152_compute_fixture_vm()?; let context = uat.prepare_t8152_execution_context(&vm)?; let mut pages = KVec::new(); let mut aliases = KVec::new(); - for index in 0..input.page_count() { - let source = input.page(index).ok_or(EINVAL)?; + for source in sources { + let (address, bytes) = source.ok_or(EINVAL)?; // Captured usage flags are not PTE permissions. These private // fixture mappings have an explicit diagnostic RW policy. - let mut page = MappedObject::new_wc_at(dev, &vm, source.address, + let mut page = MappedObject::new_wc_at(dev, &vm, address, payload::PAGE_BYTES, mmu::PROT_GPU_SHARED_RW)?; page.with_bytes_mut(|out| { - out.copy_from_slice(source.bytes); - if (payload::OUTPUT..payload::OUTPUT + payload::OUTPUT_BYTES).contains(&source.address) { + match bytes { Some(bytes) => out.copy_from_slice(bytes), None => out.fill(0) } + if (payload::OUTPUT..payload::OUTPUT + payload::OUTPUT_BYTES).contains(&address) { out.fill(0xa5); } Ok(()) })?; - if source.address < 0x78000 { - aliases.push(page.map_alias_at(&vm, va::COMPACT_BIAS + source.address, + if address < 0x78000 { + aliases.push(page.map_alias_at(&vm, va::COMPACT_BIAS + address, mmu::PROT_GPU_SHARED_RW)?, GFP_KERNEL)?; for delta in [0, va::PAGE - 1] { - if vm.translate_iova(source.address + delta)? - != vm.translate_iova(va::COMPACT_BIAS + source.address + delta)? { + if vm.translate_iova(address + delta)? + != vm.translate_iova(va::COMPACT_BIAS + address + delta)? { return Err(EFAULT); } } } - vm.translate_iova(source.address)?; - vm.translate_iova(source.address + va::PAGE - 1)?; + vm.translate_iova(address)?; + vm.translate_iova(address + va::PAGE - 1)?; pages.push(page, GFP_KERNEL)?; } - let mut command = KVec::new(); - command.extend_from_slice(input.command(), GFP_KERNEL)?; - let owner = Self { context, aliases, vdm_alias: None, ppp_aliased: false, pages, capture: input.capture(), command, - body_digest: input.body_digest(), command_digest: payload::fnv64(input.command()) }; - fence(Ordering::SeqCst); - dev_info!(dev.as_ref(), "G18_RENDER_PAYLOAD profile={} pages={} low_aliases={} body_fnv={:#018x} command_fnv={:#018x} context={} roots={:x?}; unpublished\n", - profile, owner.pages.len(), owner.aliases.len(), owner.body_digest, owner.command_digest, - owner.context_id(), owner.roots()); - Ok(owner) + Ok((context, aliases, pages)) } pub(crate) fn vm(&self) -> &mmu::Vm { self.context.vm() } From 09bdc006319f90bfce011b80f0826cb001adc6f6 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sat, 31 Oct 2026 11:54:45 +0100 Subject: [PATCH 02/17] drm/asahi: g18: log application remaps only with g18_kick_log With two clients (a compositor and a video player) every frame of each is a new client to the single application job, so G18_APP_VM and six G18_APP_TARGET lines were printed per frame, about 330 lines a second, synchronously to the boot console. Print them with g18_kick_info! like the other per-kick lines. Signed-off-by: Thomas Glanzmann (cherry picked from commit 79886333e01497b5cd2c5afa20bc1fc58d6e3546) --- drivers/gpu/drm/asahi/g18/runtime/graphics_job_application.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/gpu/drm/asahi/g18/runtime/graphics_job_application.rs b/drivers/gpu/drm/asahi/g18/runtime/graphics_job_application.rs index 5a43ad3816e62..3eb58cce499b9 100644 --- a/drivers/gpu/drm/asahi/g18/runtime/graphics_job_application.rs +++ b/drivers/gpu/drm/asahi/g18/runtime/graphics_job_application.rs @@ -93,10 +93,10 @@ impl ApplicationJob { self.guard=Some(guard);self.generation=frame.generation;self.retired=false; self.client_frames=if new_client {1} else {self.client_frames.saturating_add(1)}; if remap { - dev_info!(dev.as_ref(),"G18_APP_VM frame={} vm={} context={} target={}x{} aliases={}\n", + crate::g18_kick_info!(dev.as_ref(),"G18_APP_VM frame={} vm={} context={} target={}x{} aliases={}\n", frame.generation,vm.id(),self.job.context_id(),render.target.width,render.target.height,self.user_aliases.len()); for (target,name) in self.job.targets.iter().zip(assembly::TARGET_NAMES) { - dev_info!(dev.as_ref(),"G18_APP_TARGET name={} va={:#x} bytes={:#x} context={}\n", + crate::g18_kick_info!(dev.as_ref(),"G18_APP_TARGET name={} va={:#x} bytes={:#x} context={}\n", name,target.iova(),target.logical_size,self.job.context_id()); } } From c3f7c93db59bc50c4efb7248d4fc00b1b62d9c3e Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sat, 31 Oct 2026 12:34:29 +0100 Subject: [PATCH 03/17] dt-bindings: power: apple,pmc: add T8152 The T8152 (M6) PMC has the T8140 voter interface: perf-state floors at agent << 14 and a rails block with the honoured states and per-rail voter enables, with three rails (DCS, SOC, DISP) and higher maxima. Add apple,t8152-pmc and its performance controller, apple,t8152-clpc. Signed-off-by: Thomas Glanzmann (cherry picked from commit afb23328ea94f6ce32b0be7bc4f96f17e47b4097) --- Documentation/devicetree/bindings/power/apple,pmc.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Documentation/devicetree/bindings/power/apple,pmc.yaml b/Documentation/devicetree/bindings/power/apple,pmc.yaml index 477fc330d253f..4f85d4fc40b9b 100644 --- a/Documentation/devicetree/bindings/power/apple,pmc.yaml +++ b/Documentation/devicetree/bindings/power/apple,pmc.yaml @@ -29,6 +29,7 @@ properties: - apple,t8132-pmc - apple,t8140-pmc - apple,t8142-pmc + - apple,t8152-pmc reg: minItems: 2 @@ -191,6 +192,7 @@ patternProperties: - apple,t8132-clpc - apple,t8140-clpc - apple,t8142-clpc + - apple,t8152-clpc reg: maxItems: 1 From 6bfbe9648c5ed1a43415eaa9273bf2184abf9deb Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sat, 31 Oct 2026 12:34:29 +0100 Subject: [PATCH 04/17] pmdomain/soc: apple: T8152 PMC voters and CLPC iBoot leaves every PMC voter enable at 0 on the J873g, so no floor is honoured and DRAM and fabric stay at their lowest states: one core copies at 6.6 GB/s and the GPU clears 4K in 4.2 ms. The AppleT8152PMGR accessors (26A434) use RegMap 0x1c0 = pmgr reg 46 at 0x302800000: floors at agent << 14, honoured states at 0x1c6000, voter enables at 0x1c8000 + rail * 4 (PMC::getVoterEnablesOffset), rails DCS, SOC and DISP with maxima 7, 4 and 3. That is the T8140 layout with a rails block at 0x1c6000. Add apple,t8152-pmc and apple,t8152-clpc, the latter with per-SoC default state maps (T8140 keeps its maps). With PMP, CLPC and AP enabled: 23.5 GB/s, 4K clear 1.45 ms. Signed-off-by: Thomas Glanzmann (cherry picked from commit e19dcfa40295b9702abe25287b3161e914dd765c) --- drivers/pmdomain/apple/pmc.c | 13 +++++++++++++ drivers/soc/apple/clpc.c | 10 ++++++++++ 2 files changed, 23 insertions(+) diff --git a/drivers/pmdomain/apple/pmc.c b/drivers/pmdomain/apple/pmc.c index 00bebde94a82c..0627ab148fc0c 100644 --- a/drivers/pmdomain/apple/pmc.c +++ b/drivers/pmdomain/apple/pmc.c @@ -1160,10 +1160,23 @@ static const struct apple_pmc_hw apple_pmc_hw_t6040 = { .ap_agent = 3, }; +/* T8152 uses the T8142 voter interface with its rails at 0x3029c6000. */ +static const struct apple_pmc_hw apple_pmc_hw_t8152 = { + .agent_stride = 0x4000, + .agents = 4, + .rails = 3, + .agent_state = 0x0, + .rail_enable = 0x2000, + .dcs_rail = 0, + .dcs_max = 7, + .ap_agent = 3, +}; + static const struct of_device_id apple_pmc_of_match[] = { { .compatible = "apple,t8132-pmc", .data = &apple_pmc_hw_t8132 }, { .compatible = "apple,t8140-pmc", .data = &apple_pmc_hw_t8140 }, { .compatible = "apple,t8142-pmc", .data = &apple_pmc_hw_t8142 }, + { .compatible = "apple,t8152-pmc", .data = &apple_pmc_hw_t8152 }, { .compatible = "apple,t6041-pmc", .data = &apple_pmc_hw_t6040 }, {} }; diff --git a/drivers/soc/apple/clpc.c b/drivers/soc/apple/clpc.c index b31a46b050358..c7b29f0e6a49b 100644 --- a/drivers/soc/apple/clpc.c +++ b/drivers/soc/apple/clpc.c @@ -1141,10 +1141,20 @@ static struct kunit_suite apple_clpc_suite = { kunit_test_suite(apple_clpc_suite); #endif +/* T8152: DCS states 0-7 and fabric states 0-4. */ +static const struct apple_clpc_hw apple_clpc_hw_t8152 = { + .rail = { [APPLE_CLPC_DCS] = 0, [APPLE_CLPC_SOC] = 1 }, + .map = { + [APPLE_CLPC_DCS] = { 2, 4, 6, 7 }, + [APPLE_CLPC_SOC] = { 1, 2, 3, 4 }, + }, +}; + static const struct of_device_id apple_clpc_of_match[] = { { .compatible = "apple,t8132-clpc", .data = &apple_clpc_hw_t8132 }, { .compatible = "apple,t8140-clpc", .data = &apple_clpc_hw_t8140 }, { .compatible = "apple,t8142-clpc", .data = &apple_clpc_hw_t8142 }, + { .compatible = "apple,t8152-clpc", .data = &apple_clpc_hw_t8152 }, { .compatible = "apple,t6041-clpc", .data = &apple_clpc_hw_t6040 }, {} }; From 403cdc85d234b61e17d13e539001c0a6e7528ef4 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sat, 31 Oct 2026 11:34:29 +0000 Subject: [PATCH 05/17] arm64: dts: apple: j873: PMC voters and CLPC Bind the PMC voter interface (pmgr reg 46, rails block at 0x3029c6000) with the PMP, CLPC and AP agents as static voters, and the CLPC. Signed-off-by: Thomas Glanzmann Adapt the PMC node to the public t8152-j873g RAM-boot device tree. (cherry picked from commit 9cac3b9679803baa1b11fcb25607848151c9cc8e) --- arch/arm64/boot/dts/apple/t8152-j873-pmc.dtsi | 27 +++++++++++++++++++ arch/arm64/boot/dts/apple/t8152-j873g.dts | 2 ++ 2 files changed, 29 insertions(+) create mode 100644 arch/arm64/boot/dts/apple/t8152-j873-pmc.dtsi diff --git a/arch/arm64/boot/dts/apple/t8152-j873-pmc.dtsi b/arch/arm64/boot/dts/apple/t8152-j873-pmc.dtsi new file mode 100644 index 0000000000000..3b809b363b84e --- /dev/null +++ b/arch/arm64/boot/dts/apple/t8152-j873-pmc.dtsi @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-2.0+ OR MIT + +/ { + soc { + /* + * Perf-state floors of voter agents 0-3 (PMP, CLPC, GFX, AP) and, + * at 0x1c6000, the honoured states and per-rail voter enables + * (rails DCS, SOC, DISP) in the PMC window, pmgr reg 46. + */ + pmc: power-management@302800000 { + compatible = "apple,t8152-pmc"; + reg = <0x3 0x02800000 0x0 0x10000>, + <0x3 0x029c6000 0x0 0x3000>; + reg-names = "voters", "rails"; + #address-cells = <1>; + #size-cells = <0>; + #cooling-cells = <2>; + /* PMP, CLPC and AP agents */ + apple,static-voters = <0 1 3>; + + clpc: performance-controller@1 { + compatible = "apple,t8152-clpc"; + reg = <1>; + }; + }; + }; +}; diff --git a/arch/arm64/boot/dts/apple/t8152-j873g.dts b/arch/arm64/boot/dts/apple/t8152-j873g.dts index f4ccb05ba0aab..ec3d591410b3d 100644 --- a/arch/arm64/boot/dts/apple/t8152-j873g.dts +++ b/arch/arm64/boot/dts/apple/t8152-j873g.dts @@ -283,3 +283,5 @@ #include "t8152-j873-gpu.dtsi" #include "t8152-j873-n1.dtsi" + +#include "t8152-j873-pmc.dtsi" From 44632bfa49d6dbb0c3fbf5d603e55c7e524db834 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sat, 31 Oct 2026 14:40:55 +0100 Subject: [PATCH 06/17] dt-bindings: spi: apple,qspi: add T8152 The T8152 (M6) boot NOR sits behind the same QSPIMC controller as on T8140 (ADT /arm-io/qspi, version 0x20100). Accept apple,t8152-qspi with apple,t8140-qspi as fallback. Signed-off-by: Thomas Glanzmann (cherry picked from commit 9aafd590cd45720a93f8c7a5e19c27adbe30fa30) --- .../devicetree/bindings/spi/apple,qspi.yaml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Documentation/devicetree/bindings/spi/apple,qspi.yaml b/Documentation/devicetree/bindings/spi/apple,qspi.yaml index 0b98af1ad68d2..40d08a8273712 100644 --- a/Documentation/devicetree/bindings/spi/apple,qspi.yaml +++ b/Documentation/devicetree/bindings/spi/apple,qspi.yaml @@ -19,11 +19,15 @@ allOf: properties: compatible: - enum: - - apple,t8122-qspi - - apple,t8132-qspi - - apple,t6041-qspi - - apple,t8140-qspi + oneOf: + - enum: + - apple,t8122-qspi + - apple,t8132-qspi + - apple,t6041-qspi + - apple,t8140-qspi + - items: + - const: apple,t8152-qspi + - const: apple,t8140-qspi reg: maxItems: 1 From bbab5f6da55e644f97bfb26ff8de84116e5f14e1 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sat, 31 Oct 2026 13:51:19 +0000 Subject: [PATCH 07/17] arm64: dts: apple: t8152-j873: boot NOR with the NVRAM through QSPIMC The J873g's boot NOR (Winbond, JEDEC ef 65 14, 2 MiB) sits behind a QSPIMC controller at 0x331118000 (ADT /arm-io/qspi, version 0x20100), the one spi-apple-qspi drives on T8140. Its PMGR power states (devices SPI_P and QSPI) are off once iBoot is done; touching the controller without them takes an asynchronous SError, so describe them as power domains. The NVRAM v3 banks ("3VVN", 64 KiB each) run from 0x24000 to 0x1f4000, as on T8140 J700; the partition needs 4 KiB erase sectors to be writable (CONFIG_MTD_SPI_NOR_USE_4K_SECTORS). Tested on the J873g: asahi-nvram reads, writes and deletes variables; asahi-bless lists Macintosh HD and LinuxStub, and asahi-bless --next --set-boot "Macintosh HD" boots macOS once, after which the mini boots LinuxStub again. Signed-off-by: Thomas Glanzmann Describe the QSPI controller and its required PMGR domains in the public t8152-j873g device tree, without the unrelated legacy audio nodes. (cherry picked from commit 5164bb8e5e3cb419934fdd91eb9ddd192be3b031) --- .../arm64/boot/dts/apple/t8152-j873-qspi.dtsi | 85 +++++++++++++++++++ arch/arm64/boot/dts/apple/t8152-j873g.dts | 1 + 2 files changed, 86 insertions(+) create mode 100644 arch/arm64/boot/dts/apple/t8152-j873-qspi.dtsi diff --git a/arch/arm64/boot/dts/apple/t8152-j873-qspi.dtsi b/arch/arm64/boot/dts/apple/t8152-j873-qspi.dtsi new file mode 100644 index 0000000000000..c03fbf04b24e4 --- /dev/null +++ b/arch/arm64/boot/dts/apple/t8152-j873-qspi.dtsi @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: GPL-2.0+ OR MIT + +/* + * The boot NOR behind the QSPIMC controller (ADT /arm-io/qspi/spinor: firmware, + * anvram, syscfg), for asahi-nvram and asahi-bless. The controller's power + * states (PMGR devices SPI_P and QSPI) are off once iBoot is done with it; + * reading it without them takes an SError. + */ +/ { + aliases { + nvram = &nvram; + }; + + /* QSPIMC input clock, as on T8140 (ADT clock-ids 412) */ + clk_200m: clock-200m { + compatible = "fixed-clock"; + #clock-cells = <0>; + clock-frequency = <200000000>; + clock-output-names = "clk_200m"; + }; + + soc { + pmgr: power-management@300700000 { + compatible = "apple,t8152-pmgr", "apple,t8103-pmgr", + "syscon", "simple-mfd"; + #address-cells = <1>; + #size-cells = <1>; + reg = <0x3 0x700000 0x0 0x14000>; + }; + + qspi: spi@331118000 { + compatible = "apple,t8152-qspi", "apple,t8140-qspi"; + /* One aperture; the command block starts at offset 0x800. */ + reg = <0x3 0x31118000 0x0 0x4800>; + interrupt-parent = <&aic>; + interrupts = ; + clocks = <&clk_200m>; + power-domains = <&ps_qspi>; + #address-cells = <1>; + #size-cells = <0>; + + spinor: flash@0 { + compatible = "jedec,spi-nor"; + reg = <0>; + spi-max-frequency = <10000000>; + + partitions { + compatible = "fixed-partitions"; + #address-cells = <1>; + #size-cells = <1>; + + /* + * 64 KiB NVRAM v3 banks ("3VVN") from 0x24000 + * to 0x1f4000, as on T8140 J700. + */ + nvram: partition@24000 { + label = "nvram"; + reg = <0x24000 0x1d0000>; + }; + }; + }; + }; + }; +}; + +&pmgr { + ps_spi_p: power-controller@d40 { + compatible = "apple,t8152-pmgr-pwrstate", + "apple,t8103-pmgr-pwrstate"; + reg = <0xd40 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "spi_p"; + }; + + ps_qspi: power-controller@1140 { + compatible = "apple,t8152-pmgr-pwrstate", + "apple,t8103-pmgr-pwrstate"; + reg = <0x1140 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "qspi"; + power-domains = <&ps_spi_p>; + }; +}; diff --git a/arch/arm64/boot/dts/apple/t8152-j873g.dts b/arch/arm64/boot/dts/apple/t8152-j873g.dts index ec3d591410b3d..1513a2abbc83f 100644 --- a/arch/arm64/boot/dts/apple/t8152-j873g.dts +++ b/arch/arm64/boot/dts/apple/t8152-j873g.dts @@ -285,3 +285,4 @@ #include "t8152-j873-n1.dtsi" #include "t8152-j873-pmc.dtsi" +#include "t8152-j873-qspi.dtsi" From a3f06f7c7c428b18b6e1a18e6a7c075b2ccbbcb1 Mon Sep 17 00:00:00 2001 From: Acelogic Date: Fri, 6 Nov 2026 04:19:21 -0500 Subject: [PATCH 08/17] soc: apple: describe the J873 display mailbox and SPMI providers The J873 DCP and PD nodes require a counted-FIFO mailbox and the generation-4 SPMI layout. Both layouts already exist for SMC and T8142 respectively; add T8152 matches and bindings without changing either existing path. --- .../devicetree/bindings/mailbox/apple,mailbox.yaml | 11 ++++++++--- .../devicetree/bindings/spmi/apple,spmi.yaml | 4 +++- drivers/soc/apple/mailbox.c | 1 + drivers/spmi/spmi-apple-controller.c | 1 + 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml b/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml index 2f281baae5dca..a5d2322c28486 100644 --- a/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml +++ b/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml @@ -23,9 +23,11 @@ properties: compatible: oneOf: - description: - T8152 SMC post-office mailbox with counted FIFOs and interrupt + T8152 SMC and DCP post-office mailboxes with counted FIFOs and interrupt masking provided by the interrupt controller. - const: apple,t8152-smc-mailbox + enum: + - apple,t8152-smc-mailbox + - apple,t8152-dcp-mailbox - description: ASC mailboxes are the most common variant found on the M1 used @@ -135,7 +137,9 @@ allOf: properties: compatible: contains: - const: apple,t8152-smc-mailbox + enum: + - apple,t8152-smc-mailbox + - apple,t8152-dcp-mailbox then: properties: interrupts: @@ -195,6 +199,7 @@ allOf: - apple,t8112-ane-mailbox - apple,t8140-aop-setup-mailbox - apple,t8152-smc-mailbox + - apple,t8152-dcp-mailbox then: properties: interrupts: diff --git a/Documentation/devicetree/bindings/spmi/apple,spmi.yaml b/Documentation/devicetree/bindings/spmi/apple,spmi.yaml index bb4940558fbdb..c96ab3adeec3e 100644 --- a/Documentation/devicetree/bindings/spmi/apple,spmi.yaml +++ b/Documentation/devicetree/bindings/spmi/apple,spmi.yaml @@ -36,7 +36,9 @@ properties: - apple,t6000-spmi - apple,t8112-spmi - const: apple,spmi - - const: apple,t8142-spmi + - enum: + - apple,t8142-spmi + - apple,t8152-spmi reg: maxItems: 1 diff --git a/drivers/soc/apple/mailbox.c b/drivers/soc/apple/mailbox.c index c13c03f9d92ab..3839a62d55e68 100644 --- a/drivers/soc/apple/mailbox.c +++ b/drivers/soc/apple/mailbox.c @@ -1708,6 +1708,7 @@ static const struct apple_mbox_hw apple_mbox_t8152_smc_hw = { static const struct of_device_id apple_mbox_of_match[] = { { .compatible = "apple,t8152-smc-mailbox", .data = &apple_mbox_t8152_smc_hw }, + { .compatible = "apple,t8152-dcp-mailbox", .data = &apple_mbox_t8152_smc_hw }, { .compatible = "apple,t6031-agx-asc-mailbox", .data = &apple_mbox_asc_hw }, { .compatible = "apple,t8122-agx-asc-mailbox", .data = &apple_mbox_asc_hw }, { .compatible = "apple,t8152-gfx-ascwrap-v8", .data = &apple_mbox_t8152_gfx_ascwrap_v8_hw }, diff --git a/drivers/spmi/spmi-apple-controller.c b/drivers/spmi/spmi-apple-controller.c index fbdb9ba0e7f2a..64b3cf9e8fc0d 100644 --- a/drivers/spmi/spmi-apple-controller.c +++ b/drivers/spmi/spmi-apple-controller.c @@ -518,6 +518,7 @@ static int apple_spmi_probe(struct platform_device *pdev) static const struct of_device_id apple_spmi_match_table[] = { { .compatible = "apple,t8142-spmi", .data = &apple_spmi_gen4 }, + { .compatible = "apple,t8152-spmi", .data = &apple_spmi_gen4 }, { .compatible = "apple,t8103-spmi", .data = &apple_spmi_gen1 }, { .compatible = "apple,spmi", .data = &apple_spmi_gen1 }, {} From 81cba100a5538f3e204a5dc992bd561bcc0b0289 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sat, 31 Oct 2026 13:18:09 +0000 Subject: [PATCH 09/17] drm/apple_dcpib: port the J873 HDMI and USB-C display driver Port Thomas Glanzmann's public linux-aurora PR #44 onto the current Aurora kernel. Retain the current DART error propagation, display ownership and managed-stream logic while adding the Gen3 TLB path. The existing RTKit firmware-owned-buffer and early-endpoint rules remain intact. Include the display resources from the public J873g board tree and use the DCP-specific counted-FIFO mailbox binding. The unrelated legacy audio device tree is not required. Source: https://github.com/aurora-silicon/linux-aurora/pull/44 Source head: 13b1810610414554f57abfef0ed7ada5f4845dcc Signed-off-by: Thomas Glanzmann --- .../display/apple,t8152-dcp-iboot.yaml | 235 +++++ .../devicetree/bindings/iommu/apple,dart.yaml | 37 +- arch/arm64/boot/dts/apple/t8152-j873-dcp.dtsi | 484 ++++++++++ arch/arm64/boot/dts/apple/t8152-j873g.dts | 1 + drivers/gpu/drm/Kconfig | 1 + drivers/gpu/drm/Makefile | 1 + drivers/gpu/drm/apple_dcpib/Kconfig | 15 + drivers/gpu/drm/apple_dcpib/Makefile | 3 + drivers/gpu/drm/apple_dcpib/afk.c | 740 ++++++++++++++++ drivers/gpu/drm/apple_dcpib/atc.c | 789 +++++++++++++++++ drivers/gpu/drm/apple_dcpib/aux.c | 196 +++++ drivers/gpu/drm/apple_dcpib/dcpib.h | 334 +++++++ drivers/gpu/drm/apple_dcpib/dptx.c | 324 +++++++ drivers/gpu/drm/apple_dcpib/drv.c | 504 +++++++++++ drivers/gpu/drm/apple_dcpib/hdmi.c | 337 +++++++ drivers/gpu/drm/apple_dcpib/iboot.c | 243 +++++ drivers/gpu/drm/apple_dcpib/kms.c | 833 ++++++++++++++++++ drivers/gpu/drm/apple_dcpib/pd.c | 176 ++++ drivers/iommu/apple-dart.c | 131 ++- drivers/soc/apple/rtkit.c | 13 + include/linux/soc/apple/rtkit.h | 10 + 21 files changed, 5404 insertions(+), 3 deletions(-) create mode 100644 Documentation/devicetree/bindings/display/apple,t8152-dcp-iboot.yaml create mode 100644 arch/arm64/boot/dts/apple/t8152-j873-dcp.dtsi create mode 100644 drivers/gpu/drm/apple_dcpib/Kconfig create mode 100644 drivers/gpu/drm/apple_dcpib/Makefile create mode 100644 drivers/gpu/drm/apple_dcpib/afk.c create mode 100644 drivers/gpu/drm/apple_dcpib/atc.c create mode 100644 drivers/gpu/drm/apple_dcpib/aux.c create mode 100644 drivers/gpu/drm/apple_dcpib/dcpib.h create mode 100644 drivers/gpu/drm/apple_dcpib/dptx.c create mode 100644 drivers/gpu/drm/apple_dcpib/drv.c create mode 100644 drivers/gpu/drm/apple_dcpib/hdmi.c create mode 100644 drivers/gpu/drm/apple_dcpib/iboot.c create mode 100644 drivers/gpu/drm/apple_dcpib/kms.c create mode 100644 drivers/gpu/drm/apple_dcpib/pd.c diff --git a/Documentation/devicetree/bindings/display/apple,t8152-dcp-iboot.yaml b/Documentation/devicetree/bindings/display/apple,t8152-dcp-iboot.yaml new file mode 100644 index 0000000000000..37b14961bbe77 --- /dev/null +++ b/Documentation/devicetree/bindings/display/apple,t8152-dcp-iboot.yaml @@ -0,0 +1,235 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/display/apple,t8152-dcp-iboot.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Apple T8152 DCP on the iBoot display interface + +maintainers: + - Thomas Glanzmann + +description: | + The Display Coprocessor (DCP) of the Apple T8152 SoC runs RTKit firmware + that offers a small iBoot display interface (disp0-service) next to the + full IOMobileFramebuffer protocol. This binding describes the DCP driven + through that interface, together with the DisplayPort PHY its DPTX port + is routed to. The DCP fetches through its own DART, the display pipe + scans out through another one, which sees each scanout buffer at the same + address. + +properties: + compatible: + const: apple,t8152-dcp-iboot + + reg: + minItems: 2 + maxItems: 7 + + reg-names: + oneOf: + - items: + - const: coproc + - const: atc + - const: dp-clock + - const: atc-ps + - const: atc-psd + - const: aux + - const: aux-ps + minItems: 5 + description: + DCP coprocessor, ATC DisplayPort PHY (USB-C), the PMGR clock + control of its DisplayPort link, the PMGR power states the PHY + depends on, and optionally the AP DisplayPort AUX controller that + reads the sink's EDID with its PMGR power state + - items: + - const: coproc + - const: phy-core + - const: phy-apb + - const: aux + - const: aux-ps + minItems: 3 + description: + DCP coprocessor, the HDMI dptx-phy blocks, and optionally the AP + DisplayPort AUX controller that reads the sink's EDID with its + PMGR power state + - items: + - const: coproc + - const: dp-clock + - const: aux + - const: aux-ps + minItems: 2 + description: + DCP coprocessor, the PMGR clock control of the ATC DisplayPort + link (shared by the ports), and optionally the AP DisplayPort AUX + controller with its PMGR power state; the USB-C ports follow as + usb-c child nodes + + apple,tunable-pre: + $ref: /schemas/types.yaml#/definitions/uint32-matrix + items: + items: + - description: register offset in the ATC PHY block + - description: mask + - description: value + description: + ATC PHY tunables (ADT ACIOPHY_CMN and CIOPLL_CORE defaults), applied + before the lane crossbar setup when the PHY starts from cold. + + apple,tunable-post: + $ref: /schemas/types.yaml#/definitions/uint32-matrix + items: + items: + - description: register offset in the ATC PHY block + - description: mask + - description: value + description: + ATC PHY tunables (ADT ATC_FABRIC, DPPLL and AUX defaults), applied + after the lane crossbar setup. + + apple,hdmi: + type: boolean + description: + The DCP drives the HDMI port through the dedicated dptx-phy and an + on-board DisplayPort-to-HDMI converter, instead of an ATC USB-C PHY. + + apple,usb-pd: + $ref: /schemas/types.yaml#/definitions/phandle + description: + The USB-C port's PD controller (SPMI), polled for the DisplayPort alt + mode connection and the plug orientation. + + hpd-gpios: + maxItems: 1 + description: + Hot plug detect line of the connector, if the AP can read it (J873g + HDMI); without it the sink is assumed to stay connected. + + mboxes: + maxItems: 1 + + iommus: + maxItems: 1 + description: DCP DART stream + + scanout: + type: object + additionalProperties: false + description: + The display pipe's scanout stream. Its DART is locked with its own + root, so it is a separate device that sees each scanout buffer at the + DCP's address. + properties: + iommus: + maxItems: 1 + dma-coherent: true + required: + - iommus + + dma-coherent: true + + apple,die: + $ref: /schemas/types.yaml#/definitions/uint32 + description: Die of the DPTX connection target, 0 if absent. + + "#address-cells": + const: 2 + + "#size-cells": + const: 2 + + ranges: true + +patternProperties: + "^usb-c@[0-9a-f]+$": + type: object + additionalProperties: false + description: + A USB-C port whose ATC PHY the DCP can drive. The DCP drives one port + at a time, the one a DisplayPort sink is plugged into. + properties: + reg: + maxItems: 3 + reg-names: + items: + - const: atc + - const: atc-ps + - const: atc-psd + description: + ATC DisplayPort PHY, the PMGR power states it depends on (DP_0, + DP_1 and USB_AON) and its common power domain (PSD) + apple,dfp-endpoint: + $ref: /schemas/types.yaml#/definitions/uint32 + description: The ATC's display-crossbar0 DFP endpoint (ATC instance). + apple,usb-pd: + $ref: /schemas/types.yaml#/definitions/phandle + description: The port's PD controller (SPMI). + apple,tunable-pre: + $ref: /schemas/types.yaml#/definitions/uint32-matrix + description: As the node's apple,tunable-pre, for this port's PHY. + apple,tunable-post: + $ref: /schemas/types.yaml#/definitions/uint32-matrix + description: As the node's apple,tunable-post, for this port's PHY. + required: + - reg + - reg-names + - apple,dfp-endpoint + - apple,usb-pd + +required: + - compatible + - reg + - reg-names + - mboxes + - iommus + - scanout + +additionalProperties: false + +examples: + - | + soc { + #address-cells = <2>; + #size-cells = <2>; + + display@286e00000 { + compatible = "apple,t8152-dcp-iboot"; + reg = <0x2 0x86e00000 0x0 0x74000>, + <0x4 0x13000000 0x0 0x84000>, + <0x3 0x0002005c 0x0 0x4>, + <0x3 0x082900c0 0x0 0x104>, + <0x3 0x03a13a00 0x0 0xc0>; + reg-names = "coproc", "atc", "dp-clock", "atc-ps", "atc-psd"; + mboxes = <&dcp_mbox>; + iommus = <&dcp_dart 23>; + + scanout { + iommus = <&disp0_dart 0>; + }; + }; + + display@296e00000 { + compatible = "apple,t8152-dcp-iboot"; + reg = <0x2 0x96e00000 0x0 0x74000>, + <0x3 0x0002005c 0x0 0x4>; + reg-names = "coproc", "dp-clock"; + #address-cells = <2>; + #size-cells = <2>; + ranges; + mboxes = <&dcp_mbox>; + iommus = <&dcp_dart 23>; + + scanout { + iommus = <&disp0_dart 0>; + }; + + usb-c@413000000 { + reg = <0x4 0x13000000 0x0 0x84000>, + <0x3 0x082900c0 0x0 0x104>, + <0x3 0x03a13a00 0x0 0xc0>; + reg-names = "atc", "atc-ps", "atc-psd"; + apple,dfp-endpoint = <3>; + apple,usb-pd = <&hpm3>; + }; + }; + }; diff --git a/Documentation/devicetree/bindings/iommu/apple,dart.yaml b/Documentation/devicetree/bindings/iommu/apple,dart.yaml index 401b41f8ff18b..d438b95118085 100644 --- a/Documentation/devicetree/bindings/iommu/apple,dart.yaml +++ b/Documentation/devicetree/bindings/iommu/apple,dart.yaml @@ -28,6 +28,7 @@ properties: - apple,t8103-usb4-dart - apple,t8110-dart - apple,t6000-dart + - apple,t8152-dart-gen3 - items: - const: apple,t8140-ane-dart - const: apple,t8140-dart @@ -66,7 +67,19 @@ properties: error-mask policy rather than changing it for these streams. reg: - maxItems: 1 + minItems: 1 + items: + - description: DART registers + - description: + Gen3 only. The SoC's TLB invalidation aperture window for this + DART (ADT dart-tlbi-aperture plus tlbi-aperture-offset), shared + by all Gen3 DARTs. A stream is invalidated by writing a payload + there and broadcasting a TLBI. + + reg-names: + items: + - const: dart + - const: tlbi interrupts: maxItems: 1 @@ -158,6 +171,28 @@ required: - '#iommu-cells' - interrupts +allOf: + - if: + properties: + compatible: + contains: + const: apple,t8152-dart-gen3 + then: + description: + T8152 Gen3 DARTs are owned by the SPTM. The bootloader hands them + over locked; the operating system only adds page table entries in + root slots that are empty at handoff. + properties: + reg: + minItems: 2 + required: + - reg-names + else: + properties: + reg: + maxItems: 1 + reg-names: false + additionalProperties: false examples: diff --git a/arch/arm64/boot/dts/apple/t8152-j873-dcp.dtsi b/arch/arm64/boot/dts/apple/t8152-j873-dcp.dtsi new file mode 100644 index 0000000000000..9ab6bbb0591ab --- /dev/null +++ b/arch/arm64/boot/dts/apple/t8152-j873-dcp.dtsi @@ -0,0 +1,484 @@ +// SPDX-License-Identifier: GPL-2.0+ OR MIT +/* + * J873/T8152 DCP display driver on the iBoot interface (apple_dcpib) for the + * rear USB-C and HDMI displays, included by the public J873g board tree. + * + * Resources are from the 26A434 ADT: dcp (iop,ascwrap-v8, post-office + * mailbox 5 with alias: 0x316020000 + 2 * 5 * 0x80), dart-dcp (SID 23), + * dart-disp0 (scanout SID 0), dart-tlbi-aperture (0x200000000) with + * tlbi-aperture-offset 0x400, and atc-phy3's PHY window. Both DARTs are + * Gen3 and stay locked by iBoot, each with its own live root, so the scanout + * stream is a child device with its own domain; Linux tables are published into vacant + * root slots, so the DMA window is the upper half of the 64 GiB vm-base + * window, above the firmware's and the bootloader's mappings. + */ +#include + +/ { + soc { + /* + * ADT /arm-io/nub-spmi-a0 (gen 4): the USB-C PD controllers of + * the three rear ports, each of which can carry the USB-C display. + */ + nub_spmi_a0: spmi@308908000 { + compatible = "apple,t8152-spmi"; + reg = <0x3 0x08908000 0x0 0x4000>; + #address-cells = <2>; + #size-cells = <0>; + + /* back-middle */ + hpm0: usb-pd@c { + compatible = "apple,sn201202x-spmi"; + reg = <0xc SPMI_USID>; + }; + + /* back-left */ + hpm1: usb-pd@a { + compatible = "apple,sn201202x-spmi"; + reg = <0xa SPMI_USID>; + }; + + /* back-right */ + hpm3: usb-pd@8 { + compatible = "apple,sn201202x-spmi"; + reg = <0x8 SPMI_USID>; + }; + }; + + /* + * ADT aop-gpio. Its interrupts are the AOP's, so it has no + * interrupt controller here; the HDMI HPD line is polled. + */ + pinctrl_aop: pinctrl@310824000 { + compatible = "apple,t8152-pinctrl", "apple,t8103-pinctrl"; + reg = <0x3 0x10824000 0x0 0x4000>; + gpio-controller; + #gpio-cells = <2>; + gpio-ranges = <&pinctrl_aop 0 0 77>; + apple,npins = <77>; + }; + + dcp_dart: iommu@286340000 { + compatible = "apple,t8152-dart-gen3"; + reg = <0x2 0x86340000 0x0 0x20000>, + <0x2 0x00000400 0x0 0x10>; + reg-names = "dart", "tlbi"; + interrupts = ; + #iommu-cells = <1>; + apple,dma-range = <0x108 0x0 0x8 0x0>; + }; + + disp0_dart: iommu@286300000 { + compatible = "apple,t8152-dart-gen3"; + reg = <0x2 0x86300000 0x0 0xc000>, + <0x2 0x00000400 0x0 0x10>; + reg-names = "dart", "tlbi"; + interrupts = ; + #iommu-cells = <1>; + apple,dma-range = <0x108 0x0 0x8 0x0>; + }; + + dcp_mbox: mailbox@316020500 { + compatible = "apple,t8152-dcp-mailbox"; + reg = <0x3 0x16020500 0x0 0x80>; + interrupts = , + ; + interrupt-names = "send-empty", "recv-not-empty"; + #mbox-cells = <0>; + }; + + dcp_iboot: display@286e00000 { + compatible = "apple,t8152-dcp-iboot"; + reg = <0x2 0x86e00000 0x0 0x74000>, + <0x3 0x0002005c 0x0 0x4>, + <0x3 0x1a760000 0x0 0x4000>, + <0x3 0x00700460 0x0 0x4>; + /* aux: ADT dptx0-aux, aux-ps: PMGR GLOBAL_AUX_CTRL0 */ + reg-names = "coproc", "dp-clock", "aux", "aux-ps"; + #address-cells = <2>; + #size-cells = <2>; + ranges; + mboxes = <&dcp_mbox>; + iommus = <&dcp_dart 23>; + /* + * Cacheable DMA, as iBoot maps DCP buffers: the firmware + * faults on entries with the DART's no-cache bit. + */ + dma-coherent; + + /* + * The display pipe's own stream: a separate device, so + * its domain imports dart-disp0's root, not dart-dcp's. + */ + scanout { + iommus = <&disp0_dart 0>; + dma-coherent; + }; + + /* + * USB-C back-middle: ADT atc-phy0. PS: the ATC's DP_0, DP_1 and + * USB_AON power states (+0, +0x20, +0x100), PSD: its common + * domain. Tunables before and after the lane crossbar setup. + */ + usb-c@403000000 { + reg = <0x4 0x03000000 0x0 0x84000>, + <0x3 0x08290000 0x0 0x104>, + <0x3 0x03a13700 0x0 0xc0>; + reg-names = "atc", "atc-ps", "atc-psd"; + apple,dfp-endpoint = <0>; + apple,usb-pd = <&hpm0>; + apple,tunable-pre = + /* ACIOPHY_CMN_DFLT */ + <0x00800 0x00fc0000 0x00fc0000>, + <0x0084c 0x000ff800 0x000ff800>, + <0x0085c 0x000000ff 0x00000080>, + <0x00860 0x007fffff 0x0052bca4>, + <0x00864 0x02c00000 0x00800000>, + /* CIOPLL_CORE_DFLT */ + <0x01614 0x0003ffc0 0x0001e5c0>, + <0x01640 0x00000007 0x00000003>, + <0x01668 0x00000ffc 0x000005f0>; + apple,tunable-post = + /* ATC_FABRIC */ + <0x6c004 0x003f003f 0x00200020>, + <0x6c00c 0x000000f1 0x000000a1>, + <0x6c010 0x0000801f 0x00000004>, + <0x6c018 0x003f003f 0x00200020>, + <0x6c020 0x000000f1 0x000000a1>, + <0x6c024 0x0000801f 0x00000004>, + <0x6c02c 0x007f007f 0x00410041>, + <0x6c034 0x000000f1 0x00000071>, + <0x6c038 0x0000801f 0x00000004>, + <0x6c040 0x0000001f 0x00000010>, + <0x6c048 0x0000801f 0x00000004>, + <0x6c050 0x007f007f 0x00450045>, + <0x6c058 0x000000f1 0x00000051>, + <0x6c05c 0x0000801f 0x00000004>, + <0x6c064 0x0000003f 0x00000020>, + <0x6c06c 0x0000801f 0x00000004>, + <0x6c074 0x00000001 0x00000001>, + <0x6c078 0x0000801f 0x00000004>, + <0x6c080 0x001f001f 0x00100010>, + <0x6c084 0x000000f1 0x00000011>, + <0x6c088 0x00000001 0x00000001>, + <0x6c08c 0x00000001 0x00000001>, + <0x6c090 0x0000801f 0x00000004>, + <0x6c098 0x001f001f 0x00100010>, + <0x6c09c 0x000000f1 0x00000011>, + <0x6c0a0 0x0000801f 0x00000004>, + <0x6c0a8 0x000000ff 0x000000a0>, + <0x6c0ac 0x0000801f 0x00000004>, + <0x6c0b4 0x0000003f 0x00000020>, + <0x6c0bc 0x0000801f 0x00000004>, + <0x6c0c4 0x0000003f 0x00000020>, + <0x6c0cc 0x0000801f 0x00000004>, + <0x6c0d4 0x0000000f 0x00000008>, + <0x6c0d8 0x0000801f 0x00000004>, + <0x6c0e0 0x001f001f 0x00180018>, + <0x6c0e4 0x0000801f 0x00000004>, + <0x6c0ec 0x001f001f 0x00180018>, + <0x6c0f0 0x0000801f 0x00000004>, + <0x6c0f8 0x001f001f 0x00180018>, + <0x6c0fc 0x0000801f 0x00000004>, + <0x6c104 0x001f001f 0x00180018>, + <0x6c108 0x0000801f 0x00000004>, + <0x6c110 0x00000001 0x00000001>, + <0x6c114 0x0000801f 0x00000004>, + <0x6c11c 0x0000801f 0x00000004>, + <0x6c124 0x0000801f 0x00000004>, + <0x6c12c 0x0000801f 0x00000004>, + <0x6c134 0x0000801f 0x00000004>, + <0x6c13c 0xffffffff 0x01010101>, + <0x6c140 0x0000801f 0x00000004>, + <0x6c148 0x0000801f 0x00000004>, + <0x6c150 0xffffffff 0x01010101>, + <0x6c154 0x0000ffff 0x00000101>, + <0x6c158 0x0000801f 0x00000004>, + <0x6c160 0x0000801f 0x00000004>, + <0x6c168 0x0000801f 0x00000004>, + <0x6c170 0x0000801f 0x00000004>, + <0x6c178 0x0000801f 0x00000004>, + <0x6c180 0xffffffff 0x01010101>, + <0x6c184 0x0000801f 0x00000004>, + <0x6c18c 0x0000801f 0x00000004>, + <0x6c194 0x0000801f 0x00000004>, + /* DPPLL_CORE_DFLT */ + <0x01040 0x0000000c 0x00000008>, + <0x01060 0x0001c000 0x0000c000>, + <0x01098 0x00000f00 0x00000800>, + <0x0109c 0x000fe000 0x000fe000>, + <0x01108 0x07c00000 0x04000000>, + <0x0110c 0x0000007c 0x00000010>, + /* DPPLL_TOP_DFLT */ + <0x00e8c 0x0000007f 0x0000003c>, + <0x00e9c 0x0000007f 0x00000032>, + <0x00eac 0x0000007f 0x00000032>, + <0x00ebc 0x0000007f 0x0000005a>, + /* ASDC_AUX_SHM_DFLT */ + <0x02308 0x000000fc 0x00000084>; + }; + + /* + * USB-C back-left: ADT atc-phy1. PS: the ATC's DP_0, DP_1 and + * USB_AON power states (+0, +0x20, +0x100), PSD: its common + * domain. Tunables before and after the lane crossbar setup. + */ + usb-c@40b000000 { + reg = <0x4 0x0b000000 0x0 0x84000>, + <0x3 0x08290040 0x0 0x104>, + <0x3 0x03a13800 0x0 0xc0>; + reg-names = "atc", "atc-ps", "atc-psd"; + apple,dfp-endpoint = <1>; + apple,usb-pd = <&hpm1>; + apple,tunable-pre = + /* ACIOPHY_CMN_DFLT */ + <0x00800 0x00fc0000 0x00fc0000>, + <0x0084c 0x000ff800 0x000ff800>, + <0x0085c 0x000000ff 0x00000080>, + <0x00860 0x007fffff 0x0052bc26>, + <0x00864 0x02c00000 0x00800000>, + /* CIOPLL_CORE_DFLT */ + <0x01614 0x0003ffc0 0x0001e5c0>, + <0x01640 0x00000007 0x00000003>, + <0x01668 0x00000ffc 0x000005f0>; + apple,tunable-post = + /* ATC_FABRIC */ + <0x6c004 0x003f003f 0x00200020>, + <0x6c00c 0x000000f1 0x000000a1>, + <0x6c010 0x0000801f 0x00000004>, + <0x6c018 0x003f003f 0x00200020>, + <0x6c020 0x000000f1 0x000000a1>, + <0x6c024 0x0000801f 0x00000004>, + <0x6c02c 0x007f007f 0x00410041>, + <0x6c034 0x000000f1 0x00000071>, + <0x6c038 0x0000801f 0x00000004>, + <0x6c040 0x0000001f 0x00000010>, + <0x6c048 0x0000801f 0x00000004>, + <0x6c050 0x007f007f 0x00450045>, + <0x6c058 0x000000f1 0x00000051>, + <0x6c05c 0x0000801f 0x00000004>, + <0x6c064 0x0000003f 0x00000020>, + <0x6c06c 0x0000801f 0x00000004>, + <0x6c074 0x00000001 0x00000001>, + <0x6c078 0x0000801f 0x00000004>, + <0x6c080 0x001f001f 0x00100010>, + <0x6c084 0x000000f1 0x00000011>, + <0x6c088 0x00000001 0x00000001>, + <0x6c08c 0x00000001 0x00000001>, + <0x6c090 0x0000801f 0x00000004>, + <0x6c098 0x001f001f 0x00100010>, + <0x6c09c 0x000000f1 0x00000011>, + <0x6c0a0 0x0000801f 0x00000004>, + <0x6c0a8 0x000000ff 0x000000a0>, + <0x6c0ac 0x0000801f 0x00000004>, + <0x6c0b4 0x0000003f 0x00000020>, + <0x6c0bc 0x0000801f 0x00000004>, + <0x6c0c4 0x0000003f 0x00000020>, + <0x6c0cc 0x0000801f 0x00000004>, + <0x6c0d4 0x0000000f 0x00000008>, + <0x6c0d8 0x0000801f 0x00000004>, + <0x6c0e0 0x001f001f 0x00180018>, + <0x6c0e4 0x0000801f 0x00000004>, + <0x6c0ec 0x001f001f 0x00180018>, + <0x6c0f0 0x0000801f 0x00000004>, + <0x6c0f8 0x001f001f 0x00180018>, + <0x6c0fc 0x0000801f 0x00000004>, + <0x6c104 0x001f001f 0x00180018>, + <0x6c108 0x0000801f 0x00000004>, + <0x6c110 0x00000001 0x00000001>, + <0x6c114 0x0000801f 0x00000004>, + <0x6c11c 0x0000801f 0x00000004>, + <0x6c124 0x0000801f 0x00000004>, + <0x6c12c 0x0000801f 0x00000004>, + <0x6c134 0x0000801f 0x00000004>, + <0x6c13c 0xffffffff 0x01010101>, + <0x6c140 0x0000801f 0x00000004>, + <0x6c148 0x0000801f 0x00000004>, + <0x6c150 0xffffffff 0x01010101>, + <0x6c154 0x0000ffff 0x00000101>, + <0x6c158 0x0000801f 0x00000004>, + <0x6c160 0x0000801f 0x00000004>, + <0x6c168 0x0000801f 0x00000004>, + <0x6c170 0x0000801f 0x00000004>, + <0x6c178 0x0000801f 0x00000004>, + <0x6c180 0xffffffff 0x01010101>, + <0x6c184 0x0000801f 0x00000004>, + <0x6c18c 0x0000801f 0x00000004>, + <0x6c194 0x0000801f 0x00000004>, + /* DPPLL_CORE_DFLT */ + <0x01040 0x0000000c 0x00000008>, + <0x01060 0x0001c000 0x0000c000>, + <0x01098 0x00000f00 0x00000800>, + <0x0109c 0x000fe000 0x000fe000>, + <0x01108 0x07c00000 0x04000000>, + <0x0110c 0x0000007c 0x00000010>, + /* DPPLL_TOP_DFLT */ + <0x00e8c 0x0000007f 0x0000003c>, + <0x00e9c 0x0000007f 0x00000032>, + <0x00eac 0x0000007f 0x00000032>, + <0x00ebc 0x0000007f 0x0000005a>, + /* ASDC_AUX_SHM_DFLT */ + <0x02308 0x000000fc 0x00000084>; + }; + + /* + * USB-C back-right: ADT atc-phy3. PS: the ATC's DP_0, DP_1 and + * USB_AON power states (+0, +0x20, +0x100), PSD: its common + * domain. Tunables before and after the lane crossbar setup. + */ + usb-c@413000000 { + reg = <0x4 0x13000000 0x0 0x84000>, + <0x3 0x082900c0 0x0 0x104>, + <0x3 0x03a13a00 0x0 0xc0>; + reg-names = "atc", "atc-ps", "atc-psd"; + apple,dfp-endpoint = <3>; + apple,usb-pd = <&hpm3>; + apple,tunable-pre = + /* ACIOPHY_CMN_DFLT */ + <0x00800 0x00fc0000 0x00fc0000>, + <0x0084c 0x000ff800 0x000ff800>, + <0x0085c 0x000000ff 0x00000080>, + <0x00860 0x007fffff 0x0052bc26>, + <0x00864 0x02c00000 0x00800000>, + /* CIOPLL_CORE_DFLT */ + <0x01614 0x0003ffc0 0x0001e5c0>, + <0x01640 0x00000007 0x00000003>, + <0x01668 0x00000ffc 0x000005f0>; + apple,tunable-post = + /* ATC_FABRIC */ + <0x6c004 0x003f003f 0x00200020>, + <0x6c00c 0x000000f1 0x000000a1>, + <0x6c010 0x0000801f 0x00000004>, + <0x6c018 0x003f003f 0x00200020>, + <0x6c020 0x000000f1 0x000000a1>, + <0x6c024 0x0000801f 0x00000004>, + <0x6c02c 0x007f007f 0x00410041>, + <0x6c034 0x000000f1 0x00000071>, + <0x6c038 0x0000801f 0x00000004>, + <0x6c040 0x0000001f 0x00000010>, + <0x6c048 0x0000801f 0x00000004>, + <0x6c050 0x007f007f 0x00450045>, + <0x6c058 0x000000f1 0x00000051>, + <0x6c05c 0x0000801f 0x00000004>, + <0x6c064 0x0000003f 0x00000020>, + <0x6c06c 0x0000801f 0x00000004>, + <0x6c074 0x00000001 0x00000001>, + <0x6c078 0x0000801f 0x00000004>, + <0x6c080 0x001f001f 0x00100010>, + <0x6c084 0x000000f1 0x00000011>, + <0x6c088 0x00000001 0x00000001>, + <0x6c08c 0x00000001 0x00000001>, + <0x6c090 0x0000801f 0x00000004>, + <0x6c098 0x001f001f 0x00100010>, + <0x6c09c 0x000000f1 0x00000011>, + <0x6c0a0 0x0000801f 0x00000004>, + <0x6c0a8 0x000000ff 0x000000a0>, + <0x6c0ac 0x0000801f 0x00000004>, + <0x6c0b4 0x0000003f 0x00000020>, + <0x6c0bc 0x0000801f 0x00000004>, + <0x6c0c4 0x0000003f 0x00000020>, + <0x6c0cc 0x0000801f 0x00000004>, + <0x6c0d4 0x0000000f 0x00000008>, + <0x6c0d8 0x0000801f 0x00000004>, + <0x6c0e0 0x001f001f 0x00180018>, + <0x6c0e4 0x0000801f 0x00000004>, + <0x6c0ec 0x001f001f 0x00180018>, + <0x6c0f0 0x0000801f 0x00000004>, + <0x6c0f8 0x001f001f 0x00180018>, + <0x6c0fc 0x0000801f 0x00000004>, + <0x6c104 0x001f001f 0x00180018>, + <0x6c108 0x0000801f 0x00000004>, + <0x6c110 0x00000001 0x00000001>, + <0x6c114 0x0000801f 0x00000004>, + <0x6c11c 0x0000801f 0x00000004>, + <0x6c124 0x0000801f 0x00000004>, + <0x6c12c 0x0000801f 0x00000004>, + <0x6c134 0x0000801f 0x00000004>, + <0x6c13c 0xffffffff 0x01010101>, + <0x6c140 0x0000801f 0x00000004>, + <0x6c148 0x0000801f 0x00000004>, + <0x6c150 0xffffffff 0x01010101>, + <0x6c154 0x0000ffff 0x00000101>, + <0x6c158 0x0000801f 0x00000004>, + <0x6c160 0x0000801f 0x00000004>, + <0x6c168 0x0000801f 0x00000004>, + <0x6c170 0x0000801f 0x00000004>, + <0x6c178 0x0000801f 0x00000004>, + <0x6c180 0xffffffff 0x01010101>, + <0x6c184 0x0000801f 0x00000004>, + <0x6c18c 0x0000801f 0x00000004>, + <0x6c194 0x0000801f 0x00000004>, + /* DPPLL_CORE_DFLT */ + <0x01040 0x0000000c 0x00000008>, + <0x01060 0x0001c000 0x0000c000>, + <0x01098 0x00000f00 0x00000800>, + <0x0109c 0x000fe000 0x000fe000>, + <0x01108 0x07c00000 0x04000000>, + <0x0110c 0x0000007c 0x00000010>, + /* DPPLL_TOP_DFLT */ + <0x00e8c 0x0000007f 0x0000003c>, + <0x00e9c 0x0000007f 0x00000032>, + <0x00eac 0x0000007f 0x00000032>, + <0x00ebc 0x0000007f 0x0000005a>, + /* ASDC_AUX_SHM_DFLT */ + <0x02308 0x000000fc 0x00000084>; + }; + }; + + /* + * HDMI: DCPEXT0 (post-office mailbox 6) drives the dedicated + * dptx-phy through the PS190 converter. ADT dcpext0, + * dart-dcpext0 and dart-dispext0 (tlbi-aperture-offset 0x480). + */ + dcpext_dart: iommu@32a340000 { + compatible = "apple,t8152-dart-gen3"; + reg = <0x3 0x2a340000 0x0 0x20000>, + <0x2 0x00000480 0x0 0x10>; + reg-names = "dart", "tlbi"; + interrupts = ; + #iommu-cells = <1>; + apple,dma-range = <0x108 0x0 0x8 0x0>; + }; + + dispext0_dart: iommu@32a300000 { + compatible = "apple,t8152-dart-gen3"; + reg = <0x3 0x2a300000 0x0 0xc000>, + <0x2 0x00000480 0x0 0x10>; + reg-names = "dart", "tlbi"; + interrupts = ; + #iommu-cells = <1>; + apple,dma-range = <0x108 0x0 0x8 0x0>; + }; + + dcpext_mbox: mailbox@316020600 { + compatible = "apple,t8152-dcp-mailbox"; + reg = <0x3 0x16020600 0x0 0x80>; + interrupts = , + ; + interrupt-names = "send-empty", "recv-not-empty"; + #mbox-cells = <0>; + }; + + dcpext_iboot: display@32ae00000 { + compatible = "apple,t8152-dcp-iboot"; + reg = <0x3 0x2ae00000 0x0 0x74000>, + <0x3 0x1a740000 0x0 0x10000>, + <0x3 0x1a700000 0x0 0x4000>, + <0x3 0x1a764000 0x0 0x4000>, + <0x3 0x00700480 0x0 0x4>; + /* aux: ADT dptx1-aux, aux-ps: PMGR GLOBAL_AUX_CTRL1 */ + reg-names = "coproc", "phy-core", "phy-apb", "aux", "aux-ps"; + mboxes = <&dcpext_mbox>; + iommus = <&dcpext_dart 23>; + dma-coherent; + apple,hdmi; + /* ADT dp2hdmi-gpio0 function-hdmi_hpd */ + hpd-gpios = <&pinctrl_aop 41 GPIO_ACTIVE_HIGH>; + + scanout { + iommus = <&dispext0_dart 0>; + dma-coherent; + }; + }; + }; +}; diff --git a/arch/arm64/boot/dts/apple/t8152-j873g.dts b/arch/arm64/boot/dts/apple/t8152-j873g.dts index 1513a2abbc83f..382f581656009 100644 --- a/arch/arm64/boot/dts/apple/t8152-j873g.dts +++ b/arch/arm64/boot/dts/apple/t8152-j873g.dts @@ -286,3 +286,4 @@ #include "t8152-j873-pmc.dtsi" #include "t8152-j873-qspi.dtsi" +#include "t8152-j873-dcp.dtsi" diff --git a/drivers/gpu/drm/Kconfig b/drivers/gpu/drm/Kconfig index 89c899e836797..d80f7832f6f27 100644 --- a/drivers/gpu/drm/Kconfig +++ b/drivers/gpu/drm/Kconfig @@ -302,6 +302,7 @@ config DRM_PRIVACY_SCREEN source "drivers/gpu/drm/adp/Kconfig" source "drivers/gpu/drm/amd/amdgpu/Kconfig" source "drivers/gpu/drm/apple/Kconfig" +source "drivers/gpu/drm/apple_dcpib/Kconfig" source "drivers/gpu/drm/arm/Kconfig" source "drivers/gpu/drm/armada/Kconfig" source "drivers/gpu/drm/asahi/Kconfig" diff --git a/drivers/gpu/drm/Makefile b/drivers/gpu/drm/Makefile index cd966b1b5b47e..ccc1d8ef193c8 100644 --- a/drivers/gpu/drm/Makefile +++ b/drivers/gpu/drm/Makefile @@ -231,6 +231,7 @@ obj-$(CONFIG_DRM_APPLE_FABRIC_CORE) += apple/dcp-fabric-core.o obj-$(CONFIG_DRM_APPLE_FABRIC_KUNIT_TEST) += apple/dcp-fabric-test.o obj-$(CONFIG_DRM_APPLE) += apple/ obj-$(CONFIG_DRM_APPLE_KUNIT_TEST) += apple/iomfb-state-test.o +obj-$(CONFIG_DRM_APPLE_DCPIB) += apple_dcpib/ obj-$(CONFIG_DRM_ASPEED_GFX) += aspeed/ obj-$(CONFIG_DRM_MCDE) += mcde/ obj-$(CONFIG_DRM_TIDSS) += tidss/ diff --git a/drivers/gpu/drm/apple_dcpib/Kconfig b/drivers/gpu/drm/apple_dcpib/Kconfig new file mode 100644 index 0000000000000..9e3ba21b4b821 --- /dev/null +++ b/drivers/gpu/drm/apple_dcpib/Kconfig @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +config DRM_APPLE_DCPIB + tristate "Apple T8152 DCP display (iBoot interface)" + depends on DRM && OF && ARM64 + depends on ARCH_APPLE || COMPILE_TEST + depends on APPLE_RTKIT + depends on SPMI + select APERTURE_HELPERS + select DRM_CLIENT_SELECTION + select DRM_GEM_DMA_HELPER + select DRM_KMS_HELPER + help + Display driver for the DCP of the Apple T8152 (Mac mini J873g) on + the firmware's small iBoot interface. It takes over the display the + bootloader brought up on the USB-C port. diff --git a/drivers/gpu/drm/apple_dcpib/Makefile b/drivers/gpu/drm/apple_dcpib/Makefile new file mode 100644 index 0000000000000..5a288e7df1a29 --- /dev/null +++ b/drivers/gpu/drm/apple_dcpib/Makefile @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +apple_dcpib-y := drv.o afk.o iboot.o dptx.o atc.o hdmi.o kms.o pd.o aux.o +obj-$(CONFIG_DRM_APPLE_DCPIB) += apple_dcpib.o diff --git a/drivers/gpu/drm/apple_dcpib/afk.c b/drivers/gpu/drm/apple_dcpib/afk.c new file mode 100644 index 0000000000000..01ca8668e44f6 --- /dev/null +++ b/drivers/gpu/drm/apple_dcpib/afk.c @@ -0,0 +1,740 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +/* + * AFKEPV2 ring-buffer endpoints of the T8152 DCP (port of aura src/afk.c). + * + * Each endpoint shares one DMA buffer with the firmware, which carves a TX + * and an RX ring out of it. Messages carry an afkv2 header (interface, seq) + * and a message header (token, type, category). The firmware publishes + * services (REPORT/PUBLISH), which the AP opens; AP commands use out-of-band + * TX/RX buffers, firmware callbacks are inline standard service calls. + */ + +#include +#include +#include +#include + +#include "dcpib.h" + +#define RBEP_TYPE GENMASK_ULL(63, 48) +#define RBEP_INIT 0x80 +#define RBEP_INIT_ACK 0xa0 +#define RBEP_GETBUF 0x89 +#define RBEP_GETBUF_ACK 0xa1 +#define RBEP_INIT_TX 0x8a +#define RBEP_INIT_RX 0x8b +#define RBEP_START 0xa3 +#define RBEP_START_ACK 0x86 +#define RBEP_SEND 0xa2 +#define RBEP_RECV 0x85 +#define RBEP_SHUTDOWN 0xc0 +#define RBEP_SHUTDOWN_ACK 0xc1 + +#define BLOCK_SHIFT 6 +#define GETBUF_SIZE GENMASK_ULL(31, 16) +#define GETBUF_TAG GENMASK_ULL(15, 0) +#define GETBUF_ACK_DVA GENMASK_ULL(47, 0) +#define INITRB_OFFSET GENMASK_ULL(47, 32) +#define INITRB_SIZE GENMASK_ULL(31, 16) +#define INITRB_TAG GENMASK_ULL(15, 0) +#define SEND_WPTR GENMASK_ULL(31, 0) + +#define QE_MAGIC 0x20504f49 /* ' POI' */ +#define SERVICE_CALL_MAGIC 0x69706378 + +#define AFKV2_REPORT 0 +#define AFKV2_COMMAND 1 +#define AFKV2_RESPONSE 2 +#define AFKV2_PUBLISH 0x11 +#define AFKV2_OPEN 0x12 +#define AFKV2_STD_SERVICE 0xc0 + +#define DCPIB_BUF_SIZE 0x4000 +#define DCPIB_TIMEOUT msecs_to_jiffies(3000) + +struct afk_qe { + __le32 magic; + __le32 size; + __le32 channel; + __le32 type; + u8 data[]; +} __packed; + +struct afkv2_hdr { + u8 seq; + u8 reserved; + __le16 interface; + __le32 length; +} __packed; + +struct afkv2_msg { + __le64 token; + u8 type; + u8 category; + u8 flags; + u8 reserved[5]; +} __packed; + +struct afkv2_cmd { + u8 flags; + u8 id; + __le16 reserved; + __le32 value; /* command: response capacity; response: IOReturn */ +} __packed; + +struct afkv2_oob { + __le64 rxbuf; + __le64 txbuf; + __le32 rxlen; + __le32 txlen; +} __packed; + +struct afkv2_service_call { + __le16 reserved; + __le16 group; + __le32 command; + __le32 length; + __le32 magic; + u8 padding[48]; +} __packed; + +struct afk_announce { + char name[32]; + u8 props[]; +} __packed; + +int dcpib_dma_alloc(struct dcpib *dcp, struct dcpib_dma *dma, size_t size) +{ + dma->size = PAGE_ALIGN(size); + dma->cpu = dma_alloc_coherent(dcp->dev, dma->size, &dma->iova, + GFP_KERNEL); + return dma->cpu ? 0 : -ENOMEM; +} + +void dcpib_dma_free(struct dcpib *dcp, struct dcpib_dma *dma) +{ + if (dma->cpu) + dma_free_coherent(dcp->dev, dma->size, dma->cpu, dma->iova); + dma->cpu = NULL; +} + +static int dcpib_send(struct dcpib_ep *ep, u64 msg) +{ + return apple_rtkit_send_message(ep->dcp->rtk, ep->ep, msg, NULL, false); +} + +static int dcpib_ring_init(struct dcpib_ep *ep, struct dcpib_ring *rb, u64 base, + u64 size) +{ + u32 bufsz, block; + void *hdr; + + if (!ep->ring.cpu || base > ep->ring.size || + size > ep->ring.size - base || size < 4 * 0x40) + return -EINVAL; + + hdr = ep->ring.cpu + base; + bufsz = le32_to_cpu(READ_ONCE(*(__le32 *)hdr)); + /* Three blocks hold size, read pointer and write pointer. */ + if (!bufsz || bufsz >= size || (size - bufsz) % 3) + return -EINVAL; + block = (size - bufsz) / 3; + if (block < 0x40 || !is_power_of_2(block) || bufsz % block) + return -EINVAL; + + rb->rptr = hdr + block; + rb->wptr = hdr + 2 * block; + rb->buf = hdr + 3 * block; + rb->bufsz = bufsz; + rb->block = block; + if (READ_ONCE(*rb->rptr) >= bufsz || READ_ONCE(*rb->wptr) >= bufsz) + return -EINVAL; + rb->ready = true; + return 0; +} + +/* Queue one message on the TX ring and tell the firmware. */ +static int dcpib_tx(struct dcpib_ep *ep, const void *data, size_t size) +{ + struct dcpib_ring *rb = &ep->tx; + struct afk_qe *qe; + u32 rptr, wptr, advance; + int ret; + + mutex_lock(&ep->tx_lock); + if (!rb->ready || size > rb->bufsz - sizeof(*qe)) { + ret = -EINVAL; + goto out; + } + + rptr = le32_to_cpu(READ_ONCE(*rb->rptr)); + wptr = le32_to_cpu(READ_ONCE(*rb->wptr)); + advance = ALIGN(sizeof(*qe) + size, rb->block); + + if (wptr < rptr && advance >= rptr - wptr) + goto full; + if (wptr >= rptr) { + bool fits = advance < rb->bufsz - wptr || + (advance == rb->bufsz - wptr && rptr); + + if (!fits && advance >= rptr) + goto full; + } + + qe = rb->buf + wptr; + qe->magic = cpu_to_le32(QE_MAGIC); + qe->size = cpu_to_le32(size); + qe->channel = 0; + qe->type = 0; + wptr += sizeof(*qe); + /* A payload that does not fit before the end starts at offset 0. */ + if (size > rb->bufsz - wptr) { + memcpy(rb->buf, qe, sizeof(*qe)); + qe = rb->buf; + wptr = sizeof(*qe); + } + memcpy(qe->data, data, size); + wptr = ALIGN(wptr + size, rb->block); + if (wptr >= rb->bufsz) + wptr = 0; + + dma_mb(); + WRITE_ONCE(*rb->wptr, cpu_to_le32(wptr)); + dma_wmb(); + ret = dcpib_send(ep, FIELD_PREP(RBEP_TYPE, RBEP_SEND) | + FIELD_PREP(SEND_WPTR, wptr)); + goto out; +full: + dev_err(ep->dcp->dev, "AFK[%02x]: TX ring full\n", ep->ep); + ret = -ENOSPC; +out: + mutex_unlock(&ep->tx_lock); + return ret; +} + +static int dcpib_tx_v2(struct dcpib_ep *ep, u16 interface, void *packet, + size_t size) +{ + struct afkv2_hdr *hdr = packet; + + mutex_lock(&ep->tx_lock); + hdr->seq = ep->seq++; + mutex_unlock(&ep->tx_lock); + hdr->interface = cpu_to_le16(interface); + hdr->length = cpu_to_le32(size - sizeof(*hdr)); + return dcpib_tx(ep, packet, size); +} + +static struct dcpib_service *dcpib_find_service(struct dcpib_ep *ep, + u16 interface) +{ + for (unsigned int i = 0; i < ep->nr_services; i++) + if (ep->services[i].interface == interface) + return &ep->services[i]; + return NULL; +} + +static void dcpib_announce(struct dcpib_ep *ep, u16 interface, + const void *payload, size_t size) +{ + const struct afk_announce *ann = payload; + struct dcpib_service *service; + struct { + struct afkv2_hdr hdr; + struct afkv2_msg msg; + } __packed open = { + .msg = { .type = AFKV2_OPEN, .category = AFKV2_REPORT, .flags = 1 }, + }; + + if (!interface || size < sizeof(*ann) || + strnlen(ann->name, sizeof(ann->name)) == sizeof(ann->name)) + return; + if (dcpib_find_service(ep, interface)) + return; + if (ep->nr_services >= DCPIB_MAX_SERVICES) { + dev_warn(ep->dcp->dev, "AFK[%02x]: no room for service %s\n", + ep->ep, ann->name); + return; + } + + service = &ep->services[ep->nr_services]; + service->ep = ep; + service->ops = ep->ops; + service->interface = interface; + service->unit = ep->nr_services; + strscpy(service->name, ann->name, sizeof(service->name)); + ep->nr_services++; + + dev_info(ep->dcp->dev, "AFK[%02x]: opening %s on interface %u\n", + ep->ep, service->name, interface); + if (dcpib_tx_v2(ep, interface, &open, sizeof(open))) + dev_err(ep->dcp->dev, "AFK[%02x]: failed to open %s\n", ep->ep, + service->name); + if (ep->nr_services == ep->expected) + complete_all(&ep->services_done); +} + +/* Answer a firmware callback we cannot handle: empty, kIOReturnUnsupported. */ +static void dcpib_reject(struct dcpib_ep *ep, u16 interface, + const struct afkv2_msg *msg, + const struct afkv2_cmd *cmd, bool oob) +{ + struct { + struct afkv2_hdr hdr; + struct afkv2_msg msg; + struct afkv2_cmd cmd; + struct afkv2_oob oob; + } __packed reply = { + .msg = { .token = msg->token, .type = msg->type, + .category = AFKV2_RESPONSE }, + .cmd = { .flags = cmd->flags, .id = cmd->id, + .value = cpu_to_le32(KIO_RETURN_UNSUPPORTED) }, + }; + size_t len = sizeof(reply) - sizeof(reply.oob); + + if (oob) { + const struct afkv2_oob *o = (const void *)(cmd + 1); + + /* Keep the descriptors, return no data; never touch them. */ + reply.oob.rxbuf = o->rxbuf; + reply.oob.txbuf = o->txbuf; + len += sizeof(reply.oob); + } + dev_dbg(ep->dcp->dev, "AFK[%02x]: rejecting callback type %#x\n", + ep->ep, msg->type); + dcpib_tx_v2(ep, interface, &reply, len); +} + +static void dcpib_callback(struct dcpib_ep *ep, u16 interface, + const struct afkv2_msg *msg, const void *payload, + size_t size) +{ + const struct afkv2_cmd *cmd = payload; + const struct afkv2_service_call *call; + struct dcpib_service *service; + struct afkv2_hdr *rhdr; + struct afkv2_msg *rmsg; + struct afkv2_cmd *rcmd; + struct afkv2_service_call *rcall; + size_t body, len; + __le32 status; + u8 *reply; + + if (size < sizeof(*cmd) || cmd->flags & ~1) + return; + if ((cmd->flags & 1) && size < sizeof(*cmd) + sizeof(struct afkv2_oob)) + return; + + service = dcpib_find_service(ep, interface); + body = size - sizeof(*cmd); + call = (const void *)(cmd + 1); + /* Only the inline standard service call form is supported. */ + if (msg->type != AFKV2_STD_SERVICE || cmd->flags || !service || + !service->ops || !service->ops->call || + body < sizeof(*call) + sizeof(u32) || + le32_to_cpu(cmd->value) < body || + le32_to_cpu(call->magic) != SERVICE_CALL_MAGIC || + le32_to_cpu(call->length) != body - sizeof(*call)) { + dcpib_reject(ep, interface, msg, cmd, cmd->flags & 1); + return; + } + + len = sizeof(*rhdr) + sizeof(*rmsg) + size; + reply = kzalloc(len, GFP_KERNEL); + if (!reply) + return; + rhdr = (void *)reply; + rmsg = (void *)(rhdr + 1); + rcmd = (void *)(rmsg + 1); + rcall = (void *)(rcmd + 1); + rmsg->token = msg->token; + rmsg->type = msg->type; + rmsg->category = AFKV2_RESPONSE; + rcmd->id = cmd->id; + memcpy(rcall, call, body); + status = cpu_to_le32(service->ops->call(service, + le16_to_cpu(call->group), + le32_to_cpu(call->command), call + 1, + le32_to_cpu(call->length), rcall + 1, + le32_to_cpu(call->length))); + memcpy(rcall + 1, &status, sizeof(status)); + if (status) + dev_dbg(ep->dcp->dev, + "AFK[%02x]: callback %s group %u command %u -> %#x\n", + ep->ep, service->name, le16_to_cpu(call->group), + le32_to_cpu(call->command), le32_to_cpu(status)); + dcpib_tx_v2(ep, interface, reply, len); + kfree(reply); +} + +static void dcpib_response(struct dcpib_ep *ep, u16 interface, + const struct afkv2_msg *msg, const void *payload, + size_t size) +{ + const struct afkv2_cmd *cmd = payload; + const void *data; + size_t len; + int ret = 0; + + if (!ep->cmd_waiting || interface != ep->cmd_interface || + msg->type != ep->cmd_type || size < sizeof(*cmd) || + cmd->id != ep->cmd_id) { + dev_warn(ep->dcp->dev, + "AFK[%02x]: unexpected response on interface %u\n", + ep->ep, interface); + return; + } + + data = cmd + 1; + len = size - sizeof(*cmd); + if (cmd->flags & ~1) { + ret = -EPROTO; + } else if (le32_to_cpu(cmd->value)) { + dev_dbg(ep->dcp->dev, "AFK[%02x]: command %#x returned %#x\n", + ep->ep, msg->type, le32_to_cpu(cmd->value)); + ret = -EIO; + } else if (cmd->flags & 1) { + const struct afkv2_oob *oob = data; + + if (len < sizeof(*oob)) { + ret = -EPROTO; + } else { + data = oob + 1; + len -= sizeof(*oob); + if (le32_to_cpu(oob->rxlen)) { + if (le64_to_cpu(oob->rxbuf) != ep->rxbuf.iova || + le32_to_cpu(oob->rxlen) > ep->rxbuf.size) { + ret = -EPROTO; + } else { + dma_rmb(); + data = ep->rxbuf.cpu; + len = le32_to_cpu(oob->rxlen); + } + } + } + } + if (!ret && len > ep->cmd_out_len) + ret = -EOVERFLOW; + if (!ret && len) + memcpy(ep->cmd_out, data, len); + ep->cmd_out_len = ret ? 0 : len; + ep->cmd_ret = ret; + ep->cmd_waiting = false; + complete(&ep->cmd_done); +} + +static void dcpib_rx_work(struct work_struct *work) +{ + struct dcpib_ep *ep = container_of(work, struct dcpib_ep, rx_work); + struct dcpib_ring *rb = &ep->rx; + + if (!rb->ready) + return; + + for (;;) { + const struct afkv2_hdr *hdr; + const struct afkv2_msg *msg; + struct afk_qe *qe; + u32 rptr, wptr, qsize; + + dma_rmb(); + rptr = le32_to_cpu(READ_ONCE(*rb->rptr)); + wptr = le32_to_cpu(READ_ONCE(*rb->wptr)); + if (rptr == wptr) + break; + if (rptr >= rb->bufsz || rptr % rb->block) + goto corrupt; + + qe = rb->buf + rptr; + qsize = le32_to_cpu(qe->size); + if (le32_to_cpu(qe->magic) != QE_MAGIC || + qsize > rb->bufsz - sizeof(*qe)) + goto corrupt; + /* An entry that does not fit before the end is at offset 0. */ + if (sizeof(*qe) + qsize > rb->bufsz - rptr) { + rptr = 0; + qe = rb->buf; + qsize = le32_to_cpu(qe->size); + if (le32_to_cpu(qe->magic) != QE_MAGIC || + qsize > rb->bufsz - sizeof(*qe)) + goto corrupt; + } + + hdr = (const void *)qe->data; + msg = (const void *)(hdr + 1); + if (!qe->channel && !qe->type && + qsize >= sizeof(*hdr) + sizeof(*msg) && + le32_to_cpu(hdr->length) == qsize - sizeof(*hdr)) { + u16 interface = le16_to_cpu(hdr->interface); + const void *payload = msg + 1; + size_t size = qsize - sizeof(*hdr) - sizeof(*msg); + + if (msg->category == AFKV2_REPORT && + msg->type == AFKV2_PUBLISH) + dcpib_announce(ep, interface, payload, size); + else if (msg->category == AFKV2_RESPONSE) + dcpib_response(ep, interface, msg, payload, + size); + else if (msg->category == AFKV2_COMMAND) + dcpib_callback(ep, interface, msg, payload, + size); + else + dev_dbg(ep->dcp->dev, "AFK[%02x]: if %u cat %u type %#x: %*ph\n", + ep->ep, interface, msg->category, msg->type, + (int)min_t(size_t, size, 96), payload); + } else { + dev_dbg(ep->dcp->dev, "AFK[%02x]: ignoring message\n", + ep->ep); + } + + dma_mb(); + rptr = ALIGN(rptr + sizeof(*qe) + qsize, rb->block); + if (rptr >= rb->bufsz) + rptr = 0; + WRITE_ONCE(*rb->rptr, cpu_to_le32(rptr)); + } + return; + +corrupt: + dev_err(ep->dcp->dev, "AFK[%02x]: corrupt RX ring, stopping it\n", + ep->ep); + rb->ready = false; +} + +void dcpib_ep_recv(struct dcpib_ep *ep, u64 message) +{ + u32 type = FIELD_GET(RBEP_TYPE, message); + u64 base, size; + int ret; + + switch (type) { + case RBEP_INIT_ACK: + break; + case RBEP_GETBUF: + size = FIELD_GET(GETBUF_SIZE, message) << BLOCK_SHIFT; + ep->tag = FIELD_GET(GETBUF_TAG, message); + if (ep->ring.cpu || dcpib_dma_alloc(ep->dcp, &ep->ring, size)) { + dev_err(ep->dcp->dev, "AFK[%02x]: no ring buffer\n", + ep->ep); + break; + } + dcpib_send(ep, FIELD_PREP(RBEP_TYPE, RBEP_GETBUF_ACK) | + FIELD_PREP(GETBUF_ACK_DVA, ep->ring.iova)); + break; + case RBEP_INIT_TX: + case RBEP_INIT_RX: + base = FIELD_GET(INITRB_OFFSET, message) << BLOCK_SHIFT; + size = FIELD_GET(INITRB_SIZE, message) << BLOCK_SHIFT; + if (FIELD_GET(INITRB_TAG, message) != ep->tag) { + dev_err(ep->dcp->dev, "AFK[%02x]: wrong ring tag\n", + ep->ep); + break; + } + ret = dcpib_ring_init(ep, type == RBEP_INIT_RX ? &ep->rx : &ep->tx, + base, size); + if (ret) { + dev_err(ep->dcp->dev, "AFK[%02x]: bad ring %#llx+%#llx\n", + ep->ep, base, size); + break; + } + if (ep->rx.ready && ep->tx.ready) + dcpib_send(ep, FIELD_PREP(RBEP_TYPE, RBEP_START)); + break; + case RBEP_START_ACK: + complete_all(&ep->started); + break; + case RBEP_RECV: + queue_work(ep->dcp->wq, &ep->rx_work); + break; + case RBEP_SHUTDOWN_ACK: + complete_all(&ep->stopped); + break; + default: + dev_warn(ep->dcp->dev, "AFK[%02x]: unknown message %#llx\n", + ep->ep, message); + break; + } +} + +struct dcpib_ep *dcpib_ep_start(struct dcpib *dcp, u8 epnum, + const struct dcpib_service_ops *ops, + unsigned int expected) +{ + struct dcpib_ep *ep; + int ret; + + if (epnum < DCPIB_FIRST_EP || epnum >= DCPIB_FIRST_EP + DCPIB_NR_EPS) + return ERR_PTR(-EINVAL); + + ep = devm_kzalloc(dcp->dev, sizeof(*ep), GFP_KERNEL); + if (!ep) + return ERR_PTR(-ENOMEM); + ep->dcp = dcp; + ep->ep = epnum; + ep->ops = ops; + ep->expected = expected; + init_completion(&ep->started); + init_completion(&ep->stopped); + init_completion(&ep->services_done); + init_completion(&ep->cmd_done); + mutex_init(&ep->tx_lock); + mutex_init(&ep->cmd_lock); + INIT_WORK(&ep->rx_work, dcpib_rx_work); + + ret = dcpib_dma_alloc(dcp, &ep->txbuf, DCPIB_BUF_SIZE); + if (!ret) + ret = dcpib_dma_alloc(dcp, &ep->rxbuf, DCPIB_BUF_SIZE); + if (ret) + goto err; + + dcp->eps[epnum - DCPIB_FIRST_EP] = ep; + /* + * START_EP for an application endpoint of the adopted session; the + * bootloader consumed the endpoint map, so apple_rtkit_start_ep() + * does not know it. + */ + ret = apple_rtkit_send_message(dcp->rtk, 0, + FIELD_PREP(GENMASK_ULL(59, 52), 5) | + FIELD_PREP(GENMASK_ULL(39, 32), epnum) | + BIT_ULL(1), NULL, false); + if (ret) { + dev_err(dcp->dev, "failed to start endpoint %#x: %d\n", epnum, ret); + goto err_unregister; + } + apple_rtkit_adopt_ep(dcp->rtk, epnum); + ret = dcpib_send(ep, FIELD_PREP(RBEP_TYPE, RBEP_INIT)); + if (ret) + goto err_unregister; + if (!wait_for_completion_timeout(&ep->started, DCPIB_TIMEOUT)) { + dev_err(dcp->dev, "endpoint %#x did not start\n", epnum); + ret = -ETIMEDOUT; + goto err_unregister; + } + if (!wait_for_completion_timeout(&ep->services_done, DCPIB_TIMEOUT) && + !ep->nr_services) { + dev_err(dcp->dev, "endpoint %#x announced no service\n", epnum); + ret = -ETIMEDOUT; + goto err_unregister; + } + if (ep->nr_services < expected) + dev_warn(dcp->dev, "endpoint %#x: %u of %u services\n", epnum, + ep->nr_services, expected); + return ep; + +err_unregister: + /* The firmware may still own the rings; keep them allocated. */ + return ERR_PTR(ret); +err: + dcpib_dma_free(dcp, &ep->txbuf); + dcpib_dma_free(dcp, &ep->rxbuf); + return ERR_PTR(ret); +} + +void dcpib_ep_stop(struct dcpib_ep *ep) +{ + reinit_completion(&ep->stopped); + if (dcpib_send(ep, FIELD_PREP(RBEP_TYPE, RBEP_SHUTDOWN)) || + !wait_for_completion_timeout(&ep->stopped, DCPIB_TIMEOUT)) { + dev_err(ep->dcp->dev, "endpoint %#x did not stop\n", ep->ep); + return; + } + ep->rx.ready = ep->tx.ready = false; + flush_work(&ep->rx_work); +} + +int dcpib_command(struct dcpib_service *service, u8 type, const void *tx, + size_t txlen, void *rx, size_t *rxlen) +{ + struct dcpib_ep *ep = service->ep; + size_t cap = rxlen ? *rxlen : 0; + struct { + struct afkv2_hdr hdr; + struct afkv2_msg msg; + struct afkv2_cmd cmd; + struct afkv2_oob oob; + } __packed packet = { + .msg = { .type = type, .category = AFKV2_COMMAND }, + }; + int ret; + + if (txlen > ep->txbuf.size || cap > ep->rxbuf.size) + return -EINVAL; + + mutex_lock(&ep->cmd_lock); + memcpy(ep->txbuf.cpu, tx, txlen); + packet.cmd.flags = 1; + packet.cmd.id = ep->next_cmd_id++; + packet.cmd.value = cpu_to_le32(cap); + packet.oob.rxbuf = cpu_to_le64(cap ? ep->rxbuf.iova : 0); + packet.oob.txbuf = cpu_to_le64(ep->txbuf.iova); + packet.oob.rxlen = cpu_to_le32(cap); + packet.oob.txlen = cpu_to_le32(txlen); + + reinit_completion(&ep->cmd_done); + ep->cmd_interface = service->interface; + ep->cmd_type = type; + ep->cmd_id = packet.cmd.id; + ep->cmd_out = rx; + ep->cmd_out_len = cap; + ep->cmd_ret = -ETIMEDOUT; + WRITE_ONCE(ep->cmd_waiting, true); + + ret = dcpib_tx_v2(ep, service->interface, &packet, sizeof(packet)); + if (!ret && !wait_for_completion_timeout(&ep->cmd_done, DCPIB_TIMEOUT)) + ret = -ETIMEDOUT; + if (!ret) + ret = ep->cmd_ret; + if (!ret && rxlen) + *rxlen = ep->cmd_out_len; + WRITE_ONCE(ep->cmd_waiting, false); + /* A late response must not land in the caller's buffer. */ + flush_work(&ep->rx_work); + mutex_unlock(&ep->cmd_lock); + + if (ret) + dev_dbg(ep->dcp->dev, "AFK[%02x]: command %#x on %s: %d\n", + ep->ep, type, service->name, ret); + return ret; +} + +/* An AP-initiated standard service call (aura afk_service_call). */ +int dcpib_service_call(struct dcpib_service *service, u16 group, u32 command, + const void *data, size_t data_len, size_t data_pad, + void *output, size_t output_len, size_t output_pad) +{ + struct afkv2_service_call *call; + size_t len = max(data_len + data_pad, output_len + output_pad) + + sizeof(*call); + size_t rxlen = len; + u8 *buf; + int ret; + + buf = kzalloc(len, GFP_KERNEL); + if (!buf) + return -ENOMEM; + call = (void *)buf; + call->group = cpu_to_le16(group); + call->command = cpu_to_le32(command); + call->length = cpu_to_le32(data_len + data_pad); + call->magic = cpu_to_le32(SERVICE_CALL_MAGIC); + if (data_len) + memcpy(call + 1, data, data_len); + + ret = dcpib_command(service, AFKV2_STD_SERVICE, buf, len, buf, &rxlen); + if (ret) + goto out; + if (rxlen < sizeof(*call) || + le32_to_cpu(call->magic) != SERVICE_CALL_MAGIC || + le16_to_cpu(call->group) != group || + le32_to_cpu(call->command) != command || + le32_to_cpu(call->length) > rxlen - sizeof(*call) || + le32_to_cpu(call->length) < output_len) { + ret = -EPROTO; + goto out; + } + if (output && output_len) + memcpy(output, call + 1, output_len); +out: + kfree(buf); + return ret; +} diff --git a/drivers/gpu/drm/apple_dcpib/atc.c b/drivers/gpu/drm/apple_dcpib/atc.c new file mode 100644 index 0000000000000..a284a4ca195f9 --- /dev/null +++ b/drivers/gpu/drm/apple_dcpib/atc.c @@ -0,0 +1,789 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * T8152/J873g ATC DisplayPort PHYs for the DCP's DPTX port (from aura + * src/dcp/atc_phy.c, which drives ATC3). + * + * The bootloader trains the USB-C display before Linux starts; this driver + * takes that link over and then follows the firmware: link rate changes + * (stop and set up the lanes again), drive settings, the DCP0 -> ATCn/DFP0 + * display crossbar route, and with the display off or unplugged a full + * power down and a cold start. The DCP drives one USB-C port at a time: + * the one a sink is plugged into (each port's PD controller is polled). + */ + +#include +#include +#include +#include +#include +#include + +#include "dcpib.h" + +#define ATC_AON 0x1e00 +#define ATC_CFG 0x60000 +#define ATC_XBAR 0x74000 +#define ATC_TIMEOUT_US 10000 + +struct dcpib_atc_port { + void __iomem *base; + unsigned int index; /* ATC instance */ + /* display-crossbar0 DFP endpoint of this ATC */ + u32 dfp; + struct spmi_device *pd; + /* AP DisplayPort AUX controller aux0 (shared), for the EDID */ + struct dcpib_aux *aux; + /* PMGR power states of the four ATC dependencies (aura gate_addr) */ + void __iomem *ps; + void __iomem *psd; + /* the dependencies this driver powered up, and may power down */ + u8 owned; + /* {offset, mask, value} tunables from the ADT, before/after xbar config */ + const __be32 *tunable[2]; + int tunable_len[2]; + /* PMGR DP clock control (shared): the divider selects the link rate */ + void __iomem *dp_clock; + bool swap; + bool active; + bool route_started; + u32 link_rate; +}; + +struct dcpib_atc { + struct dcpib_aux aux; + struct dcpib_atc_port port[4]; + unsigned int nports; + /* the port the DCP drives, and the one a sink was found on */ + struct dcpib_atc_port *cur, *next; +}; + +static struct dcpib_atc_port *atc_cur(struct dcpib *dcp) +{ + return ((struct dcpib_atc *)dcp->phy_priv)->cur; +} + +static const u32 atc_rates[] = { + DCPIB_LINK_RATE_RBR, DCPIB_LINK_RATE_HBR, DCPIB_LINK_RATE_HBR2, + DCPIB_LINK_RATE_HBR3, +}; + +static void atc_update(struct dcpib_atc_port *atc, u32 offset, u32 mask, u32 value) +{ + u32 reg = readl(atc->base + offset); + + writel((reg & ~mask) | value, atc->base + offset); +} + +static int atc_poll(struct dcpib_atc_port *atc, u32 offset, u32 mask, u32 value) +{ + u32 reg; + + return readl_poll_timeout(atc->base + offset, reg, + (reg & mask) == value, 1, ATC_TIMEOUT_US); +} + +static int atc_route_stop(struct dcpib *dcp, struct dcpib_atc_port *atc) +{ + if (!atc->route_started) + return 0; + atc_update(atc, ATC_XBAR + 0x3c, 1, 0); + atc_update(atc, ATC_XBAR, 1, 0); + atc_update(atc, ATC_XBAR + 0xc, 1, 0); + atc_update(atc, ATC_XBAR + 0x24, 0x100, 0); + udelay(1); + /* + * With the pipe powered off (the bootloader hands the display over + * that way) nothing acknowledges the stop; the route is idle then, so + * finish it anyway. + */ + if (atc_poll(atc, ATC_XBAR + 0x800, 1, 0) || + atc_poll(atc, ATC_XBAR + 0x808, 1, 0) || + atc_poll(atc, ATC_XBAR + 0x81c, 0x100, 0)) + dev_info(dcp->dev, "ATC%u: no crossbar stop acknowledgement\n", atc->index); + atc_update(atc, ATC_XBAR + 8, 1, 0); + atc_update(atc, ATC_XBAR + 0x20, 7, 0); + atc_update(atc, ATC_XBAR + 0x38, 0x700, 0); + atc_update(atc, ATC_XBAR + 4, 1, 1); + atc_update(atc, ATC_XBAR + 0x1c, 1, 1); + atc_update(atc, ATC_XBAR + 0x34, 0x100, 0x100); + atc->route_started = false; + dev_dbg(dcp->dev, "ATC%u: display route disabled\n", atc->index); + return 0; +} + +/* + * A source -> ATCn/DFP0 route, PCLK1: both DFP0 mux fields select the + * source (AppleT8142ATCDPXBAR::connect): DCP0 or an AP AUX controller. + */ +#define ATC_SEL_DCP0 0 +#define ATC_SEL_AUX0 9 /* UFP endpoint 5 + 4 (getMuxSelector) */ + +static int atc_route_start_sel(struct dcpib *dcp, struct dcpib_atc_port *atc, + u32 sel) +{ + if (atc->route_started) + return 0; + atc_update(atc, ATC_XBAR + 0x44, 0x00f00000, sel << 20); + atc_update(atc, ATC_XBAR + 0x44, 0x00000f00, sel << 8); + atc_update(atc, ATC_XBAR + 4, 1, 0); + atc_update(atc, ATC_XBAR + 0x1c, 1, 0); + atc_update(atc, ATC_XBAR + 0x34, 0x100, 0); + udelay(1); + if (atc_poll(atc, ATC_XBAR + 0x804, 1, 0) || + atc_poll(atc, ATC_XBAR + 0x818, 1, 0) || + atc_poll(atc, ATC_XBAR + 0x82c, 0x100, 0)) { + dev_err(dcp->dev, "ATC%u: crossbar reset acknowledgement timeout\n", atc->index); + return -ETIMEDOUT; + } + atc_update(atc, ATC_XBAR + 8, 1, 1); + atc_update(atc, ATC_XBAR + 0x20, 7, 1); + atc_update(atc, ATC_XBAR + 0x38, 0x700, 0x100); + atc_update(atc, ATC_XBAR + 0x48, 1, 0); + atc_update(atc, ATC_XBAR, 1, 1); + atc_update(atc, ATC_XBAR + 0xc, 1, 1); + atc_update(atc, ATC_XBAR + 0x24, 0x100, 0x100); + atc_update(atc, ATC_XBAR + 0x4c, 0x100, 0x100); + atc_update(atc, ATC_XBAR + 0x3c, 1, 1); + atc->route_started = true; + dev_dbg(dcp->dev, "ATC%u: source %u to local DFP0 route configured\n", atc->index, sel); + return 0; +} + +static int atc_route_start(struct dcpib *dcp, struct dcpib_atc_port *atc) +{ + return atc_route_start_sel(dcp, atc, ATC_SEL_DCP0); +} + +/* Common four-lane setup; the rate-dependent fields follow in atc_dp_start. */ +static const struct { + u32 offset, mask, value; +} atc_dp_setup[] = { + { 0x2044, 0xffff, 0x4b0 }, + { 0x4c, 0x3c0000, 0x3c0000 }, + { 0x4c80, 0x40, 0x40 }, + { 0xdc80, 0x40, 0x40 }, + { 0x4fd4, 0x2000000, 0x2000000 }, + { 0x4fd4, 0x1800000, 0x800000 }, + { 0xdfd4, 0x2000000, 0x2000000 }, + { 0xdfd4, 0x1800000, 0x800000 }, + { 0x4d84, 0x1c0000, 0 }, + { 0x4fd4, 1, 0 }, + { 0x4fd4, 8, 0 }, + { 0x4fd4, 2, 2 }, + { 0x4fd4, 0x10, 0x10 }, + { 0x8904, 0x1c0000, 0 }, + { 0x8800, 0x10000000, 0 }, + { 0x8800, 0x80000000, 0 }, + { 0x8800, 0x20000000, 0x20000000 }, + { 0x8804, 1, 1 }, + { 0xdd84, 0x1c0000, 0 }, + { 0xdfd4, 1, 0 }, + { 0xdfd4, 8, 0 }, + { 0xdfd4, 2, 2 }, + { 0xdfd4, 0x10, 0x10 }, + { 0x11904, 0x1c0000, 0 }, + { 0x11800, 0x10000000, 0 }, + { 0x11800, 0x80000000, 0 }, + { 0x11800, 0x20000000, 0x20000000 }, + { 0x11804, 1, 1 }, + { 0x3c04, 2, 2 }, + { 0x3c04, 1, 1 }, + { 0xcc04, 2, 2 }, + { 0xcc04, 1, 1 }, +}; + +/* Program the PMGR DP clock divider and wait for the change to settle. */ +static int atc_clock_write(struct dcpib_atc_port *atc, u32 divider) +{ + u32 reg = readl(atc->dp_clock); + + writel((reg & ~0x0f000000) | (divider << 24), atc->dp_clock); + return readl_poll_timeout(atc->dp_clock, reg, !(reg & BIT(30)), 1, + ATC_TIMEOUT_US); +} + +/* Stop the DP lanes: route, shim clock, then both PMAs (aura atc_dp_stop). */ +static int atc_dp_stop(struct dcpib *dcp, struct dcpib_atc_port *atc) +{ + u32 reg; + + atc_route_stop(dcp, atc); + atc_update(atc, ATC_CFG + 0x14, 7, 7); + atc_update(atc, ATC_CFG + 0x18, 1, 0); + if (readl_poll_timeout(atc->base + ATC_CFG + 0x1c, reg, !(reg & 1), 1, + ATC_TIMEOUT_US)) { + dev_err(dcp->dev, "ATC%u: DPTX clock stop failed\n", atc->index); + return -ETIMEDOUT; + } + atc_update(atc, 0x2040, BIT(16), 0); + udelay(5); + atc_update(atc, 0x2000, BIT(15), 0); + atc_update(atc, 0x2000, BIT(13), 0); + atc_update(atc, 0x2000, BIT(14), 0); + atc_update(atc, 0x2000, BIT(2), 0); + atc_update(atc, 0x2000, BIT(3), BIT(3)); + atc_update(atc, 0xe10, BIT(2), BIT(2)); + atc_update(atc, 0xe08, BIT(29), BIT(29)); + /* Remove both PMA clock requests, then clamp and power down both lanes. */ + atc_update(atc, ATC_AON + 0x24, BIT(12) | BIT(16) | BIT(14) | BIT(18), 0); + udelay(1); + for (u32 offset = 0x1e18; offset <= 0x1e1c; offset += 4) { + atc_update(atc, offset, BIT(5), 0); + udelay(1); + atc_update(atc, offset, BIT(0), BIT(0)); + udelay(1); + atc_update(atc, offset, BIT(2), 0); + udelay(1); + atc_update(atc, offset, BIT(1), 0); + udelay(1); + } + atc->link_rate = 0; + return 0; +} + +/* Four-lane DP setup at rate index r (aura atc_phy_set_link_rate). */ +static int atc_dp_start(struct dcpib *dcp, struct dcpib_atc_port *atc, u32 r) +{ + static const u32 tx_ctrl[] = { 0x4eb0, 0x8b10, 0xdeb0, 0x11b10 }; + static const u32 wake_count[] = { 0xbb5, 0xbb7, 0xbb8, 0xbb9 }; + int ret; + + if (readl(atc->dp_clock) & BIT(30)) + return -EBUSY; + atc_update(atc, ATC_CFG + 0x14, 7, 0); + atc_update(atc, ATC_CFG + 0x18, 1, 1); + if (!r) { + atc_update(atc, 0xe10, BIT(2), BIT(2)); + atc_update(atc, 0xe08, BIT(29), BIT(29)); + } + for (u32 i = 0; i < 2; i++) { + u32 offset = 0x1e18 + 4 * i; + + atc_update(atc, ATC_AON + 0x24, BIT(12 + 2 * i), BIT(12 + 2 * i)); + udelay(1); + atc_update(atc, ATC_AON + 0x24, BIT(16 + 2 * i), BIT(16 + 2 * i)); + udelay(1); + atc_update(atc, offset, BIT(1), BIT(1)); + udelay(1); + atc_update(atc, offset, BIT(2), BIT(2)); + udelay(1); + atc_update(atc, offset, BIT(0), 0); + udelay(1); + atc_update(atc, offset, BIT(5), BIT(5)); + udelay(2); + } + atc_update(atc, 0x2000, 0x380000, r << 19); + for (unsigned int i = 0; i < ARRAY_SIZE(atc_dp_setup); i++) + atc_update(atc, atc_dp_setup[i].offset, atc_dp_setup[i].mask, + atc_dp_setup[i].value); + for (u32 i = 0; i < 2; i++) { + atc_update(atc, 0x8684 + i * 0x9000, 3, r ? 0 : 1); + atc_update(atc, 0x4c80 + i * 0x9000, 0x30, r ? 0 : 0x10); + } + for (unsigned int i = 0; i < ARRAY_SIZE(tx_ctrl); i++) { + atc_update(atc, tx_ctrl[i], 0x7e07ff, 1); + atc_update(atc, tx_ctrl[i] - 0x14, 0x7f, r < 2 ? 0x65 : 0x40); + } + atc_update(atc, 0x2000, 0x70, 0x10); + atc_update(atc, 0x2000, BIT(13), BIT(13)); + atc_update(atc, 0x2044, 0xffff0000, wake_count[r] << 16); + atc_update(atc, atc->swap ? 0x89f0 : 0x119f0, BIT(14), BIT(14)); + atc_update(atc, 0x2000, BIT(2), BIT(2)); + atc_update(atc, 0x2000, BIT(3), BIT(3)); + atc_update(atc, 0x2040, BIT(16), BIT(16)); + + ret = atc_clock_write(atc, 10 - r); + if (ret) { + dev_err(dcp->dev, "ATC%u: DP clock change timeout\n", atc->index); + return ret; + } + ret = atc_route_start(dcp, atc); + if (ret) + return ret; + atc->link_rate = atc_rates[r]; + dev_info(dcp->dev, "ATC%u: four-lane DPTX setup at rate %#x\n", atc->index, + atc->link_rate); + return 0; +} + +/* + * The power dependencies, in order: the ATC's USB AON (PS +0x100), its common + * PSD, then its two DP PS registers (+0, +0x20), with the values they idle at + * (ATC3: PS 0x1c0, PSD 58, PS 0xc0, 0xe0). + */ +#define ATC_PSD_TARGET 0x90 +#define ATC_PSD_STATE 0x80 +static const u32 atc_ps_offset[] = { 0x100, 0, 0x0, 0x20 }; +static const u32 atc_ps_idle[] = { 0x300, 6, 0x244, 0x244 }; + +static int atc_psd_wait(struct dcpib_atc_port *atc, u32 state) +{ + u32 reg; + + /* + * Only the state: the acknowledge bit aura also waits for stays clear + * when Linux switches the PSD, in either direction. + */ + return readl_poll_timeout(atc->psd + ATC_PSD_STATE, reg, + (reg & 7) == state, 1, ATC_TIMEOUT_US); +} + +static int atc_gate(struct dcpib_atc_port *atc, unsigned int i, bool on) +{ + u32 value = on ? (atc_ps_idle[i] & 0xfffffcf0) | 15 : atc_ps_idle[i]; + u32 reg; + + /* Already up (the bootloader's or an earlier activation's). */ + if (on && (i == 1 ? !(readl(atc->psd + ATC_PSD_STATE) & 7) : + (readl(atc->ps + atc_ps_offset[i]) & 0xf0) == 0xf0)) + return 0; + if (on) + atc->owned |= BIT(i); + else + atc->owned &= ~BIT(i); + if (i == 1) { + writel(on ? 0 : atc_ps_idle[i], atc->psd + ATC_PSD_TARGET); + return atc_psd_wait(atc, on ? 0 : atc_ps_idle[i]); + } + writel(value, atc->ps + atc_ps_offset[i]); + return readl_poll_timeout(atc->ps + atc_ps_offset[i], reg, + (reg & 0xf0) == ((value & 15) << 4), 1, + ATC_TIMEOUT_US); +} + +static void atc_apply_tunables(struct dcpib_atc_port *atc, unsigned int which) +{ + const __be32 *t = atc->tunable[which]; + + for (int i = 0; i + 3 <= atc->tunable_len[which]; i += 3) + atc_update(atc, be32_to_cpu(t[i]), be32_to_cpu(t[i + 1]), + be32_to_cpu(t[i + 2])); +} + +/* + * Cold start: the power dependencies, the common block and AUX (aura + * atc_gates_enable and atc_phy_activate), then the DP lanes at HBR. + */ +static int atc_activate(struct dcpib *dcp) +{ + struct dcpib_atc_port *atc = atc_cur(dcp); + void __iomem *cfg = atc->base + ATC_CFG; + unsigned int gates = 0; + u32 reg; + + if (atc->active) + return 0; + if (!atc->tunable[0] || !atc->tunable[1]) + return -EOPNOTSUPP; + for (; gates < ARRAY_SIZE(atc_ps_idle); gates++) + if (atc_gate(atc, gates, true)) + goto fail; + if (readl(cfg) != 4 || (readl(cfg + 4) & 3) || readl(cfg + 0x30)) + goto fail; + writel(readl(cfg + 0x30) | BIT(0), cfg + 0x30); + udelay(1); + writel(readl(cfg) | BIT(0), cfg); + if (readl_poll_timeout(cfg + 4, reg, reg & BIT(0), 1, ATC_TIMEOUT_US)) + goto fail; + writel(readl(cfg) | BIT(1), cfg); + if (readl_poll_timeout(cfg + 4, reg, reg & BIT(1), 1, ATC_TIMEOUT_US)) + goto fail; + writel(readl(cfg) & ~BIT(2), cfg); + udelay(1); + writel(readl(cfg) | BIT(3), cfg); + + atc_apply_tunables(atc, 0); + /* aciophy_xbar_config(mode=3): both PMAs carry DisplayPort. */ + atc_update(atc, 0x4c, 0x1f, 0x14); + atc_update(atc, 0x4c, 0x3ffe0, atc->swap ? 0x100 : 0x22000); + atc_apply_tunables(atc, 1); + atc_update(atc, ATC_CFG + 8, BIT(1), atc->swap ? BIT(1) : 0); + writel(readl(cfg) | BIT(4), cfg); + if ((readl(cfg) & 0x1f) != 0x1b) + goto fail; + /* aciophy_aux_init: release small/big sleep, then the AUX clamp. */ + atc_update(atc, ATC_AON + 0x24, BIT(8), BIT(8)); + udelay(1); + atc_update(atc, ATC_AON + 0x24, BIT(6), BIT(6)); + udelay(1); + atc_update(atc, ATC_AON + 0x24, BIT(10), 0); + udelay(1); + atc->active = true; + dev_info(dcp->dev, "ATC%u: common PHY and AUX up from cold\n", atc->index); + /* + * As aura's activation: the lanes start at HBR right away. From cold, + * AUX only works with the DP PLL and PMAs running (without them the + * firmware reports DEVICE_NOT_RESPONDING); the firmware switches to + * its rate afterwards. + */ + return atc_dp_start(dcp, atc, 1); +fail: + dev_err(dcp->dev, "ATC%u: cold start failed (gates %u, cfg %#x/%#x)\n", atc->index, + gates, gates == ARRAY_SIZE(atc_ps_idle) ? readl(cfg) : 0, + gates == ARRAY_SIZE(atc_ps_idle) ? readl(cfg + 4) : 0); + return -EIO; +} + +/* Power the PHY down (aura atc_phy_deactivate), dependencies included. */ +static int atc_deactivate(struct dcpib *dcp) +{ + struct dcpib_atc_port *atc = atc_cur(dcp); + void __iomem *cfg = atc->base + ATC_CFG; + int ret = 0; + u32 reg; + + if (!atc->active) + return 0; + if (atc->link_rate && atc_dp_stop(dcp, atc)) { + dev_err(dcp->dev, "ATC%u: DP stop failed\n", atc->index); + return -EIO; + } + atc_route_stop(dcp, atc); + atc_update(atc, ATC_AON + 0x24, BIT(10), BIT(10)); + udelay(1); + atc_update(atc, ATC_AON + 0x24, BIT(6), 0); + udelay(1); + atc_update(atc, ATC_AON + 0x24, BIT(8), 0); + udelay(1); + writel(readl(cfg) & ~BIT(4), cfg); + udelay(1); + writel(readl(cfg) & ~BIT(3), cfg); + writel(readl(cfg) | BIT(2), cfg); + writel(readl(cfg) & ~BIT(1), cfg); + if (readl_poll_timeout(cfg + 4, reg, !(reg & BIT(1)), 1, ATC_TIMEOUT_US)) { + dev_err(dcp->dev, "ATC%u: big-sleep entry timeout\n", atc->index); + ret = -ETIMEDOUT; + } + writel(readl(cfg) & ~BIT(0), cfg); + if (readl_poll_timeout(cfg + 4, reg, !(reg & BIT(0)), 1, ATC_TIMEOUT_US)) { + dev_err(dcp->dev, "ATC%u: small-sleep entry timeout\n", atc->index); + ret = -ETIMEDOUT; + } + writel(readl(cfg + 0x30) & ~BIT(0), cfg + 0x30); + atc->active = false; + /* + * Then the power dependencies it powered up: the next activation starts + * it cold. The USB controller of a port keeps the ones it uses. + */ + for (int i = ARRAY_SIZE(atc_ps_idle) - 1; i >= 0; i--) + if ((atc->owned & BIT(i)) && atc_gate(atc, i, false)) + ret = -ETIMEDOUT; + dev_info(dcp->dev, "ATC%u: powered down: %d\n", atc->index, ret); + return ret; +} + +static int atc_set_link_rate(struct dcpib *dcp, u32 rate) +{ + struct dcpib_atc_port *atc = atc_cur(dcp); + + u32 r; + int ret; + + /* The firmware drops the link (rate 0) before it retrains. */ + if (!rate || rate == atc->link_rate) + return 0; + for (r = 0; r < ARRAY_SIZE(atc_rates); r++) + if (atc_rates[r] == rate) + break; + if (r == ARRAY_SIZE(atc_rates) || !atc->active) + return -EINVAL; + dev_info(dcp->dev, "ATC%u: link rate %#x, was %#x\n", atc->index, rate, atc->link_rate); + if (atc->link_rate) { + ret = atc_dp_stop(dcp, atc); + if (ret) + return ret; + } + return atc_dp_start(dcp, atc, r); +} + +static int atc_set_route(struct dcpib *dcp, bool enable) +{ + struct dcpib_atc_port *atc = atc_cur(dcp); + + if (!enable) + return atc_route_stop(dcp, atc); + if (!atc->active) + return -ENODEV; + if (atc_route_start(dcp, atc)) + return -EIO; + mdelay(5); + return 0; +} + +/* + * Matching T8152 fallback table, indexed by voltage * 4 + pre-emphasis; the + * ADT has no rate-specific dp-training-table, so it serves every rate. + */ +static const u32 atc_drive_presets[16] = { + 0x025, 0x420, 0x717, 0x90f, 0x019, 0x397, 0x78f, 0x025, + 0x011, 0x38f, 0x025, 0x025, 0x000, 0x025, 0x025, 0x025, +}; + +static int atc_set_drive(struct dcpib *dcp, + const struct dcpib_drive_settings *settings, u32 count) +{ + struct dcpib_atc_port *atc = atc_cur(dcp); + u32 presets[4]; + + if (!count) + return 0; + if (count > 4 || !atc->active || !atc->link_rate) + return -EINVAL; + /* Validate the entire request before changing any lane. */ + for (u32 i = 0; i < count; i++) { + u32 v = le32_to_cpu(settings[i].voltage); + u32 e = le32_to_cpu(settings[i].pre_emphasis); + + if (settings[i].format || v > 3 || e > 3 || v + e > 3) + return -EINVAL; + presets[i] = atc_drive_presets[v * 4 + e]; + } + for (u32 i = 0; i < count; i++) { + u32 pma = (i / 2) ^ !atc->swap; + /* Even DP lanes use the PMA RX pair, odd lanes its TX pair. */ + u32 offset = ((i & 1) ? 0x4eb0 : 0x8b10) + pma * 0x9000; + u32 preset = presets[i]; + u32 control = 1 | (((preset >> 13) & 0x3f) << 17) | + (((preset >> 7) & 0x3f) << 5); + + atc_update(atc, offset, 0x7e07ff, control); + atc_update(atc, offset - 0x14, 0x7f, (preset & 0x3f) | 0x40); + } + return 0; +} + +static void atc_set_orientation(struct dcpib *dcp, bool flipped) +{ + struct dcpib_atc_port *atc = atc_cur(dcp); + + /* Applied by the next cold start; a running PHY keeps its own. */ + if (atc->active && atc->swap != flipped) + dev_warn(dcp->dev, "ATC%u: orientation changed while active\n", atc->index); + atc->swap = flipped; +} + +/* + * The sink's EDID through the AP AUX controller aux0, which reaches the ATC + * through crossbar UFP aux0 like the DCP: the DCP's route comes down for + * the read, so this runs while the firmware does no AUX itself (display + * off, HPD not asserted yet). + */ +static int atc_read_edid(struct dcpib *dcp, u8 *buf, size_t size) +{ + struct dcpib_atc_port *atc = atc_cur(dcp); + bool was_started = atc->route_started; + int ret; + + if (!atc->active || !atc->aux->regs) + return -ENODEV; + ret = dcpib_aux_init(dcp, atc->aux); + if (ret) + return ret; + atc_route_stop(dcp, atc); + ret = atc_route_start_sel(dcp, atc, ATC_SEL_AUX0); + if (!ret) + ret = dcpib_aux_read_edid(dcp, atc->aux, buf, size); + atc_route_stop(dcp, atc); + if (was_started) + atc_route_start(dcp, atc); + return ret; +} + +/* + * 1 with a sink on one of the ports: the current one first, then the + * others; select() switches to a port found that way. + */ +static int atc_detect(struct dcpib *dcp, bool *flipped) +{ + struct dcpib_atc *atc = dcp->phy_priv; + int ret = dcpib_pd_sink(atc->cur->pd, flipped); + + atc->next = atc->cur; + if (ret) + return ret; + for (unsigned int i = 0; i < atc->nports; i++) { + struct dcpib_atc_port *port = &atc->port[i]; + + if (port == atc->cur || dcpib_pd_sink(port->pd, flipped) <= 0) + continue; + atc->next = port; + /* A plug the other port's HPD state does not show yet. */ + dcp->hpd = 0; + return 1; + } + return 0; +} + +/* + * Drive the port a sink was found on: power the old one down and connect + * the DPTX port to the new one's display crossbar endpoint. + */ +static void atc_select(struct dcpib *dcp) +{ + struct dcpib_atc *atc = dcp->phy_priv; + struct dcpib_atc_port *port = atc->next; + + if (!port || port == atc->cur) + return; + if (atc->cur->active) + atc_deactivate(dcp); + dev_info(dcp->dev, "USB-C display on ATC%u now, was ATC%u\n", + port->index, atc->cur->index); + atc->cur = port; + dcp->pd = port->pd; + dcp->dcp_output = port->dfp; + if (dcpib_dptx_connect(dcp, 0)) + dev_err(dcp->dev, "DPTX connect to ATC%u failed\n", port->index); +} + +static const struct dcpib_phy_ops atc_ops = { + .activate = atc_activate, + .deactivate = atc_deactivate, + .set_link_rate = atc_set_link_rate, + .set_drive = atc_set_drive, + .set_route = atc_set_route, + .set_orientation = atc_set_orientation, + .read_edid = atc_read_edid, + .detect = atc_detect, + .select = atc_select, + .max_link_rate = DCPIB_LINK_RATE_HBR2, + /* + * Modes up to 1920x1080@60 and 1600x1200@60 (and 2560x1440@30) show, + * 1920x1200@60 and anything faster stay black at HBR2 with no error: + * the DCP's pixel clock on the ATC path tops out between 162 and + * about 190 MHz with the firmware on its iBoot interface. + */ + .max_pixel_rate = 130000000, + /* + * 720x480@60 and 720x576@50 (27 MHz) show, 640x480@60 (25.2 MHz) + * stays black here while HDMI shows it. + */ + .min_pixel_rate = 20000000, +}; + +/* PMGR registers, inside blocks their power domain driver has claimed. */ +static void __iomem *atc_map(struct dcpib *dcp, struct device_node *np, + const char *name) +{ + int i = of_property_match_string(np, "reg-names", name); + struct resource res; + + if (i < 0 || of_address_to_resource(np, i, &res)) + return NULL; + return devm_ioremap(dcp->dev, res.start, resource_size(&res)); +} + +static int atc_port_init(struct dcpib *dcp, struct dcpib_atc *atc, + struct device_node *np, void __iomem *dp_clock) +{ + struct dcpib_atc_port *port = &atc->port[atc->nports]; + u32 cfg, r; + + port->base = atc_map(dcp, np, "atc"); + port->ps = atc_map(dcp, np, "atc-ps"); + port->psd = atc_map(dcp, np, "atc-psd"); + if (!port->base || !port->ps || !port->psd) + return -ENOMEM; + port->pd = dcpib_pd_get(dcp, np); + if (IS_ERR_OR_NULL(port->pd)) + return port->pd ? PTR_ERR(port->pd) : -ENODEV; + /* The node without ports is ATC3's (J873g back-right). */ + port->dfp = 3; + of_property_read_u32(np, "apple,dfp-endpoint", &port->dfp); + port->index = port->dfp; + port->dp_clock = dp_clock; + port->aux = &atc->aux; + port->tunable[0] = of_get_property(np, "apple,tunable-pre", + &port->tunable_len[0]); + port->tunable[1] = of_get_property(np, "apple,tunable-post", + &port->tunable_len[1]); + port->tunable_len[0] /= sizeof(__be32); + port->tunable_len[1] /= sizeof(__be32); + atc->nports++; + + /* + * The PHY block hangs the bus while it is power-gated: look at it only + * with its DP dependencies and the common PSD up. + */ + if ((readl(port->ps) & 0xf0) != 0xf0 || + (readl(port->psd + ATC_PSD_STATE) & 7)) { + dev_info(dcp->dev, "ATC%u: powered down\n", port->index); + return 0; + } + /* Common block out of reset and AUX up, as aura's activation leaves it. */ + cfg = readl(port->base + ATC_CFG); + port->active = (cfg & 0x1f) == 0x1b; + port->swap = (readl(port->base + 0x4c) & 0x3ffe0) == 0x100; + port->route_started = (readl(port->base + ATC_XBAR) & 1) && + (readl(port->base + ATC_XBAR + 0x3c) & 1); + r = (readl(port->base + 0x2000) >> 19) & 7; + if (port->active && (readl(port->base + 0x2040) & BIT(16)) && + r < ARRAY_SIZE(atc_rates)) + port->link_rate = atc_rates[r]; + /* The bootloader's display: its dependencies are the display's. */ + if (port->active) + port->owned = GENMASK(ARRAY_SIZE(atc_ps_idle) - 1, 0); + dev_info(dcp->dev, + "ATC%u: inherited state: active %d, rate %#x, route %d, reversed %d (cfg %#x)\n", + port->index, port->active, port->link_rate, port->route_started, + port->swap, cfg); + return 0; +} + +int dcpib_atc_init(struct dcpib *dcp) +{ + struct device_node *np = dcp->dev->of_node; + void __iomem *dp_clock; + struct dcpib_atc *atc; + bool flipped; + int ret; + + atc = devm_kzalloc(dcp->dev, sizeof(*atc), GFP_KERNEL); + if (!atc) + return -ENOMEM; + dp_clock = atc_map(dcp, np, "dp-clock"); + if (!dp_clock) + return -ENOMEM; + /* AUX output select 9: the crossbar's UFP aux0 */ + dcpib_aux_probe(dcp, &atc->aux, 9); + + /* One child per USB-C port, or the node itself for ATC3 alone. */ + for_each_child_of_node_scoped(np, child) { + if (!of_property_present(child, "apple,dfp-endpoint")) + continue; + if (atc->nports == ARRAY_SIZE(atc->port)) + break; + ret = atc_port_init(dcp, atc, child, dp_clock); + if (ret) + return ret; + } + if (!atc->nports) { + ret = atc_port_init(dcp, atc, np, dp_clock); + if (ret) + return ret; + } + + /* The bootloader's display port, else one with a sink, else the first. */ + atc->cur = &atc->port[0]; + for (unsigned int i = 0; i < atc->nports; i++) { + if (atc->port[i].active) { + atc->cur = &atc->port[i]; + goto found; + } + } + for (unsigned int i = 0; i < atc->nports; i++) { + if (dcpib_pd_sink(atc->port[i].pd, &flipped) > 0) { + atc->cur = &atc->port[i]; + break; + } + } +found: + atc->next = atc->cur; + dcp->phy = &atc_ops; + dcp->phy_priv = atc; + dcp->pd = atc->cur->pd; + dcp->dcp_output = atc->cur->dfp; + dev_info(dcp->dev, "USB-C display on ATC%u (%u ports)\n", atc->cur->index, + atc->nports); + return 0; +} diff --git a/drivers/gpu/drm/apple_dcpib/aux.c b/drivers/gpu/drm/apple_dcpib/aux.c new file mode 100644 index 0000000000000..c8c6bf9ce3137 --- /dev/null +++ b/drivers/gpu/drm/apple_dcpib/aux.c @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +/* + * The AP's DisplayPort AUX controllers aux0/aux1 (ADT dptx0-aux, dptx1-aux; + * macOS AppleAUXDPTX): Analogix-derived DPTX cores with Apple's request + * framing (AppleDPTXController::setupAuxTransaction, readDPCDGated). They + * attach to the display crossbar as UFP endpoints 5 and 6; the phy code + * routes one to its port for the time of a read. Used for the sink's EDID, + * which the DCP firmware does not hand out on its iBoot interface. + */ + +#include +#include +#include +#include +#include +#include + +#include + +#include "dcpib.h" + +#define AUX_TIMEOUT_US 10000 +#define AUX_SEL 0x000 /* output: 9 crossbar UFP aux0, 0x10 lpdptx phy */ +#define AUX_FUNC_EN_2 0x01c +#define AUX_FUNC_EN_N BIT(2) +#define AUX_HW_RETRY 0x390 +#define AUX_INT_STA 0x3dc +#define AUX_REPLY BIT(1) +#define AUX_ERROR BIT(0) +#define AUX_CH_STA 0x780 +#define AUX_DEFER_CTL 0x788 +#define AUX_RX_COMM 0x78c +#define AUX_BUF_CTL 0x790 +#define AUX_BUF_CLR BIT(9) +#define AUX_BUF_COUNT GENMASK(8, 0) +#define AUX_LEN 0x794 +#define AUX_ADDR_7_0 0x798 +#define AUX_ADDR_15_8 0x79c +#define AUX_CMD_ADDR 0x7a0 /* request in [7:4], address [19:16] */ +#define AUX_CTL 0x7a4 +#define AUX_GO BIT(0) +#define AUX_ADDR_ONLY BIT(1) +#define AUX_DATA 0xc00 /* one byte per word */ +#define AUX_MAX 16 + +#define AUX_NATIVE_WRITE 0x80 +#define AUX_I2C_WRITE 0x00 +#define AUX_I2C_READ 0x10 +#define AUX_I2C_MOT 0x40 +#define DDC_ADDR 0x50 +#define DPCD_SET_POWER 0x600 +#define DP_SET_POWER_D0 1 + +static void aux_update(void __iomem *reg, u32 mask, u32 value) +{ + writel((readl(reg) & ~mask) | value, reg); +} + +int dcpib_aux_init(struct dcpib *dcp, struct dcpib_aux *aux) +{ + u32 reg; + + /* PMGR GLOBAL_AUX_CTRLn */ + aux_update(aux->ps, 0xf, 0xf); + if (readl_poll_timeout(aux->ps, reg, ((reg >> 4) & 0xf) == 0xf, 1, + AUX_TIMEOUT_US)) { + dev_err(dcp->dev, "AUX: controller does not power up\n"); + return -ETIMEDOUT; + } + writel(AUX_REPLY | AUX_ERROR, aux->regs + AUX_INT_STA); + aux_update(aux->regs + AUX_FUNC_EN_2, AUX_FUNC_EN_N, AUX_FUNC_EN_N); + udelay(10); + aux_update(aux->regs + AUX_FUNC_EN_2, AUX_FUNC_EN_N, 0); + writel(3 << 8, aux->regs + AUX_HW_RETRY); + writel(BIT(7) | 1, aux->regs + AUX_DEFER_CTL); + aux_update(aux->regs + AUX_SEL, 0x3f, aux->sel); + return 0; +} + +/* One AUX request of up to 16 bytes; returns the bytes transferred. */ +static int aux_xfer(struct dcpib_aux *aux, u32 cmd, u32 addr, u8 *buf, + u32 len, bool write) +{ + u32 sta, n; + int i; + + for (int tries = 0; tries < 7; tries++) { + writel(AUX_BUF_CLR, aux->regs + AUX_BUF_CTL); + writel(len ? len - 1 : 0, aux->regs + AUX_LEN); + writel(addr & 0xff, aux->regs + AUX_ADDR_7_0); + writel((addr >> 8) & 0xff, aux->regs + AUX_ADDR_15_8); + writel((cmd & 0xf0) | ((addr >> 16) & 0xf), aux->regs + AUX_CMD_ADDR); + aux_update(aux->regs + AUX_CTL, AUX_ADDR_ONLY, len ? 0 : AUX_ADDR_ONLY); + for (i = 0; write && i < len; i++) + writel(buf[i], aux->regs + AUX_DATA + 4 * i); + writel(AUX_REPLY | AUX_ERROR, aux->regs + AUX_INT_STA); + aux_update(aux->regs + AUX_CTL, AUX_GO, AUX_GO); + if (readl_poll_timeout(aux->regs + AUX_INT_STA, sta, + sta & (AUX_REPLY | AUX_ERROR), 10, 20000)) + return -ETIMEDOUT; + writel(AUX_REPLY | AUX_ERROR, aux->regs + AUX_INT_STA); + if ((sta & AUX_ERROR) || (readl(aux->regs + AUX_CH_STA) & 0xf)) + return -EIO; + /* [3:2] I2C and [1:0] AUX reply: 0 ACK, 1 NACK, 2 DEFER */ + sta = readl(aux->regs + AUX_RX_COMM); + if (sta & 0x5) + return -ENXIO; + if (sta & 0xa) { + usleep_range(500, 1000); + continue; + } + if (write) + return len; + n = min(readl(aux->regs + AUX_BUF_CTL) & AUX_BUF_COUNT, len); + for (i = 0; i < n; i++) + buf[i] = readl(aux->regs + AUX_DATA + 4 * i); + return n; + } + return -EBUSY; +} + +/* DDC over I2C-over-AUX: offset 0, then 16-byte reads up to @size. */ +static int aux_ddc_read(struct dcpib_aux *aux, u8 *buf, size_t size) +{ + u8 offset = 0; + size_t done = 0; + int ret; + + ret = aux_xfer(aux, AUX_I2C_WRITE | AUX_I2C_MOT, DDC_ADDR, + &offset, 1, true); + while (ret >= 0 && done < size) { + ret = aux_xfer(aux, AUX_I2C_READ | AUX_I2C_MOT, DDC_ADDR, + buf + done, min_t(size_t, size - done, AUX_MAX), + false); + if (ret > 0) + done += ret; + else if (!ret) + ret = -EIO; + } + /* End the I2C transaction with an address-only read without MOT. */ + aux_xfer(aux, AUX_I2C_READ, DDC_ADDR, NULL, 0, false); + return ret < 0 ? ret : done; +} + +/* + * The EDID over DDC, the base block and its extensions; the sink answers + * AUX only some time after the path is up, and a sink just plugged in a + * little later still. + */ +int dcpib_aux_read_edid(struct dcpib *dcp, struct dcpib_aux *aux, u8 *buf, + size_t size) +{ + int ret = -EIO, ext; + + u8 d0 = DP_SET_POWER_D0; + + for (int i = 0; i < 30 && (ret == -EIO || ret == -ETIMEDOUT); i++) { + msleep(i ? 100 : 20); + /* + * A sink left in D3 (after an unplug) NACKs DDC: wake it + * first, as the DP spec asks before AUX use. + */ + ret = aux_xfer(aux, AUX_NATIVE_WRITE, DPCD_SET_POWER, &d0, 1, true); + if (ret < 0) + continue; + usleep_range(1000, 2000); + ret = aux_ddc_read(aux, buf, min_t(size_t, size, EDID_LENGTH)); + } + if (ret != EDID_LENGTH) + return ret < 0 ? ret : -EIO; + ext = min_t(size_t, buf[0x7e], size / EDID_LENGTH - 1); + return ext ? aux_ddc_read(aux, buf, (1 + ext) * EDID_LENGTH) : ret; +} + +/* The optional "aux" and "aux-ps" (PMGR, mapped without claiming) regs. */ +/* The controller named by np's "aux" and "aux-ps" regs (np need not be dcp's). */ +void dcpib_aux_probe_node(struct dcpib *dcp, struct dcpib_aux *aux, u32 sel, + struct device_node *np) +{ + int ps = of_property_match_string(np, "reg-names", "aux-ps"); + int regs = of_property_match_string(np, "reg-names", "aux"); + struct resource res; + + aux->sel = sel; + if (ps < 0 || regs < 0 || of_address_to_resource(np, ps, &res)) + return; + aux->ps = devm_ioremap(dcp->dev, res.start, resource_size(&res)); + aux->regs = devm_of_iomap(dcp->dev, np, regs, NULL); + if (IS_ERR(aux->regs) || !aux->ps) + aux->regs = NULL; +} + +void dcpib_aux_probe(struct dcpib *dcp, struct dcpib_aux *aux, u32 sel) +{ + dcpib_aux_probe_node(dcp, aux, sel, dcp->dev->of_node); +} diff --git a/drivers/gpu/drm/apple_dcpib/dcpib.h b/drivers/gpu/drm/apple_dcpib/dcpib.h new file mode 100644 index 0000000000000..b7449e7a3b2f0 --- /dev/null +++ b/drivers/gpu/drm/apple_dcpib/dcpib.h @@ -0,0 +1,334 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ +/* + * Apple DCP display driver on the iBoot interface (T8152). + * + * The T8152 DCP firmware (AppleDCP-1317) offers a small "disp0-service", the + * interface iBoot and aura use, next to the full IOMobileFramebuffer protocol. + * This driver uses only that service, the DPTX port service the firmware + * calls back into for the PHY and the DPAV controller services it expects to + * be opened, all over AFKEPV2. It is a port of aura's src/afk.c, + * src/dcp_iboot.c, src/dcp/dptx_port_ep.c and src/dcp/atc_phy.c. + */ +#ifndef __APPLE_DCPIB_H__ +#define __APPLE_DCPIB_H__ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +struct dcpib; +struct dcpib_ep; +struct dcpib_service; +struct spmi_device; +struct drm_edid; + +#define DCPIB_EP_IBOOT 0x23 +#define DCPIB_EP_DPAV 0x24 +#define DCPIB_EP_DPTX 0x2a +#define DCPIB_FIRST_EP 0x20 +#define DCPIB_NR_EPS 0x10 + +#define DCPIB_MAX_SERVICES 8 + +/* IOReturn values the firmware understands. */ +#define KIO_RETURN_SUCCESS 0 +#define KIO_RETURN_ERROR 0xe00002bcU +#define KIO_RETURN_BAD_ARG 0xe00002c2U +#define KIO_RETURN_UNSUPPORTED 0xe00002c7U +#define KIO_RETURN_NOT_READY 0xe00002d8U + +/* DPCD link rates in units of 0.27 Gbps */ +#define DCPIB_LINK_RATE_RBR 0x06 +#define DCPIB_LINK_RATE_HBR 0x0a +#define DCPIB_LINK_RATE_HBR2 0x14 +#define DCPIB_LINK_RATE_HBR3 0x1e + +struct dcpib_dma { + void *cpu; + dma_addr_t iova; + size_t size; +}; + +struct dcpib_service_ops { + /* Firmware-initiated standard service call; returns an IOReturn. */ + u32 (*call)(struct dcpib_service *service, u16 group, u32 command, + const void *data, size_t data_size, void *reply, + size_t reply_size); +}; + +struct dcpib_service { + struct dcpib_ep *ep; + const struct dcpib_service_ops *ops; + char name[32]; + u16 interface; + unsigned int unit; + void *cookie; +}; + +struct dcpib_ring { + bool ready; + u32 *rptr; + u32 *wptr; + void *buf; + u32 bufsz; + u32 block; +}; + +struct dcpib_ep { + struct dcpib *dcp; + u8 ep; + const struct dcpib_service_ops *ops; + + struct dcpib_dma ring; + u16 tag; + struct dcpib_ring tx, rx; + struct dcpib_dma txbuf, rxbuf; + + struct completion started; + struct completion stopped; + struct work_struct rx_work; + + /* Serialises TX ring writes and the sequence number. */ + struct mutex tx_lock; + u8 seq; + + struct dcpib_service services[DCPIB_MAX_SERVICES]; + unsigned int nr_services; + unsigned int expected; + struct completion services_done; + + /* One outstanding AP command at a time. */ + struct mutex cmd_lock; + bool cmd_waiting; + u16 cmd_interface; + u8 cmd_type; + u8 cmd_id; + u8 next_cmd_id; + void *cmd_out; + size_t cmd_out_len; + int cmd_ret; + struct completion cmd_done; +}; + +struct dcpib_timing_mode { + __le32 valid; + __le32 width; + __le32 height; + __le32 fps; /* 16.16 */ + u8 pad[8]; +} __packed; + +struct dcpib_color_mode { + __le32 valid; + __le32 colorimetry; + __le32 eotf; + __le32 encoding; + __le32 bpp; + u8 pad[4]; +} __packed; + +struct dcpib_drive_settings { + __le32 format; + __le32 voltage; + __le32 pre_emphasis; +} __packed; + +/* The phy behind the DCP's DPTX port. */ +struct dcpib_phy_ops { + int (*activate)(struct dcpib *dcp); + int (*deactivate)(struct dcpib *dcp); + int (*set_link_rate)(struct dcpib *dcp, u32 rate); /* DPCD units */ + int (*set_lane_count)(struct dcpib *dcp, u32 lanes); /* optional */ + int (*set_drive)(struct dcpib *dcp, + const struct dcpib_drive_settings *settings, u32 count); + int (*set_route)(struct dcpib *dcp, bool enable); + /* optional: plug orientation for the next activation */ + void (*set_orientation)(struct dcpib *dcp, bool flipped); + /* optional: the sink's EDID, returns its length */ + int (*read_edid)(struct dcpib *dcp, u8 *buf, size_t size); + /* + * optional: 1 with a sink on one of the phy's ports, 0 without; the + * port it found is taken by select() in the plug path + */ + int (*detect)(struct dcpib *dcp, bool *flipped); + void (*select)(struct dcpib *dcp); + u32 max_link_rate; /* DPCD units */ + /* active pixels per second the path scans out, 0 if not limited */ + u64 min_pixel_rate, max_pixel_rate; +}; + +struct dcpib_port { + struct dcpib_service *service; + u32 link_rate; /* DPCD units */ + u32 lanes; + /* lanes of the last training, kept while the link is down */ + u32 trained_lanes; + /* last drive settings the firmware set, for GET_DRIVE_SETTINGS */ + struct dcpib_drive_settings drive[4]; + u32 drive_count; +}; + +struct dcpib { + struct device *dev; + struct drm_device drm; + struct apple_rtkit *rtk; + struct workqueue_struct *wq; + + struct dcpib_ep *eps[DCPIB_NR_EPS]; + struct dcpib_ep *iboot_ep; + struct dcpib_ep *dpav_ep; + /* the sink's EDID, read by the phy (NULL if unavailable) */ + const struct drm_edid *edid; + struct dcpib_ep *dptx_ep; + struct dcpib_service *iboot; + struct dcpib_port port[2]; + + /* The scanout DART's own device and domain (see dcpib_scanout_init). */ + struct platform_device *scanout; + struct iommu_domain *scanout_dom; + + /* + * Scanout buffers (dcpib_boot_surface), each for the largest mode: a + * frame is copied into one not on screen, which SET_SURFACE then shows. + * Copying into the buffer being scanned out tore every frame. Three, so + * the one written next left the screen a frame ago. + */ +#define DCPIB_SCANOUT_BUFS 3 + struct dcpib_dma scan[DCPIB_SCANOUT_BUFS]; + /* their pages write-combined: the pipe does not snoop the CPU caches */ + void *scan_wc[DCPIB_SCANOUT_BUFS]; + /* their addresses in the display pipe's DART (dcpib_scan_free_dva) */ + dma_addr_t scan_dva[DCPIB_SCANOUT_BUFS]; + /* scan[front] is on screen (or set as the surface next power on) */ + unsigned int front; + /* the part of the current picture scan[i] does not hold yet */ + struct drm_rect stale[DCPIB_SCANOUT_BUFS]; + /* when scan[i] stopped being the requested surface */ + ktime_t retired[DCPIB_SCANOUT_BUFS]; + /* the last SET_SURFACE of a new buffer */ + ktime_t last_swap; + /* + * A frame written within one refresh of the last swap waits in + * scan[pending] (-1: none) for swap_timer, at most one swap per + * refresh; a newer frame overwrites it. + */ + int pending; + u32 pending_w, pending_h, pending_pitch; + struct hrtimer swap_timer; + struct work_struct swap_work; + u32 boot_w, boot_h, boot_stride; + + const struct dcpib_phy_ops *phy; + void *phy_priv; + /* display-crossbar DFP endpoint the DCP drives */ + u32 dcp_output; + /* optional sink HPD line (J873g HDMI), polled; last level seen */ + struct gpio_desc *hpd_gpio; + int hpd; + /* USB-C: the PD controller of the port in use, polled the same way */ + struct spmi_device *pd; + /* HDMI phy: DCPEXT0, or the main DCP on the bootloader's HDMI-only route */ + bool hdmi; + u32 die; + + /* iBoot state below, under lock */ + struct mutex lock; + bool powered; + struct dcpib_timing_mode *tmodes; + unsigned int nr_tmodes; + struct dcpib_color_mode *cmodes; + unsigned int nr_cmodes; + struct dcpib_timing_mode cur_mode; + bool mode_set; + /* SET_MODE of cur_mode failed; the next plug sets it again */ + bool mode_failed; + /* USB-C: re-enter DP alt mode when the sink stopped answering */ + struct work_struct pd_reset_work; + unsigned long pd_reset_time; + + struct drm_plane plane; + struct drm_crtc crtc; + struct drm_encoder encoder; + struct drm_connector connector; +}; + +static inline struct dcpib *to_dcpib(struct drm_device *drm) +{ + return container_of(drm, struct dcpib, drm); +} + +/* afk.c */ +struct dcpib_ep *dcpib_ep_start(struct dcpib *dcp, u8 ep, + const struct dcpib_service_ops *ops, + unsigned int expected); +void dcpib_ep_stop(struct dcpib_ep *ep); +void dcpib_ep_recv(struct dcpib_ep *ep, u64 message); +int dcpib_command(struct dcpib_service *service, u8 type, const void *tx, + size_t txlen, void *rx, size_t *rxlen); +int dcpib_service_call(struct dcpib_service *service, u16 group, u32 command, + const void *data, size_t data_len, size_t data_pad, + void *output, size_t output_len, size_t output_pad); +int dcpib_dma_alloc(struct dcpib *dcp, struct dcpib_dma *dma, size_t size); +void dcpib_dma_free(struct dcpib *dcp, struct dcpib_dma *dma); + +/* iboot.c */ +int dcpib_iboot_init(struct dcpib *dcp); +int dcpib_ib_set_power(struct dcpib *dcp, bool on); +int dcpib_ib_get_hpd(struct dcpib *dcp, u32 *timings, u32 *colors); +int dcpib_ib_get_modes(struct dcpib *dcp); +int dcpib_ib_set_mode(struct dcpib *dcp, const struct dcpib_timing_mode *t, + const struct dcpib_color_mode *c); +int dcpib_ib_set_surface(struct dcpib *dcp, u64 iova, u32 width, u32 height, + u32 stride); + +/* drv.c */ +int dcpib_wait_hpd(struct dcpib *dcp); + +/* dptx.c */ +int dcpib_dptx_init(struct dcpib *dcp); +int dcpib_dpav_init(struct dcpib *dcp); +int dcpib_dptx_connect(struct dcpib *dcp, unsigned int port); +int dcpib_dptx_set_hpd(struct dcpib *dcp, unsigned int port, bool hpd); + +/* aux.c */ +struct dcpib_aux { + void __iomem *regs; + void __iomem *ps; + u32 sel; /* output select of the core */ +}; + +void dcpib_aux_probe(struct dcpib *dcp, struct dcpib_aux *aux, u32 sel); +void dcpib_aux_probe_node(struct dcpib *dcp, struct dcpib_aux *aux, u32 sel, + struct device_node *np); +int dcpib_aux_init(struct dcpib *dcp, struct dcpib_aux *aux); +int dcpib_aux_read_edid(struct dcpib *dcp, struct dcpib_aux *aux, u8 *buf, + size_t size); + +/* atc.c, hdmi.c */ +int dcpib_atc_init(struct dcpib *dcp); +struct device_node *dcpib_hdmi_node(void); +int dcpib_hdmi_source(struct device_node *np); +int dcpib_hdmi_init(struct dcpib *dcp, struct device_node *np); + +/* pd.c */ +struct spmi_device *dcpib_pd_get(struct dcpib *dcp, struct device_node *np); +int dcpib_pd_sink(struct spmi_device *pd, bool *flipped); +int dcpib_pd_reconnect(struct dcpib *dcp); + +/* kms.c */ +int dcpib_kms_init(struct dcpib *dcp); +int dcpib_set_boot_mode(struct dcpib *dcp); +void dcpib_update_edid(struct dcpib *dcp); + +#endif diff --git a/drivers/gpu/drm/apple_dcpib/dptx.c b/drivers/gpu/drm/apple_dcpib/dptx.c new file mode 100644 index 0000000000000..55752bd74e056 --- /dev/null +++ b/drivers/gpu/drm/apple_dcpib/dptx.c @@ -0,0 +1,324 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +/* + * DCP DPTX port service and DPAV controllers (port of aura + * src/dcp/dptx_port_ep.c and src/dcp/dpav_ep.c, AFKEPV2 form only). + */ + +#include +#include +#include + +#include "dcpib.h" + +enum dptx_apcall { + DPTX_APCALL_ACTIVATE = 0, + DPTX_APCALL_DEACTIVATE = 1, + DPTX_APCALL_GET_MAX_DRIVE_SETTINGS = 2, + DPTX_APCALL_SET_DRIVE_SETTINGS = 3, + DPTX_APCALL_GET_DRIVE_SETTINGS = 4, + DPTX_APCALL_WILL_CHANGE_LINK_CONFIG = 5, + DPTX_APCALL_DID_CHANGE_LINK_CONFIG = 6, + DPTX_APCALL_GET_MAX_LINK_RATE = 7, + DPTX_APCALL_GET_LINK_RATE = 8, + DPTX_APCALL_SET_LINK_RATE = 9, + DPTX_APCALL_GET_MAX_LANE_COUNT = 10, + DPTX_APCALL_GET_ACTIVE_LANE_COUNT = 11, + DPTX_APCALL_SET_ACTIVE_LANE_COUNT = 12, + DPTX_APCALL_GET_SUPPORTS_DOWN_SPREAD = 13, + DPTX_APCALL_GET_DOWN_SPREAD = 14, + DPTX_APCALL_SET_DOWN_SPREAD = 15, + DPTX_APCALL_GET_SUPPORTS_LANE_MAPPING = 16, + DPTX_APCALL_SET_LANE_MAP = 17, + DPTX_APCALL_GET_SUPPORTS_HPD = 18, + DPTX_APCALL_FORCE_HOTPLUG_DETECT = 19, + DPTX_APCALL_INACTIVE_SINK_DETECTED = 20, + DPTX_APCALL_SET_TILED_DISPLAY_HINTS = 21, + DPTX_APCALL_DEVICE_NOT_RESPONDING = 22, + DPTX_APCALL_DEVICE_BUSY_TIMEOUT = 23, + DPTX_APCALL_DEVICE_NOT_STARTED = 24, + DPTX_APCALL_GET_VIRTUAL_DEVICE_MODE = 26, +}; + +#define DPTX_REMOTE_PORT_CORE GENMASK(3, 0) +#define DPTX_REMOTE_PORT_DFP GENMASK(7, 4) +#define DPTX_REMOTE_PORT_DIE GENMASK(11, 8) +#define DPTX_REMOTE_PORT_CONNECTED BIT(15) + +struct dptx_connection { + __le32 unk; + __le32 target; +} __packed; + +struct dptx_connection_reply { + __le32 unk; + __le32 target; + __le64 reserved; + __le32 status; + u8 padding[12]; +} __packed; + +struct dptx_hotplug { + u8 pad[16]; + __le32 unk; +} __packed; + +static bool dptx_needs_port(u32 command) +{ + switch (command) { + case DPTX_APCALL_ACTIVATE: + case DPTX_APCALL_DEACTIVATE: + case DPTX_APCALL_WILL_CHANGE_LINK_CONFIG: + case DPTX_APCALL_DID_CHANGE_LINK_CONFIG: + case DPTX_APCALL_SET_LINK_RATE: + case DPTX_APCALL_SET_DOWN_SPREAD: + case DPTX_APCALL_SET_ACTIVE_LANE_COUNT: + case DPTX_APCALL_SET_DRIVE_SETTINGS: + case DPTX_APCALL_GET_LINK_RATE: + case DPTX_APCALL_GET_ACTIVE_LANE_COUNT: + case DPTX_APCALL_GET_DRIVE_SETTINGS: + return true; + default: + return false; + } +} + +/* Requests carry their argument, replies their value, at offset 16. */ +static u32 dptx_call(struct dcpib_service *service, u16 group, u32 command, + const void *data, size_t data_size, void *reply, + size_t reply_size) +{ + struct dcpib *dcp = service->ep->dcp; + struct dcpib_port *port = service->cookie; + const struct dcpib_phy_ops *phy = dcp->phy; + u64 value; + u32 arg; + + if (group && !(group == 1 && (command == DPTX_APCALL_SET_DRIVE_SETTINGS || + command == DPTX_APCALL_GET_DRIVE_SETTINGS))) + return KIO_RETURN_UNSUPPORTED; + + dev_dbg(dcp->dev, "DPTX: port %u group %u command %u size %zu\n", + service->unit, group, command, data_size); + if ((!port || !phy) && dptx_needs_port(command)) + return KIO_RETURN_NOT_READY; + + switch (command) { + case DPTX_APCALL_ACTIVATE: + if (phy->activate(dcp)) + return KIO_RETURN_ERROR; + port->link_rate = DCPIB_LINK_RATE_HBR; + return 0; + case DPTX_APCALL_DEACTIVATE: + if (phy->deactivate(dcp)) + return KIO_RETURN_ERROR; + port->link_rate = 0; + return 0; + case DPTX_APCALL_WILL_CHANGE_LINK_CONFIG: + case DPTX_APCALL_DID_CHANGE_LINK_CONFIG: + return phy->set_route(dcp, command == + DPTX_APCALL_DID_CHANGE_LINK_CONFIG) ? + KIO_RETURN_ERROR : 0; + case DPTX_APCALL_GET_MAX_LINK_RATE: + value = phy ? phy->max_link_rate : DCPIB_LINK_RATE_HBR; + break; + case DPTX_APCALL_GET_LINK_RATE: + value = port->link_rate; + break; + case DPTX_APCALL_SET_LINK_RATE: + if (data_size < 17) + return KIO_RETURN_BAD_ARG; + arg = ((const u8 *)data)[16]; + if (arg > phy->max_link_rate) + return KIO_RETURN_UNSUPPORTED; + if (phy->set_link_rate(dcp, arg)) + return KIO_RETURN_ERROR; + port->link_rate = arg; + return 0; + case DPTX_APCALL_GET_MAX_LANE_COUNT: + value = 4; + break; + case DPTX_APCALL_GET_ACTIVE_LANE_COUNT: + value = port->lanes; + break; + case DPTX_APCALL_SET_ACTIVE_LANE_COUNT: + if (data_size < 20) + return KIO_RETURN_BAD_ARG; + arg = get_unaligned_le32(data + 16); + if (arg == 3 || arg > 4) + return KIO_RETURN_BAD_ARG; + dev_info(dcp->dev, "DPTX: %u active lanes\n", arg); + if (phy->set_lane_count && phy->set_lane_count(dcp, arg)) + return KIO_RETURN_ERROR; + port->lanes = arg; + if (arg) + port->trained_lanes = arg; + return 0; + case DPTX_APCALL_GET_MAX_DRIVE_SETTINGS: { + const struct dcpib_drive_settings maximum = { + 0, cpu_to_le32(3), cpu_to_le32(3) + }; + + if (reply_size < 16 + sizeof(maximum)) + return KIO_RETURN_BAD_ARG; + memcpy(reply + 16, &maximum, sizeof(maximum)); + return 0; + } + case DPTX_APCALL_SET_DRIVE_SETTINGS: { + struct dcpib_drive_settings settings[4]; + + if (group != 1) + return KIO_RETURN_UNSUPPORTED; + if (data_size < 32) + return KIO_RETURN_BAD_ARG; + arg = get_unaligned_le32(data + 16); + if (arg > 4 || data_size < 32 + arg * sizeof(settings[0])) + return KIO_RETURN_BAD_ARG; + memcpy(settings, data + 32, arg * sizeof(settings[0])); + if (phy->set_drive(dcp, settings, arg)) + return KIO_RETURN_ERROR; + memcpy(port->drive, settings, arg * sizeof(settings[0])); + port->drive_count = arg; + return 0; + } + case DPTX_APCALL_GET_DRIVE_SETTINGS: + /* + * The firmware reads back what it set during link training; an + * unsupported reply here was followed by a firmware crash. + * Same layout as SET: count at 16, settings from 32. + */ + if (group != 1) + return KIO_RETURN_UNSUPPORTED; + if (reply_size < 32 + port->drive_count * sizeof(port->drive[0])) + return KIO_RETURN_BAD_ARG; + memcpy(reply, data, min(reply_size, data_size)); + put_unaligned_le32(port->drive_count, reply + 16); + memcpy(reply + 32, port->drive, + port->drive_count * sizeof(port->drive[0])); + return 0; + case DPTX_APCALL_SET_DOWN_SPREAD: + if (data_size < 17) + return KIO_RETURN_BAD_ARG; + /* The qualified link setup runs with downspread disabled. */ + return ((const u8 *)data)[16] ? KIO_RETURN_UNSUPPORTED : 0; + case DPTX_APCALL_GET_SUPPORTS_DOWN_SPREAD: + case DPTX_APCALL_GET_DOWN_SPREAD: + case DPTX_APCALL_GET_SUPPORTS_LANE_MAPPING: + case DPTX_APCALL_GET_VIRTUAL_DEVICE_MODE: + value = 0; + break; + case DPTX_APCALL_GET_SUPPORTS_HPD: + value = 1; + break; + case DPTX_APCALL_INACTIVE_SINK_DETECTED: + case DPTX_APCALL_DEVICE_NOT_RESPONDING: + case DPTX_APCALL_DEVICE_BUSY_TIMEOUT: + case DPTX_APCALL_DEVICE_NOT_STARTED: + /* Link fault reports: acknowledged with a zeroed retcode. */ + dev_info(dcp->dev, "DPTX: firmware reports link fault %u\n", + command); + memcpy(reply, data, min(reply_size, data_size)); + if (reply_size > 4) + memset(reply, 0, 4); + return 0; + case DPTX_APCALL_SET_TILED_DISPLAY_HINTS: + memcpy(reply, data, min(reply_size, data_size)); + if (reply_size >= 4) + *(__le32 *)reply = cpu_to_le32(1); + return 0; + default: + dev_info(dcp->dev, "DPTX: unsupported call %u\n", command); + return KIO_RETURN_UNSUPPORTED; + } + + if (reply_size < 16 + sizeof(value)) + return KIO_RETURN_BAD_ARG; + put_unaligned_le64(value, reply + 16); + return 0; +} + +static const struct dcpib_service_ops dptx_ops = { + .call = dptx_call, +}; + +int dcpib_dptx_init(struct dcpib *dcp) +{ + struct dcpib_ep *ep = dcpib_ep_start(dcp, DCPIB_EP_DPTX, &dptx_ops, 2); + + if (IS_ERR(ep)) + return PTR_ERR(ep); + dcp->dptx_ep = ep; + /* Ports in the order the firmware announced them. */ + for (unsigned int i = 0; i < ep->nr_services && i < 2; i++) { + dcp->port[i].service = &ep->services[i]; + ep->services[i].cookie = &dcp->port[i]; + } + return 0; +} + +int dcpib_dpav_init(struct dcpib *dcp) +{ + struct dcpib_ep *ep = dcpib_ep_start(dcp, DCPIB_EP_DPAV, NULL, 4); + + if (IS_ERR(ep)) + return PTR_ERR(ep); + dcp->dpav_ep = ep; + return 0; +} + +/* connectTo, then requestDisplay, as aura dcp_dptx_connect. */ +int dcpib_dptx_connect(struct dcpib *dcp, unsigned int port) +{ + struct dcpib_service *service = dcp->port[port].service; + u32 target = FIELD_PREP(DPTX_REMOTE_PORT_CORE, 0) | + FIELD_PREP(DPTX_REMOTE_PORT_DFP, dcp->dcp_output) | + FIELD_PREP(DPTX_REMOTE_PORT_DIE, dcp->die) | + DPTX_REMOTE_PORT_CONNECTED; + struct dptx_connection cmd = { + /* IODPTXPortAttributes supportsHPD */ + .unk = cpu_to_le32(0x100), + .target = cpu_to_le32(target), + }; + struct dptx_connection_reply resp = {}; + __le32 status = 0; + int ret; + + if (!service) + return -ENODEV; + ret = dcpib_service_call(service, 0, 11, &cmd, sizeof(cmd), 24, &resp, + sizeof(resp), 0); + if (ret) + return ret; + if (resp.status) { + dev_err(dcp->dev, "DPTX: connect returned %#x\n", + le32_to_cpu(resp.status)); + return -EIO; + } + if (le32_to_cpu(resp.target) != target || + le32_to_cpu(resp.unk) != 0x100) + return -EPROTO; + + ret = dcpib_service_call(service, 0, 6, NULL, 0, 16, &status, + sizeof(status), 12); + if (!ret && status) { + dev_err(dcp->dev, "DPTX: requestDisplay returned %#x\n", + le32_to_cpu(status)); + ret = -EIO; + } + return ret; +} + +int dcpib_dptx_set_hpd(struct dcpib *dcp, unsigned int port, bool hpd) +{ + struct dcpib_service *service = dcp->port[port].service; + struct dptx_hotplug cmd = { .unk = cpu_to_le32(hpd) }, resp = {}; + int ret; + + if (!service) + return -ENODEV; + ret = dcpib_service_call(service, 8, 8, &cmd, sizeof(cmd), 12, &resp, + sizeof(resp), 12); + if (ret) + return ret; + /* The status is the first word of the reply. */ + return get_unaligned_le32(resp.pad) ? -EIO : 0; +} diff --git a/drivers/gpu/drm/apple_dcpib/drv.c b/drivers/gpu/drm/apple_dcpib/drv.c new file mode 100644 index 0000000000000..e6ad3e2fc2767 --- /dev/null +++ b/drivers/gpu/drm/apple_dcpib/drv.c @@ -0,0 +1,504 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +/* + * Apple T8152 DCP on the iBoot interface: probe and bring-up. + * + * The bootloader (aura) trains the display and hands the DCP over with its + * RTKit session running. This driver adopts that session, starts the iBoot, + * DPAV and DPTX port endpoints, reconnects the DPTX port and asserts HPD, + * then registers a DRM device whose flips are iBoot SET_SURFACE calls, in + * the order aura's display_init_t8152 uses. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "dcpib.h" + +#define DCPIB_HPD_TIMEOUT_MS 10000 + +static void dcpib_rtk_recv(void *cookie, u8 ep, u64 message) +{ + struct dcpib *dcp = cookie; + struct dcpib_ep *e = NULL; + + if (ep >= DCPIB_FIRST_EP && ep < DCPIB_FIRST_EP + DCPIB_NR_EPS) + e = dcp->eps[ep - DCPIB_FIRST_EP]; + if (e) + dcpib_ep_recv(e, message); + else + dev_dbg(dcp->dev, "message %#llx for idle endpoint %#x\n", + message, ep); +} + +static void dcpib_rtk_crashed(void *cookie, const void *crashlog, size_t size) +{ + struct dcpib *dcp = cookie; + + dev_err(dcp->dev, "DCP firmware crashed\n"); +} + +/* Buffers the firmware pre-allocated in its own DART mappings, or new ones. */ +static int dcpib_rtk_shmem_setup(void *cookie, struct apple_rtkit_shmem *bfr) +{ + struct dcpib *dcp = cookie; + + if (bfr->iova) { + struct iommu_domain *domain = iommu_get_domain_for_dev(dcp->dev); + phys_addr_t phys; + + if (!domain) + return -ENOMEM; + phys = iommu_iova_to_phys(domain, bfr->iova); + if (!phys) { + dev_warn(dcp->dev, "no mapping for firmware buffer %pad\n", + &bfr->iova); + return -ENOMEM; + } + /* The firmware writes these behind the CPU's back. */ + bfr->buffer = memremap(phys, bfr->size, MEMREMAP_WC); + if (!bfr->buffer) + return -ENOMEM; + bfr->is_mapped = true; + return 0; + } + + bfr->buffer = dma_alloc_coherent(dcp->dev, bfr->size, &bfr->iova, + GFP_KERNEL); + return bfr->buffer ? 0 : -ENOMEM; +} + +static void dcpib_rtk_shmem_destroy(void *cookie, struct apple_rtkit_shmem *bfr) +{ + struct dcpib *dcp = cookie; + + if (bfr->is_mapped) + memunmap(bfr->buffer); + else + dma_free_coherent(dcp->dev, bfr->size, bfr->buffer, bfr->iova); +} + +static const struct apple_rtkit_ops dcpib_rtkit_ops = { + .crashed = dcpib_rtk_crashed, + .recv_message = dcpib_rtk_recv, + .shmem_setup = dcpib_rtk_shmem_setup, + .shmem_destroy = dcpib_rtk_shmem_destroy, +}; + +DEFINE_DRM_GEM_DMA_FOPS(dcpib_fops); + +static const struct drm_driver dcpib_drm_driver = { + /* + * Frames are copied by the CPU, imported ones too: a compositor + * driving both displays imports one device's buffers into the other. + */ + DRM_GEM_DMA_DRIVER_OPS_VMAP, + DRM_FBDEV_DMA_DRIVER_OPS, + .name = "apple-dcpib", + .desc = "Apple DCP (iBoot interface)", + .major = 1, + .minor = 0, + .driver_features = DRIVER_GEM | DRIVER_MODESET | DRIVER_ATOMIC, + .fops = &dcpib_fops, +}; + +/* + * Adopt the bootloader's RTKit session. The T8152 DCP does not acknowledge + * AP QUIESCED, so a quiesced handoff cannot be negotiated, and an IOP INIT + * while its AP power is still ON panics the firmware ("Invalid AP power: + * 32"). Keep the running session instead and start the application + * endpoints explicitly (dcpib_ep_start), as drm/apple does for live M5 + * sessions. + */ +static int dcpib_boot(struct dcpib *dcp) +{ + dcp->rtk = devm_apple_rtkit_init_adopted(dcp->dev, dcp, NULL, 0, + &dcpib_rtkit_ops); + if (IS_ERR(dcp->rtk)) + return PTR_ERR(dcp->rtk); + return apple_rtkit_adopt_running(dcp->rtk); +} + +/* + * The display pipe scans out through dart-disp0, the DCP fetches through + * dart-dcp. Both are locked with their own live root, so each needs its own + * device and domain: one domain spanning both would publish dart-dcp's root + * into dart-disp0 and drop the scanout mappings the bootloader left there + * (the pipe then faults, IOMFB axi_rd_err). Scanout buffers are allocated on + * the DCP device and mapped into the scanout domain at the same address. + */ +static int dcpib_scanout_init(struct dcpib *dcp) +{ + struct device_node *np __free(device_node) = + of_get_child_by_name(dcp->dev->of_node, "scanout"); + int ret; + + if (!np) + return -ENODEV; + dcp->scanout = of_platform_device_create(np, NULL, dcp->dev); + if (!dcp->scanout) + dcp->scanout = of_find_device_by_node(np); + if (!dcp->scanout) + return -ENODEV; + ret = dma_set_mask_and_coherent(&dcp->scanout->dev, DMA_BIT_MASK(64)); + if (ret) + return ret; + ret = of_dma_configure(&dcp->scanout->dev, np, true); + if (ret) + return ret; + dcp->scanout_dom = iommu_get_domain_for_dev(&dcp->scanout->dev); + return dcp->scanout_dom ? 0 : -ENODEV; +} + +static void *dcpib_vmap_wc(struct sg_table *sgt) +{ + unsigned int n = 0, count = 0; + struct sg_page_iter iter; + struct page **pages; + void *vaddr; + + for_each_sgtable_page(sgt, &iter, 0) + count++; + pages = kvmalloc_array(count, sizeof(*pages), GFP_KERNEL); + if (!pages) + return NULL; + for_each_sgtable_page(sgt, &iter, 0) + pages[n++] = sg_page_iter_page(&iter); + vaddr = vmap(pages, count, VM_MAP, pgprot_writecombine(PAGE_KERNEL)); + kvfree(pages); + return vaddr; +} + +/* + * The display pipe's DART is locked and comes with the firmware's own + * mappings. The address the coprocessor's allocator handed out for a scanout + * buffer can be one of them, and mapping over it corrupts what the pipe + * fetches every frame (the picture flickers). Start at @dva and go down until + * a range with no mapping is found. + */ +static dma_addr_t dcpib_scan_free_dva(struct dcpib *dcp, dma_addr_t dva, + size_t size) +{ + const size_t step = SZ_16K; + + size = ALIGN(size, step); + for (int tries = 0; tries < 64 && dva >= size; tries++) { + size_t off; + + for (off = 0; off < size; off += step) + if (iommu_iova_to_phys(dcp->scanout_dom, dva + off)) + break; + if (off == size) + return dva; + dev_info(dcp->dev, "scanout DART: %pad+%#zx is in use, moving down\n", + &dva, off); + dva -= size; + } + return 0; +} + +/* One scanout buffer: allocated, cleared, mapped for the CPU and the pipe. */ +static int dcpib_scan_alloc(struct dcpib *dcp, int i, size_t size) +{ + struct dcpib_dma *fb = &dcp->scan[i]; + struct sg_table sgt; + ssize_t mapped; + dma_addr_t dva; + int ret; + + ret = dcpib_dma_alloc(dcp, fb, size); + if (ret) + return ret; + ret = dma_get_sgtable(dcp->dev, &sgt, fb->cpu, fb->iova, fb->size); + if (ret) + return ret; + /* + * The DCP is coherent, the display pipe is not: it reads memory past + * the CPU caches, and frames written through the cacheable mapping + * show only once evicted. Write through a write-combined mapping of + * the same pages, with the zeroing of the allocation cleaned out first. + */ + arch_wb_cache_pmem(fb->cpu, fb->size); + dcp->scan_wc[i] = dcpib_vmap_wc(&sgt); + if (!dcp->scan_wc[i]) { + sg_free_table(&sgt); + return -ENOMEM; + } + memset(dcp->scan_wc[i], 0, fb->size); + /* + * Cacheable, as iBoot maps scanout buffers: the pipe faults on + * entries with the DART's no-cache bit (IOMFB axi_rd_err). + */ + dva = dcpib_scan_free_dva(dcp, fb->iova, fb->size); + if (!dva) { + sg_free_table(&sgt); + return -ENOSPC; + } + mapped = iommu_map_sgtable(dcp->scanout_dom, dva, &sgt, + IOMMU_READ | IOMMU_WRITE | IOMMU_CACHE); + sg_free_table(&sgt); + if (mapped < (ssize_t)fb->size) + return mapped < 0 ? mapped : -ENOMEM; + dcp->scan_dva[i] = dva; + dev_info(dcp->dev, "scanout buffer %d: %zu bytes at %pad\n", i, fb->size, &dva); + return 0; +} + +/* + * A fresh iBoot session has no surface. Whenever the pipe runs without one + * (the link retrains when the DPTX services restart, SET_MODE turns it on), + * it fetches from a null base and the firmware takes an NMI (IOMFB + * axi_rd_err). Hand it a black buffer of the boot mode at once, sized from + * the bootloader's simple-framebuffer. + */ +static int dcpib_boot_surface(struct dcpib *dcp) +{ + /* + * The boot display's framebuffer is /chosen's; the bootloader adds the + * second display's (HDMI) at the root. + */ + struct device_node *chosen __free(device_node) = + of_find_node_by_path("/chosen"); + struct device_node *parent __free(device_node) = + of_property_read_bool(dcp->dev->of_node, "apple,hdmi") ? + of_find_node_by_path("/") : of_node_get(chosen); + u32 width = 1280, height = 720, stride = 1280 * 4; + int ret; + + for_each_child_of_node_scoped(parent, np) { + struct resource res; + + if (!of_device_is_compatible(np, "simple-framebuffer")) + continue; + of_property_read_u32(np, "width", &width); + of_property_read_u32(np, "height", &height); + of_property_read_u32(np, "stride", &stride); + /* + * Take the display over from simpledrm on the bootloader's + * framebuffer, so it does not shadow this driver's output + * (fbcon on a framebuffer nothing scans out). + */ + if (!of_address_to_resource(np, 0, &res)) { + ret = aperture_remove_conflicting_devices(res.start, + resource_size(&res), dcpib_drm_driver.name); + if (ret) + dev_warn(dcp->dev, "simpledrm stays: %d\n", ret); + } + break; + } + /* + * Large enough for the largest mode: while a SET_MODE switches, the + * pipe briefly scans the current surface with the new mode's size. + */ + size_t size = (size_t)stride * height; + + for (unsigned int i = 0; i < dcp->nr_tmodes; i++) + size = max_t(size_t, size, (size_t)le32_to_cpu(dcp->tmodes[i].width) * + le32_to_cpu(dcp->tmodes[i].height) * 4); + dcp->boot_w = width; + dcp->boot_h = height; + dcp->boot_stride = stride; + for (int i = 0; i < ARRAY_SIZE(dcp->scan); i++) { + ret = dcpib_scan_alloc(dcp, i, size); + if (ret) + return ret; + } + dcp->front = 0; + ret = dcpib_ib_set_surface(dcp, dcp->scan_dva[0], width, height, + stride); + dev_info(dcp->dev, "boot surface %ux%u at %pad: %d\n", width, height, + &dcp->scan_dva[0], ret); + return ret; +} + +/* Wait until the firmware reports a sink with modes, as aura does. */ +int dcpib_wait_hpd(struct dcpib *dcp) +{ + unsigned long timeout = jiffies + msecs_to_jiffies(DCPIB_HPD_TIMEOUT_MS); + u32 timings = 0, colors = 0; + int hpd; + + do { + hpd = dcpib_ib_get_hpd(dcp, &timings, &colors); + if (hpd > 0 && timings && colors) { + dev_info(dcp->dev, "sink: %u timing, %u color modes\n", + timings, colors); + return 0; + } + msleep(100); + } while (time_before(jiffies, timeout)); + + dev_err(dcp->dev, "no sink (hpd %d, %u timing, %u color modes)\n", hpd, + timings, colors); + return hpd < 0 ? hpd : -ENODEV; +} + +static int dcpib_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct device_node *hdmi_np __free(device_node) = dcpib_hdmi_node(); + int source = hdmi_np ? dcpib_hdmi_source(hdmi_np) : -ENODEV; + struct dcpib *dcp; + int ret; + + /* + * Without a USB-C display the bootloader drives HDMI from the main DCP + * (source 0) and never starts DCPEXT0. + */ + if (dev->of_node == hdmi_np && source == 0) { + dev_info(dev, "HDMI is on the main DCP\n"); + return -ENODEV; + } + + ret = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64)); + if (ret) + return ret; + + dcp = devm_drm_dev_alloc(dev, &dcpib_drm_driver, struct dcpib, drm); + if (IS_ERR(dcp)) + return PTR_ERR(dcp); + dcp->dev = dev; + platform_set_drvdata(pdev, dcp); + mutex_init(&dcp->lock); + of_property_read_u32(dev->of_node, "apple,die", &dcp->die); + + dcp->wq = devm_alloc_ordered_workqueue(dev, "dcpib", WQ_MEM_RECLAIM); + if (!dcp->wq) + return -ENOMEM; + + ret = dcpib_scanout_init(dcp); + if (ret) + return dev_err_probe(dev, ret, "no scanout DART stream\n"); + + dcp->hdmi = dev->of_node == hdmi_np || source == 0; + if (dcp->hdmi && dev->of_node != hdmi_np) + dev_info(dev, "HDMI from the main DCP (no USB-C display at boot)\n"); + /* Read only: the pin belongs to a controller the AOP runs. */ + dcp->hpd_gpio = devm_fwnode_gpiod_get(dev, + of_fwnode_handle(dcp->hdmi ? hdmi_np : dev->of_node), "hpd", + GPIOD_ASIS, "hpd"); + if (PTR_ERR(dcp->hpd_gpio) == -ENOENT) + dcp->hpd_gpio = NULL; + if (IS_ERR(dcp->hpd_gpio)) + return dev_err_probe(dev, PTR_ERR(dcp->hpd_gpio), "no HPD GPIO\n"); + dcp->hpd = 1; + ret = dcp->hdmi ? dcpib_hdmi_init(dcp, hdmi_np) : dcpib_atc_init(dcp); + if (ret) + return dev_err_probe(dev, ret, "no DisplayPort phy\n"); + + ret = dcpib_boot(dcp); + if (ret) + return dev_err_probe(dev, ret, "cannot adopt the DCP\n"); + + ret = dcpib_iboot_init(dcp); + if (ret) + return dev_err_probe(dev, ret, "no disp0-service\n"); + /* + * The new iBoot session has dropped the surface while the pipe still + * runs: it fetches from a null base (IOMFB axi_rd_err, firmware NMI) + * unless it is powered off at once. It comes back on below, after a + * surface is set. + */ + ret = dcpib_ib_set_power(dcp, false); + if (ret) + return dev_err_probe(dev, ret, "display power off failed\n"); + ret = dcpib_dpav_init(dcp); + if (ret) + return dev_err_probe(dev, ret, "no DPAV controllers\n"); + ret = dcpib_dptx_init(dcp); + if (ret) + return dev_err_probe(dev, ret, "no DPTX port service\n"); + + ret = dcpib_dptx_connect(dcp, 0); + if (ret) + return dev_err_probe(dev, ret, "DPTX connect failed\n"); + ret = dcpib_dptx_set_hpd(dcp, 0, true); + if (ret) + return dev_err_probe(dev, ret, "DPTX hotplug failed\n"); + + ret = dcpib_wait_hpd(dcp); + if (ret) + return ret; + ret = dcpib_ib_get_modes(dcp); + if (ret) + return dev_err_probe(dev, ret, "no modes\n"); + ret = dcpib_boot_surface(dcp); + if (ret) + return dev_err_probe(dev, ret, "no boot surface\n"); + /* + * Without the bootloader's link (no display on the port at boot) the + * phy starts cold, the right way round for the plug. + */ + if (dcp->phy->detect && dcp->phy->set_orientation) { + bool flipped; + + if (dcp->phy->detect(dcp, &flipped) > 0) { + if (dcp->phy->select) + dcp->phy->select(dcp); + dcp->phy->set_orientation(dcp, flipped); + } + } + ret = dcp->phy->activate(dcp); + if (ret) + return dev_err_probe(dev, ret, "phy activation failed\n"); + /* While the display is still off and the firmware does no AUX. */ + dcpib_update_edid(dcp); + ret = dcpib_ib_set_power(dcp, true); + if (ret) + return dev_err_probe(dev, ret, "set_power failed\n"); + dcp->powered = true; + dev_info(dev, "%u timing modes, %u color modes\n", dcp->nr_tmodes, + dcp->nr_cmodes); + + ret = dcpib_kms_init(dcp); + if (ret) + return ret; + ret = dcpib_set_boot_mode(dcp); + if (ret) + dev_warn(dev, "boot mode not set again: %d\n", ret); + ret = drm_dev_register(&dcp->drm, 0); + if (ret) + return ret; + if (dcp->hpd_gpio || dcp->pd) + drmm_kms_helper_poll_init(&dcp->drm); + drm_client_setup(&dcp->drm, NULL); + return 0; +} + +static const struct of_device_id dcpib_of_match[] = { + { .compatible = "apple,t8152-dcp-iboot" }, + {} +}; +MODULE_DEVICE_TABLE(of, dcpib_of_match); + +static struct platform_driver dcpib_platform_driver = { + .driver = { + .name = "apple-dcpib", + .of_match_table = dcpib_of_match, + /* The DCP and the link cannot be handed back yet. */ + .suppress_bind_attrs = true, + }, + .probe = dcpib_probe, +}; +module_platform_driver(dcpib_platform_driver); + +MODULE_DESCRIPTION("Apple T8152 DCP display driver (iBoot interface)"); +MODULE_LICENSE("Dual MIT/GPL"); diff --git a/drivers/gpu/drm/apple_dcpib/hdmi.c b/drivers/gpu/drm/apple_dcpib/hdmi.c new file mode 100644 index 0000000000000..36d7ee6135573 --- /dev/null +++ b/drivers/gpu/drm/apple_dcpib/hdmi.c @@ -0,0 +1,337 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +/* + * The J873g HDMI port: the dedicated T8152 dptx-phy in display-crossbar0's + * lpdptx slot, driven by DCPEXT0 and converted by a PS190 (aura + * src/dcp/dptx_phy.c, DPTX_PHY_T8152_HDMI). + * + * The bootloader powers the phy up, applies its tunables, selects the DCP + * and trains the link. This keeps that setup and reprograms what a link + * training changes: the PLL for a new link rate and the active lanes. Drive + * settings are only recorded, as on macOS, and there is no per-link route. + */ + +#include +#include +#include +#include +#include +#include + +#include "dcpib.h" + +#define HDMI_LANES 4 +#define HDMI_LANE0 0x4500 +#define HDMI_LANE_STRIDE 0x2800 + +struct dcpib_hdmi { + void __iomem *core; + void __iomem *apb; + /* AP DisplayPort AUX controller aux1, for the EDID */ + struct dcpib_aux aux; +}; + +/* AppleT8112DPTXPort::setDPTXSourceSelect: crossbar UFP feeding the phy */ +#define HDMI_SOURCE_SELECT 0x10 +#define HDMI_SOURCE_AUX1 6 /* UFP endpoint of dptx1-aux */ + +/* AppleT8152DPTXPort::setBitsInReg */ +static void hdmi_set_bits(struct dcpib_hdmi *hdmi, u32 offset, u32 shift, + u32 mask, u32 value) +{ + u32 reg = readl(hdmi->core + offset); + + writel((reg & ~mask) | (value << shift), hdmi->core + offset); +} + +static void hdmi_apb_update(struct dcpib_hdmi *hdmi, u32 offset, u32 mask, + u32 value) +{ + u32 reg = readl(hdmi->apb + offset); + + writel((reg & ~mask) | value, hdmi->apb + offset); +} + +/* + * AppleT8152DPTXPort::getTunableConfig: the DPPLLConfig words for each link + * rate (constant tables in AppleDisplayCrossbar, 26A434). + */ +static const struct { + u32 rate; /* DPCD units */ + u32 w[22]; +} hdmi_pll_configs[] = { + { DCPIB_LINK_RATE_RBR, + { 1, 0x21c, 0, 0, 1, 0x13, 1, 0, 0, 0x1a, 1, 0x3c, 2, 1, 0, 0, 1, 0, + 0, 0, 1, 3 } }, + { DCPIB_LINK_RATE_HBR, + { 1, 0x1c2, 0x1fff, 0x3ffe, 1, 0x13, 1, 0, 0, 0x1a, 1, 0x32, 2, 1, 0, + 0, 1, 0, 0, 0, 1, 3 } }, + { DCPIB_LINK_RATE_HBR2, + { 0, 0x1c2, 0x1fff, 0x3ffe, 1, 9, 1, 0, 0, 0x1a, 1, 0x32, 1, 0, 0, 1, + 0, 0, 0, 1, 1, 3 } }, + { DCPIB_LINK_RATE_HBR3, + { 0, 0x2a3, 0x2ffd, 0x3ffc, 1, 9, 1, 0, 0, 0x1a, 1, 0x5a, 1, 0, 0, 1, + 0, 0, 0, 1, 1, 3 } }, +}; + +/* + * AppleT8152DPTXPort::pllPowerUp/pllPowerDown: command in 0x408[2:0], + * request bit 3, acknowledge bit 19. + */ +static int hdmi_pll_command(struct dcpib *dcp, struct dcpib_hdmi *hdmi, + u32 command) +{ + int i; + + hdmi_set_bits(hdmi, 0x408, 6, 0x40, 1); + hdmi_set_bits(hdmi, 0x408, 0, 0x7, command); + hdmi_set_bits(hdmi, 0x408, 3, 0x8, 1); + for (i = 0; i < 10; i++) { + udelay(16); + if (readl(hdmi->core + 0x408) & BIT(19)) + break; + } + hdmi_set_bits(hdmi, 0x408, 3, 0x8, 0); + hdmi_set_bits(hdmi, 0x408, 6, 0x40, 0); + if (i == 10) { + dev_err(dcp->dev, "HDMI: no ACK to PLL command %u\n", command); + return -ETIMEDOUT; + } + return 0; +} + +/* + * AppleT8152DPTXPort::phySetActiveLaneCount: lane-disable bits, 0x2420 holds + * the mask of unused lanes. + */ +static void hdmi_set_lanes(struct dcpib_hdmi *hdmi, u32 lanes) +{ + static const u32 offsets[] = { 0x64, 0x24, 0x24, 0x48, 0x48, 0x5c, 0x5c, 0x48 }; + static const u32 shifts[] = { 4, 4, 1, 0, 1, 2, 3, 4 }; + static const u32 masks[] = { 0x10, 0x10, 0x2, 0x1, 0x2, 0x4, 0x8, 0x10 }; + u32 lane, i; + + hdmi_set_bits(hdmi, 0x2400, 0, 0x1, 0); + for (lane = 0; lane < HDMI_LANES; lane++) { + u32 base = HDMI_LANE0 + lane * HDMI_LANE_STRIDE; + + for (i = 0; i < ARRAY_SIZE(offsets); i++) + hdmi_set_bits(hdmi, base + offsets[i], shifts[i], + masks[i], lane >= lanes); + } + hdmi_set_bits(hdmi, 0x2420, 0, 0xf, 0xf & ~((1 << lanes) - 1)); + hdmi_set_bits(hdmi, 0x2400, 0, 0x1, 1); +} + +static int hdmi_activate(struct dcpib *dcp) +{ + return 0; +} + +static int hdmi_deactivate(struct dcpib *dcp) +{ + dev_info(dcp->dev, "HDMI: keeping the link up on deactivate\n"); + return 0; +} + +/* + * AppleDPTXPort::setLinkRate with the T8152 hooks: phyPowerDown, + * pllPowerDown, pllConfigureFrequency, phy/pllSetHalfLinkRate, pllPowerUp. + */ +static int hdmi_set_link_rate(struct dcpib *dcp, u32 rate) +{ + struct dcpib_hdmi *hdmi = dcp->phy_priv; + const u32 *w = NULL; + bool half; + size_t i; + int ret; + + for (i = 0; i < ARRAY_SIZE(hdmi_pll_configs); i++) + if (hdmi_pll_configs[i].rate == rate) + w = hdmi_pll_configs[i].w; + if (!w) + return -EINVAL; + + dev_info(dcp->dev, "HDMI: link rate %#x\n", rate); + hdmi_set_bits(hdmi, 0x2400, 0, 0x1, 0); + ret = hdmi_pll_command(dcp, hdmi, 0); + if (ret) + return ret; + + /* pllConfigureFrequency, downspread off */ + hdmi_set_bits(hdmi, 0x6c0, 0, 0x7ff, w[1]); + hdmi_set_bits(hdmi, 0x6c0, 11, 0x800, w[6]); + hdmi_set_bits(hdmi, 0x6c0, 13, 0x2000, w[10]); + hdmi_set_bits(hdmi, 0x6c0, 18, 0x3c0000, w[21]); + hdmi_set_bits(hdmi, 0x6c4, 14, 0xfffc000, w[2]); + hdmi_set_bits(hdmi, 0x6c4, 0, 0x3fff, + (!(w[7] & 1) && w[2] < 2) ? 0 : w[3]); + hdmi_set_bits(hdmi, 0x6c8, 14, 0xc000, w[0]); + hdmi_set_bits(hdmi, 0x6c8, 8, 0x100, 0); + hdmi_set_bits(hdmi, 0x6c8, 0, 0xff, w[8]); + hdmi_set_bits(hdmi, 0x6c8, 9, 0x3e00, w[5]); + hdmi_set_bits(hdmi, 0x6c8, 16, 0x10000, w[17]); + hdmi_set_bits(hdmi, 0x6c8, 26, 0xfc000000, w[9]); + hdmi_set_bits(hdmi, 0x6cc, 0, 0x7f, w[11]); + hdmi_set_bits(hdmi, 0x6cc, 9, 0x600, w[13]); + hdmi_set_bits(hdmi, 0x6cc, 11, 0x7800, w[14]); + hdmi_set_bits(hdmi, 0x6cc, 15, 0x38000, w[15]); + hdmi_set_bits(hdmi, 0x6cc, 20, 0x100000, w[16]); + hdmi_set_bits(hdmi, 0x6cc, 21, 0x200000, w[18]); + hdmi_set_bits(hdmi, 0x6cc, 22, 0xc00000, w[19]); + hdmi_set_bits(hdmi, 0x6cc, 24, 0x3000000, w[4]); + hdmi_set_bits(hdmi, 0x6cc, 26, 0xc000000, w[20]); + + /* AppleT8112DPTXPort::updateLinkRateDependentShadowRegisters */ + hdmi_apb_update(hdmi, 0xa0, BIT(3), BIT(3)); + hdmi_apb_update(hdmi, 0xa0, BIT(2), BIT(2)); + hdmi_apb_update(hdmi, 0xa0, BIT(18), BIT(18)); + hdmi_apb_update(hdmi, 0xa0, BIT(18), 0); + hdmi_apb_update(hdmi, 0xa0, BIT(19), BIT(19)); + hdmi_apb_update(hdmi, 0xa0, BIT(19), 0); + hdmi_apb_update(hdmi, 0xa0, BIT(2), 0); + hdmi_apb_update(hdmi, 0xa0, BIT(3), 0); + + /* No UHBR rates; half link rate from HBR up. */ + half = rate >= DCPIB_LINK_RATE_HBR; + hdmi_set_bits(hdmi, 0x2400, 2, 0x4, 0); + hdmi_set_bits(hdmi, 0x2400, 3, 0x8, !half); + hdmi_set_bits(hdmi, 0x0000, 17, 0x20000, !half); + + ret = hdmi_pll_command(dcp, hdmi, 7); + if (ret) + return ret; + if (!(readl(hdmi->core + 0x438) & BIT(30))) { + dev_err(dcp->dev, "HDMI: PLL failed to lock at rate %#x\n", rate); + return -EIO; + } + return 0; +} + +static int hdmi_set_lane_count(struct dcpib *dcp, u32 lanes) +{ + /* T8152 takes no zero lane count. */ + if (lanes) + hdmi_set_lanes(dcp->phy_priv, lanes); + return 0; +} + +static int hdmi_set_drive(struct dcpib *dcp, + const struct dcpib_drive_settings *settings, u32 count) +{ + return 0; +} + +static int hdmi_set_route(struct dcpib *dcp, bool enable) +{ + return 0; +} + +/* + * The sink's EDID through the AP AUX controller aux1: the phy takes its + * source from aux1 for the read (its AUX reaches the PS190, which reads the + * monitor's DDC), then from the DCP again. The display is off meanwhile. + */ +static int hdmi_read_edid(struct dcpib *dcp, u8 *buf, size_t size) +{ + struct dcpib_hdmi *hdmi = dcp->phy_priv; + u32 src; + int ret; + + if (!hdmi->aux.regs) + return -ENODEV; + ret = dcpib_aux_init(dcp, &hdmi->aux); + if (ret) + return ret; + src = readl(hdmi->apb + HDMI_SOURCE_SELECT); + writel((src & ~0xf) | HDMI_SOURCE_AUX1, hdmi->apb + HDMI_SOURCE_SELECT); + ret = dcpib_aux_read_edid(dcp, &hdmi->aux, buf, size); + writel(src, hdmi->apb + HDMI_SOURCE_SELECT); + return ret; +} + +static const struct dcpib_phy_ops hdmi_ops = { + .activate = hdmi_activate, + .deactivate = hdmi_deactivate, + .set_link_rate = hdmi_set_link_rate, + .set_lane_count = hdmi_set_lane_count, + .set_drive = hdmi_set_drive, + .set_route = hdmi_set_route, + .read_edid = hdmi_read_edid, + .max_link_rate = DCPIB_LINK_RATE_HBR3, + /* display-crossbar0 DFP endpoint of the lpdptx slot */ +}; + +/* The DCP node that describes the HDMI phy (DCPEXT0's). */ +struct device_node *dcpib_hdmi_node(void) +{ + struct device_node *np; + + for_each_compatible_node(np, NULL, "apple,t8152-dcp-iboot") + if (of_property_read_bool(np, "apple,hdmi")) + return np; + return NULL; +} + +static void __iomem *hdmi_map(struct device *dev, struct device_node *np, + const char *name) +{ + int index = of_property_match_string(np, "reg-names", name); + + if (index < 0) + return IOMEM_ERR_PTR(index); + return devm_of_iomap(dev, np, index, NULL); +} + +/* + * The DCP the bootloader wired to the phy (aura writes its DCP index: 0 for + * the main DCP on the HDMI-only route, 1 for DCPEXT0 next to a USB-C + * display), or -ENODEV if it left the PLL unlocked. + */ +int dcpib_hdmi_source(struct device_node *np) +{ + int core = of_property_match_string(np, "reg-names", "phy-core"); + int apb = of_property_match_string(np, "reg-names", "phy-apb"); + void __iomem *core_regs, *apb_regs; + int ret = -ENODEV; + + if (core < 0 || apb < 0) + return -ENODEV; + core_regs = of_iomap(np, core); + apb_regs = of_iomap(np, apb); + if (core_regs && apb_regs && readl(core_regs + 0x438) & BIT(30)) + ret = readl(apb_regs + HDMI_SOURCE_SELECT) & 0xf; + if (apb_regs) + iounmap(apb_regs); + if (core_regs) + iounmap(core_regs); + return ret; +} + +/* The phy, its AUX controller and its HPD line come from np. */ +int dcpib_hdmi_init(struct dcpib *dcp, struct device_node *np) +{ + struct dcpib_hdmi *hdmi; + + hdmi = devm_kzalloc(dcp->dev, sizeof(*hdmi), GFP_KERNEL); + if (!hdmi) + return -ENOMEM; + hdmi->core = hdmi_map(dcp->dev, np, "phy-core"); + if (IS_ERR(hdmi->core)) + return PTR_ERR(hdmi->core); + hdmi->apb = hdmi_map(dcp->dev, np, "phy-apb"); + if (IS_ERR(hdmi->apb)) + return PTR_ERR(hdmi->apb); + + /* AUX output select 0x10: the lpdptx phy */ + dcpib_aux_probe_node(dcp, &hdmi->aux, 0x10, np); + + dev_info(dcp->dev, "HDMI: inherited source select %u, PLL %slocked\n", + readl(hdmi->apb + HDMI_SOURCE_SELECT) & 0xf, + readl(hdmi->core + 0x438) & BIT(30) ? "" : "not "); + + dcp->phy = &hdmi_ops; + dcp->phy_priv = hdmi; + /* display-crossbar0 DFP endpoint lpdptx */ + dcp->dcp_output = 4; + return 0; +} diff --git a/drivers/gpu/drm/apple_dcpib/iboot.c b/drivers/gpu/drm/apple_dcpib/iboot.c new file mode 100644 index 0000000000000..b6a0b0bf32a03 --- /dev/null +++ b/drivers/gpu/drm/apple_dcpib/iboot.c @@ -0,0 +1,243 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +/* The DCP "disp0-service" iBoot interface (port of aura src/dcp_iboot.c). */ + +#include +#include + +#include "dcpib.h" + +#define IBOOT_SET_SURFACE 1 +#define IBOOT_SET_POWER 2 +#define IBOOT_GET_HPD 3 +#define IBOOT_GET_TIMING_MODES 4 +#define IBOOT_GET_COLOR_MODES 5 +#define IBOOT_SET_MODE 6 + +#define IBOOT_BUF_SIZE 0x4000 + +#define FMT_BGRA 1 +#define ADDR_PLANAR 1 +#define EOTF_GAMMA_SDR 1 + +struct iboot_txcmd { + __le32 op; + __le32 len; + __le32 unk1; + __le32 unk2; +} __packed; + +struct iboot_rxcmd { + __le32 op; + __le32 len; +} __packed; + +struct iboot_hpd { + u8 hpd; + u8 pad[3]; + __le32 timing_cnt; + __le32 color_cnt; +} __packed; + +struct iboot_plane { + __le32 unk1; + __le64 addr; + __le32 tile_size; + __le32 stride; + __le32 unk2[4]; + __le32 addr_format; + __le32 unk3; +} __packed; + +struct iboot_layer { + struct iboot_plane planes[3]; + __le32 unk; + __le32 plane_cnt; + __le32 width; + __le32 height; + __le32 surface_fmt; + __le32 colorspace; + __le32 eotf; + u8 transform; + u8 padding[3]; +} __packed; + +static int dcpib_ib_disp0_init(struct dcpib *dcp) +{ + struct dcpib_ep *ep = dcp->iboot_ep; + + for (unsigned int i = 0; i < ep->nr_services; i++) { + if (!strcmp(ep->services[i].name, "disp0-service") || + !strcmp(ep->services[i].name, "dispext-service")) { + dcp->iboot = &ep->services[i]; + return 0; + } + } + return -ENODEV; +} + +int dcpib_iboot_init(struct dcpib *dcp) +{ + struct dcpib_ep *ep = dcpib_ep_start(dcp, DCPIB_EP_IBOOT, NULL, 1); + + if (IS_ERR(ep)) + return PTR_ERR(ep); + dcp->iboot_ep = ep; + return dcpib_ib_disp0_init(dcp); +} + +/* + * The AFKEPV2 setters answer with transport status alone: supplying a reply + * buffer makes the firmware try to copy an empty message and assert. + */ +static int dcpib_ib_cmd(struct dcpib *dcp, u32 op, const void *in, + size_t in_size, void *out, size_t out_cap, + size_t min_size) +{ + struct iboot_txcmd *tx; + struct iboot_rxcmd *rx = NULL; + bool setter = op == IBOOT_SET_POWER || op == IBOOT_SET_MODE || + op == IBOOT_SET_SURFACE; + size_t rxlen = IBOOT_BUF_SIZE; + int ret; + + if (!dcp->iboot) + return -ENODEV; + tx = kzalloc(sizeof(*tx) + in_size, GFP_KERNEL); + if (!tx) + return -ENOMEM; + tx->op = cpu_to_le32(op); + tx->len = cpu_to_le32(sizeof(*tx) + in_size); + if (in_size) + memcpy(tx + 1, in, in_size); + + if (!setter) { + rx = kzalloc(rxlen, GFP_KERNEL); + if (!rx) { + ret = -ENOMEM; + goto out; + } + } + ret = dcpib_command(dcp->iboot, 0xc0, tx, sizeof(*tx) + in_size, rx, + setter ? NULL : &rxlen); + if (ret || setter) + goto out; + + if (rxlen < sizeof(*rx) || le32_to_cpu(rx->len) < sizeof(*rx) || + le32_to_cpu(rx->len) > rxlen) { + dev_err(dcp->dev, "iboot: bad response length for op %u\n", op); + ret = -EPROTO; + goto out; + } + rxlen = le32_to_cpu(rx->len) - sizeof(*rx); + if (rxlen < min_size) { + ret = -EPROTO; + goto out; + } + memcpy(out, rx + 1, min(rxlen, out_cap)); +out: + kfree(rx); + kfree(tx); + return ret; +} + +int dcpib_ib_set_power(struct dcpib *dcp, bool on) +{ + __le32 power = cpu_to_le32(on); + + /* The command carries one byte of payload. */ + return dcpib_ib_cmd(dcp, IBOOT_SET_POWER, &power, 1, NULL, 0, 0); +} + +int dcpib_ib_get_hpd(struct dcpib *dcp, u32 *timings, u32 *colors) +{ + struct iboot_hpd hpd; + int ret = dcpib_ib_cmd(dcp, IBOOT_GET_HPD, NULL, 0, &hpd, sizeof(hpd), + sizeof(hpd)); + + if (ret) + return ret; + if (timings) + *timings = le32_to_cpu(hpd.timing_cnt); + if (colors) + *colors = le32_to_cpu(hpd.color_cnt); + return !!hpd.hpd; +} + +static int dcpib_ib_get_list(struct dcpib *dcp, u32 op, size_t entry, + void **list, unsigned int *count) +{ + u8 *buf; + u32 n; + int ret; + + buf = kzalloc(IBOOT_BUF_SIZE, GFP_KERNEL); + if (!buf) + return -ENOMEM; + ret = dcpib_ib_cmd(dcp, op, NULL, 0, buf, IBOOT_BUF_SIZE, sizeof(__le32)); + if (ret) + goto out; + /* The response holds a count followed by the entries. */ + n = le32_to_cpu(*(__le32 *)buf); + if (n > (IBOOT_BUF_SIZE - sizeof(__le32)) / entry) { + ret = -EPROTO; + goto out; + } + kfree(*list); + *list = kmemdup(buf + sizeof(__le32), n * entry, GFP_KERNEL); + *count = *list ? n : 0; + ret = *list ? 0 : -ENOMEM; +out: + kfree(buf); + return ret; +} + +int dcpib_ib_get_modes(struct dcpib *dcp) +{ + int ret; + + ret = dcpib_ib_get_list(dcp, IBOOT_GET_TIMING_MODES, + sizeof(struct dcpib_timing_mode), + (void **)&dcp->tmodes, &dcp->nr_tmodes); + if (ret) + return ret; + return dcpib_ib_get_list(dcp, IBOOT_GET_COLOR_MODES, + sizeof(struct dcpib_color_mode), + (void **)&dcp->cmodes, &dcp->nr_cmodes); +} + +int dcpib_ib_set_mode(struct dcpib *dcp, const struct dcpib_timing_mode *t, + const struct dcpib_color_mode *c) +{ + dev_dbg(dcp->dev, "SET_MODE %ux%u fps %#x, bpp %u\n", + le32_to_cpu(t->width), le32_to_cpu(t->height), + le32_to_cpu(t->fps), le32_to_cpu(c->bpp)); + struct { + struct dcpib_timing_mode t; + struct dcpib_color_mode c; + } __packed cmd = { *t, *c }; + + return dcpib_ib_cmd(dcp, IBOOT_SET_MODE, &cmd, sizeof(cmd), NULL, 0, 0); +} + +int dcpib_ib_set_surface(struct dcpib *dcp, u64 iova, u32 width, u32 height, + u32 stride) +{ + dev_dbg(dcp->dev, "SET_SURFACE %#llx %ux%u stride %u\n", iova, width, + height, stride); + struct iboot_layer layer = { + .planes = { { + .addr = cpu_to_le64(iova), + .stride = cpu_to_le32(stride), + .addr_format = cpu_to_le32(ADDR_PLANAR), + } }, + .plane_cnt = cpu_to_le32(1), + .width = cpu_to_le32(width), + .height = cpu_to_le32(height), + .surface_fmt = cpu_to_le32(FMT_BGRA), + .colorspace = cpu_to_le32(2), + .eotf = cpu_to_le32(EOTF_GAMMA_SDR), + }; + + return dcpib_ib_cmd(dcp, IBOOT_SET_SURFACE, &layer, sizeof(layer), + NULL, 0, 0); +} diff --git a/drivers/gpu/drm/apple_dcpib/kms.c b/drivers/gpu/drm/apple_dcpib/kms.c new file mode 100644 index 0000000000000..788f384016e33 --- /dev/null +++ b/drivers/gpu/drm/apple_dcpib/kms.c @@ -0,0 +1,833 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +/* + * KMS on the iBoot interface: one primary plane, CRTC, encoder and + * connector. The firmware only takes width, height and refresh rate for a + * mode and picks the timing itself, so the modes are built with CVT for + * userspace. + * + * The pipe always scans out one buffer, the boot surface, which holds the + * largest mode; framebuffers are shadow buffers copied into it, and a + * modeset only changes its geometry, as aura does. Changing the surface + * address between Linux buffers made later modesets crash the firmware + * (IOMFB axi_rd_err). There is no vblank interrupt. + */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dcpib.h" + +/* + * Swap between the scanout buffers per frame. Off: frames are copied into + * the buffer on screen, which tears during motion. + */ +static bool buffered = true; +module_param(buffered, bool, 0644); +MODULE_PARM_DESC(buffered, "Copy each frame into a scanout buffer that is not on screen (default: on)"); + +static const u32 dcpib_formats[] = { + DRM_FORMAT_XRGB8888, + DRM_FORMAT_ARGB8888, +}; + +static int dcpib_refresh(const struct dcpib_timing_mode *t) +{ + return (le32_to_cpu(t->fps) + 0x8000) >> 16; +} + +/* + * Whether @t works: the link at the PHY's highest rate must carry it (24 bpp + * plus about 12% blanking against rate x lanes x 0.8 for 8b/10b), and the + * pixel rate must stay within what the PHY's path scans out; a mode beyond + * either only blanks the display. + */ +static bool dcpib_mode_fits(struct dcpib *dcp, const struct dcpib_timing_mode *t) +{ + /* The link's lanes even while the display is off and it is down. */ + u32 lanes = dcp->port[0].trained_lanes ?: 2; + u64 link = (u64)dcp->phy->max_link_rate * 270000000ULL * lanes * 8 / 10; + u64 pixels = (u64)le32_to_cpu(t->width) * le32_to_cpu(t->height) * + dcpib_refresh(t); + + if (pixels < dcp->phy->min_pixel_rate || + (dcp->phy->max_pixel_rate && pixels > dcp->phy->max_pixel_rate)) + return false; + return pixels * 24 * 112 / 100 <= link; +} + +/* + * The firmware mode for @mode: same size, refresh rate within 1 Hz (EDID's + * 59.94 against the firmware's 60.00), the nearest one. + */ +static const struct dcpib_timing_mode * +dcpib_find_mode(struct dcpib *dcp, const struct drm_display_mode *mode) +{ + const struct dcpib_timing_mode *best = NULL; + int refresh = drm_mode_vrefresh(mode); + + for (unsigned int i = 0; i < dcp->nr_tmodes; i++) { + const struct dcpib_timing_mode *t = &dcp->tmodes[i]; + + if (!t->valid || le32_to_cpu(t->width) != mode->hdisplay || + le32_to_cpu(t->height) != mode->vdisplay || + abs(dcpib_refresh(t) - refresh) > 1 || + !dcpib_mode_fits(dcp, t)) + continue; + if (!best || abs(dcpib_refresh(t) - refresh) < + abs(dcpib_refresh(best) - refresh)) + best = t; + } + return best; +} + +/* + * aura's display_choose_color_mode: valid, at most 32 bpp, the highest + * depth, then the lowest colorimetry, encoding and EOTF. Deeper modes the + * HDMI converter offers are refused by SET_MODE. + */ +static bool dcpib_color_better(const struct dcpib_color_mode *a, + const struct dcpib_color_mode *b) +{ + u32 abpp = le32_to_cpu(a->bpp), bbpp = le32_to_cpu(b->bpp); + + if (!!a->valid != !!b->valid) + return a->valid; + if ((abpp <= 32) != (bbpp <= 32)) + return abpp <= 32; + if (abpp != bbpp) + return abpp > bbpp; + if (a->colorimetry != b->colorimetry) + return le32_to_cpu(a->colorimetry) < le32_to_cpu(b->colorimetry); + if (a->encoding != b->encoding) + return le32_to_cpu(a->encoding) < le32_to_cpu(b->encoding); + return le32_to_cpu(a->eotf) < le32_to_cpu(b->eotf); +} + +static const struct dcpib_color_mode *dcpib_find_color(struct dcpib *dcp) +{ + const struct dcpib_color_mode *best = NULL; + + for (unsigned int i = 0; i < dcp->nr_cmodes; i++) + if (!best || dcpib_color_better(&dcp->cmodes[i], best)) + best = &dcp->cmodes[i]; + return best && best->valid ? best : NULL; +} + +/* + * The largest mode from 50 to 60 Hz for the console, up to 1920x1080 on + * USB-C: on the 2-lane HBR link of the J873g USB-C monitor 1920x1080@60 + * works, while SET_MODE 1920x1200@60 crashes the firmware. Low refresh + * modes (2560x1440@30) are left to userspace. + */ +static const struct dcpib_timing_mode *dcpib_preferred(struct dcpib *dcp) +{ + const struct dcpib_timing_mode *best = NULL; + + for (unsigned int i = 0; i < dcp->nr_tmodes; i++) { + const struct dcpib_timing_mode *t = &dcp->tmodes[i]; + + /* + * Byte 0 bit 6 marks standard modes a converter adds on its own + * (the J873g PS190 lists 3840x2160 for a 2560x1440 monitor), as + * aura's "max" mode skips them. + */ + if (!t->valid || dcpib_refresh(t) > 60 || dcpib_refresh(t) < 50 || + (t->pad[0] & 0x40) || + (!dcp->hdmi && (le32_to_cpu(t->width) > 1920 || + le32_to_cpu(t->height) > 1080)) || + !dcpib_mode_fits(dcp, t)) + continue; + if (!best || (u64)le32_to_cpu(t->width) * le32_to_cpu(t->height) > + (u64)le32_to_cpu(best->width) * le32_to_cpu(best->height) || + (t->width == best->width && t->height == best->height && + le32_to_cpu(t->fps) > le32_to_cpu(best->fps))) + best = t; + } + return best; +} + +/* + * The sink's EDID modes the firmware also has (mode_valid drops the rest), + * the largest of them preferred: firmware modes the sink does not list, + * such as 1400x1050 on the J873g EV2780, stay black. + */ +static int dcpib_edid_get_modes(struct drm_connector *connector) +{ + struct dcpib *dcp = to_dcpib(connector->dev); + struct drm_display_mode *mode, *best = NULL; + int count = drm_edid_connector_add_modes(connector); + + mutex_lock(&dcp->lock); + list_for_each_entry(mode, &connector->probed_modes, head) { + mode->type &= ~DRM_MODE_TYPE_PREFERRED; + if (!dcpib_find_mode(dcp, mode)) + continue; + if (!best || mode->hdisplay * mode->vdisplay > + best->hdisplay * best->vdisplay || + (mode->hdisplay * mode->vdisplay == + best->hdisplay * best->vdisplay && + drm_mode_vrefresh(mode) > drm_mode_vrefresh(best))) + best = mode; + } + if (best) + best->type |= DRM_MODE_TYPE_PREFERRED; + mutex_unlock(&dcp->lock); + return count; +} + +static int dcpib_connector_get_modes(struct drm_connector *connector) +{ + struct dcpib *dcp = to_dcpib(connector->dev); + const struct dcpib_timing_mode *preferred; + int count = 0; + + drm_edid_connector_update(connector, dcp->edid); + if (dcp->edid) + return dcpib_edid_get_modes(connector); + + /* Without an EDID, the firmware's list. */ + mutex_lock(&dcp->lock); + preferred = dcpib_preferred(dcp); + for (unsigned int i = 0; i < dcp->nr_tmodes; i++) { + const struct dcpib_timing_mode *t = &dcp->tmodes[i]; + struct drm_display_mode *mode; + + if (!t->valid || !dcpib_mode_fits(dcp, t)) + continue; + mode = drm_cvt_mode(connector->dev, le32_to_cpu(t->width), + le32_to_cpu(t->height), dcpib_refresh(t), + false, false, false); + if (!mode) + continue; + if (t == preferred) + mode->type |= DRM_MODE_TYPE_PREFERRED; + drm_mode_probed_add(connector, mode); + count++; + } + mutex_unlock(&dcp->lock); + return count; +} + +static enum drm_mode_status +dcpib_connector_mode_valid(struct drm_connector *connector, + const struct drm_display_mode *mode) +{ + struct dcpib *dcp = to_dcpib(connector->dev); + + return dcpib_find_mode(dcp, mode) ? MODE_OK : MODE_NOMODE; +} + +static const struct drm_connector_helper_funcs dcpib_connector_helper = { + .get_modes = dcpib_connector_get_modes, + .mode_valid = dcpib_connector_mode_valid, +}; + +static int dcpib_set_mode_locked(struct dcpib *dcp, + const struct dcpib_timing_mode *t, + const struct dcpib_color_mode *c); + +/* Read the sink's EDID through the phy, if it can, and keep it. */ +void dcpib_update_edid(struct dcpib *dcp) +{ + const struct drm_edid *edid = NULL; + u8 *buf; + int len; + + if (!dcp->phy->read_edid) + return; + buf = kzalloc(4 * EDID_LENGTH, GFP_KERNEL); + if (!buf) + return; + len = dcp->phy->read_edid(dcp, buf, 4 * EDID_LENGTH); + if (len >= EDID_LENGTH) { + edid = drm_edid_alloc(buf, len); + if (edid && !drm_edid_valid(edid)) { + drm_edid_free(edid); + edid = NULL; + } + } + if (!edid) + dev_warn(dcp->dev, "no EDID: %d\n", len); + else + dev_info(dcp->dev, "EDID: %d bytes\n", len); + drm_edid_free(dcp->edid); + dcp->edid = edid; + kfree(buf); +} + +static enum drm_connector_status +dcpib_connector_detect(struct drm_connector *connector, bool force) +{ + struct dcpib *dcp = to_dcpib(connector->dev); + bool flipped = false; + int hpd; + + if (dcp->hpd_gpio) + hpd = gpiod_get_value_cansleep(dcp->hpd_gpio); + else if (dcp->phy->detect) + hpd = dcp->phy->detect(dcp, &flipped); + else + return dcp->nr_tmodes ? connector_status_connected : + connector_status_disconnected; + if (hpd < 0) + return connector_status_unknown; + if (hpd != dcp->hpd) { + dev_info(dcp->dev, "sink %s\n", hpd ? "plugged" : "unplugged"); + dcp->hpd = hpd; + mutex_lock(&dcp->lock); + /* + * USB-C: the DPTX port's HPD follows the PD controller, and a + * new plug may come the other way round. + */ + if (dcp->pd) { + /* + * A plug while the display stayed on (unplug and plug + * between two polls): start the PHY cold again, the + * new plug may come the other way round. + */ + if (hpd && dcp->powered) + dcp->phy->deactivate(dcp); + /* The sink may be on another port now. */ + if (hpd && dcp->phy->select) + dcp->phy->select(dcp); + if (hpd && dcp->phy->set_orientation) + dcp->phy->set_orientation(dcp, flipped); + /* The firmware talks AUX as soon as it sees HPD. */ + if (hpd && dcp->phy->activate(dcp)) + dev_err(dcp->dev, "phy activation failed\n"); + if (dcpib_dptx_set_hpd(dcp, 0, hpd)) + dev_err(dcp->dev, "DPTX HPD update failed\n"); + } + /* A new sink: the firmware reads its EDID, take the new lists. */ + if (hpd && (dcpib_wait_hpd(dcp) || dcpib_ib_get_modes(dcp))) + dev_err(dcp->dev, "no modes from the new sink\n"); + /* + * The EDID once the firmware has read it, so the sink's DDC + * answers, and before SET_MODE: reading it borrows the AUX path + * while the firmware does no AUX. + */ + if (hpd) + dcpib_update_edid(dcp); + /* The unplug dropped the link; SET_MODE trains it again. */ + if (hpd && dcp->powered && (dcp->mode_set || dcp->mode_failed)) { + struct dcpib_timing_mode t = dcp->cur_mode; + const struct dcpib_color_mode *c = dcpib_find_color(dcp); + + if (c) + dcpib_set_mode_locked(dcp, &t, c); + } + mutex_unlock(&dcp->lock); + } + return hpd && dcp->nr_tmodes ? connector_status_connected : + connector_status_disconnected; +} + +static const struct drm_connector_funcs dcpib_connector_funcs = { + .detect = dcpib_connector_detect, + .fill_modes = drm_helper_probe_single_connector_modes, + .destroy = drm_connector_cleanup, + .reset = drm_atomic_helper_connector_reset, + .atomic_duplicate_state = drm_atomic_helper_connector_duplicate_state, + .atomic_destroy_state = drm_atomic_helper_connector_destroy_state, +}; + +static int dcpib_plane_atomic_check(struct drm_plane *plane, + struct drm_atomic_state *state) +{ + struct drm_plane_state *new = drm_atomic_get_new_plane_state(state, plane); + struct drm_crtc_state *crtc_state = NULL; + + if (new->crtc) + crtc_state = drm_atomic_get_new_crtc_state(state, new->crtc); + /* The surface fills the whole mode; no scaling or positioning. */ + return drm_atomic_helper_check_plane_state(new, crtc_state, + DRM_PLANE_NO_SCALING, + DRM_PLANE_NO_SCALING, + false, false); +} + +static void dcpib_rect_grow(struct drm_rect *r, const struct drm_rect *add) +{ + if (!drm_rect_visible(r)) { + *r = *add; + return; + } + r->x1 = min(r->x1, add->x1); + r->y1 = min(r->y1, add->y1); + r->x2 = max(r->x2, add->x2); + r->y2 = max(r->y2, add->y2); +} + +/* The current mode's refresh period, from its exact 16.16 rate. */ +static s64 dcpib_period_ns(struct dcpib *dcp) +{ + u32 fps = le32_to_cpu(dcp->cur_mode.fps); + + if (fps < (24 << 16)) + fps = 60 << 16; + return div64_u64((u64)NSEC_PER_SEC << 16, fps); +} + +/* Show scan[buf]. Called with dcp->lock held. */ +static void dcpib_swap_locked(struct dcpib *dcp, unsigned int buf, u32 w, + u32 h, u32 pitch) +{ + int ret; + + /* The pipe must see the whole frame before it is swapped in. */ + wmb(); + ret = dcpib_ib_set_surface(dcp, dcp->scan_dva[buf], w, h, pitch); + if (ret) { + /* Still on the old buffer, which misses this frame. */ + dev_err_ratelimited(dcp->dev, "swap failed: %d\n", ret); + dcp->stale[dcp->front] = DRM_RECT_INIT(0, 0, INT_MAX / 2, INT_MAX / 2); + return; + } + dcp->last_swap = ktime_get(); + dcp->retired[dcp->front] = dcp->last_swap; + dcp->front = buf; +} + +static void dcpib_swap_work(struct work_struct *work) +{ + struct dcpib *dcp = container_of(work, struct dcpib, swap_work); + + mutex_lock(&dcp->lock); + if (dcp->pending >= 0) { + dcpib_swap_locked(dcp, dcp->pending, dcp->pending_w, + dcp->pending_h, dcp->pending_pitch); + dcp->pending = -1; + } + mutex_unlock(&dcp->lock); +} + +static enum hrtimer_restart dcpib_swap_timer(struct hrtimer *t) +{ + struct dcpib *dcp = container_of(t, struct dcpib, swap_timer); + + queue_work(system_highpri_wq, &dcp->swap_work); + return HRTIMER_NORESTART; +} + +/* + * Copy the frame into a scanout buffer that is not on screen and show it. + * That buffer already holds an older frame: copy this frame's damage and + * what changed since that buffer was last written. + * + * SET_SURFACE returns before the pipe takes the buffer at its next frame + * start, and there is no vblank interrupt. Several swaps within one refresh + * let an older frame stay on screen (a cursor that jumps back), so swaps are + * spaced one refresh apart: a frame that comes sooner waits in a pending + * buffer, which a newer frame overwrites, and a timer swaps it in. The + * commit itself never waits, so a client's next frame is not held up. + */ +static void dcpib_plane_atomic_update(struct drm_plane *plane, + struct drm_atomic_state *state) +{ + struct drm_plane_state *old = drm_atomic_get_old_plane_state(state, plane); + struct drm_plane_state *new = drm_atomic_get_new_plane_state(state, plane); + struct drm_shadow_plane_state *shadow = to_drm_shadow_plane_state(new); + struct dcpib *dcp = to_dcpib(plane->dev); + struct drm_framebuffer *fb = new->fb; + struct drm_atomic_helper_damage_iter iter; + struct drm_rect clip, damage = { 0, 0, 0, 0 }; + struct drm_rect full = DRM_RECT_INIT(0, 0, fb ? fb->width : 0, + fb ? fb->height : 0); + unsigned int pitch, back; + struct iosys_map dst; + s64 period, since; + + if (!fb || !new->visible) + return; + pitch = (new->src_w >> 16) * 4; + drm_atomic_helper_damage_iter_init(&iter, old, new); + drm_atomic_for_each_plane_damage(&iter, &clip) + dcpib_rect_grow(&damage, &clip); + if (!drm_rect_visible(&damage)) + return; + + mutex_lock(&dcp->lock); + period = dcpib_period_ns(dcp); + if (!READ_ONCE(buffered)) { + /* Into the buffer on screen (tears during motion). */ + back = dcp->front; + } else if (dcp->pending >= 0) { + back = dcp->pending; + } else { + /* The other buffer that left the screen first. */ + unsigned int a = (dcp->front + 1) % DCPIB_SCANOUT_BUFS; + unsigned int b = (dcp->front + 2) % DCPIB_SCANOUT_BUFS; + s64 wait; + + back = ktime_before(dcp->retired[a], dcp->retired[b]) ? a : b; + /* Off screen for a full refresh: only after a mode change. */ + wait = period - ktime_to_ns(ktime_sub(ktime_get(), dcp->retired[back])); + if (dcp->mode_set && wait > 0) + usleep_range(div_s64(wait, NSEC_PER_USEC), + div_s64(wait, NSEC_PER_USEC) + 100); + } + clip = dcp->stale[back]; + dcpib_rect_grow(&clip, &damage); + if (drm_rect_intersect(&clip, &full)) { + iosys_map_set_vaddr(&dst, dcp->scan_wc[back]); + iosys_map_incr(&dst, drm_fb_clip_offset(pitch, fb->format, &clip)); + drm_fb_memcpy(&dst, &pitch, shadow->data, fb, &clip); + } + dcp->stale[back] = (struct drm_rect){ 0, 0, 0, 0 }; + for (unsigned int i = 0; i < DCPIB_SCANOUT_BUFS; i++) + if (i != back) + dcpib_rect_grow(&dcp->stale[i], &damage); + if (back == dcp->front) + goto out; + + since = ktime_to_ns(ktime_sub(ktime_get(), dcp->last_swap)); + if (dcp->pending < 0 && (!dcp->mode_set || since >= period)) { + dcpib_swap_locked(dcp, back, new->src_w >> 16, new->src_h >> 16, + pitch); + goto out; + } + dcp->pending_w = new->src_w >> 16; + dcp->pending_h = new->src_h >> 16; + dcp->pending_pitch = pitch; + if (dcp->pending < 0) { + dcp->pending = back; + hrtimer_start(&dcp->swap_timer, + ktime_add_ns(dcp->last_swap, period), + HRTIMER_MODE_ABS); + } +out: + mutex_unlock(&dcp->lock); +} + +static const struct drm_plane_helper_funcs dcpib_plane_helper = { + DRM_GEM_SHADOW_PLANE_HELPER_FUNCS, + .atomic_check = dcpib_plane_atomic_check, + .atomic_update = dcpib_plane_atomic_update, +}; + +static const struct drm_plane_funcs dcpib_plane_funcs = { + .update_plane = drm_atomic_helper_update_plane, + .disable_plane = drm_atomic_helper_disable_plane, + .destroy = drm_plane_cleanup, + DRM_GEM_SHADOW_PLANE_FUNCS, +}; + +static int dcpib_crtc_atomic_check(struct drm_crtc *crtc, + struct drm_atomic_state *state) +{ + struct drm_crtc_state *new = drm_atomic_get_new_crtc_state(state, crtc); + struct dcpib *dcp = to_dcpib(crtc->dev); + + /* No vblank interrupt: the helpers complete events after the flush. */ + new->no_vblank = true; + if (new->enable && !dcpib_find_mode(dcp, &new->mode)) + return -EINVAL; + return 0; +} + +/* SET_MODE, then the scanout buffer with the new mode's geometry. */ +static int dcpib_set_mode_locked(struct dcpib *dcp, + const struct dcpib_timing_mode *t, + const struct dcpib_color_mode *c) +{ + int ret; + + /* + * Right after power on the firmware still trains the link and fails + * SET_MODE with kIOReturnError; it takes the mode once it is up. + */ + for (int i = 0; i < 5; i++) { + ret = dcpib_ib_set_mode(dcp, t, c); + if (ret != -EIO) + break; + msleep(200); + } + if (ret) { + dev_err(dcp->dev, "set_mode failed: %d\n", ret); + /* + * USB-C: a sink that no longer answers AUX (seen after the + * monitor's deep sleep) comes back with a fresh DP alt mode + * entry; the plug that follows sets the mode again. At most + * once a minute, so a dead sink does not loop. + */ + if (dcp->pd && time_after(jiffies, dcp->pd_reset_time + 60 * HZ)) { + dcp->cur_mode = *t; + dcp->mode_failed = true; + dcp->pd_reset_time = jiffies; + schedule_work(&dcp->pd_reset_work); + } + return ret; + } + dcp->cur_mode = *t; + dcp->mode_set = true; + dcp->mode_failed = false; + /* Both buffers hold the old geometry: the next frame is copied whole. */ + for (unsigned int i = 0; i < DCPIB_SCANOUT_BUFS; i++) + dcp->stale[i] = DRM_RECT_INIT(0, 0, INT_MAX / 2, INT_MAX / 2); + /* A pending frame has the old geometry. */ + dcp->pending = -1; + /* As aura swaps after a modeset. */ + ret = dcpib_ib_set_surface(dcp, dcp->scan_dva[dcp->front], le32_to_cpu(t->width), + le32_to_cpu(t->height), + le32_to_cpu(t->width) * 4); + if (ret) + dev_err(dcp->dev, "set_surface failed: %d\n", ret); + return ret; +} + +/* + * The boot mode again: a display powered on without SET_MODE counts as idle + * to the firmware's AV stack, which powers the link off after 30 s. + */ +int dcpib_set_boot_mode(struct dcpib *dcp) +{ + const struct dcpib_timing_mode *best = NULL; + const struct dcpib_color_mode *c; + int ret = -ENOENT; + + mutex_lock(&dcp->lock); + for (unsigned int i = 0; i < dcp->nr_tmodes; i++) { + const struct dcpib_timing_mode *t = &dcp->tmodes[i]; + + if (!t->valid || le32_to_cpu(t->width) != dcp->boot_w || + le32_to_cpu(t->height) != dcp->boot_h || + dcpib_refresh(t) > 60) + continue; + if (!best || le32_to_cpu(t->fps) > le32_to_cpu(best->fps)) + best = t; + } + c = dcpib_find_color(dcp); + if (best && c) + ret = dcpib_set_mode_locked(dcp, best, c); + mutex_unlock(&dcp->lock); + return ret; +} + +static void dcpib_crtc_atomic_enable(struct drm_crtc *crtc, + struct drm_atomic_state *state) +{ + struct drm_crtc_state *new = drm_atomic_get_new_crtc_state(state, crtc); + struct dcpib *dcp = to_dcpib(crtc->dev); + const struct dcpib_timing_mode *t; + const struct dcpib_color_mode *c; + int ret; + + mutex_lock(&dcp->lock); + t = dcpib_find_mode(dcp, &new->mode); + c = dcpib_find_color(dcp); + if (!t || !c) + goto out; + if (!dcp->powered) { + /* The phy starts cold first; the firmware then trains. */ + ret = dcp->phy->activate(dcp); + if (ret) + dev_err(dcp->dev, "phy activation failed: %d\n", ret); + /* A pipe powered on without a surface fetches from a null base. */ + ret = dcpib_ib_set_surface(dcp, dcp->scan_dva[dcp->front], dcp->boot_w, + dcp->boot_h, dcp->boot_stride); + if (ret) + dev_err(dcp->dev, "set_surface failed: %d\n", ret); + ret = dcpib_ib_set_power(dcp, true); + if (ret) + dev_err(dcp->dev, "set_power failed: %d\n", ret); + dcp->powered = !ret; + } + if (dcp->mode_set && !memcmp(t, &dcp->cur_mode, sizeof(*t))) + goto out; + dev_info(dcp->dev, "modeset %ux%u@%d\n", le32_to_cpu(t->width), + le32_to_cpu(t->height), dcpib_refresh(t)); + /* + * As aura's display_configure: query the sink and both mode lists + * again before SET_MODE, and look the mode up in the fresh list. The + * DPTX port stays connected; reconnecting it here drops the link and + * the HDMI firmware then fails SET_MODE. + */ + ret = dcpib_ib_set_power(dcp, true); + if (!ret) + ret = dcpib_ib_get_hpd(dcp, NULL, NULL) < 0; + if (!ret) + ret = dcpib_ib_get_modes(dcp); + if (ret) + dev_err(dcp->dev, "refreshing the modes failed: %d\n", ret); + t = dcpib_find_mode(dcp, &new->mode); + c = dcpib_find_color(dcp); + if (!t || !c) + goto out; + dcpib_set_mode_locked(dcp, t, c); +out: + mutex_unlock(&dcp->lock); +} + +static void dcpib_crtc_atomic_disable(struct drm_crtc *crtc, + struct drm_atomic_state *state) +{ + struct drm_crtc_state *new = drm_atomic_get_new_crtc_state(state, crtc); + struct dcpib *dcp = to_dcpib(crtc->dev); + int ret; + + /* A mode change: SET_MODE alone switches, as before. */ + if (new->active) + return; + /* + * Display off (DPMS, blanking): the firmware stops the pipe and the + * monitor goes to sleep. The next enable sets the mode again. + */ + mutex_lock(&dcp->lock); + if (dcp->powered) { + ret = dcpib_ib_set_power(dcp, false); + if (ret) { + dev_err(dcp->dev, "set_power off failed: %d\n", ret); + } else { + dev_info(dcp->dev, "display off\n"); + /* Nothing scans out: power the phy down as well. */ + if (!dcp->phy->deactivate(dcp)) + dcp->port[0].link_rate = 0; + } + dcp->powered = !!ret; + dcp->mode_set = false; + } + mutex_unlock(&dcp->lock); +} + +static const struct drm_crtc_helper_funcs dcpib_crtc_helper = { + .atomic_check = dcpib_crtc_atomic_check, + .atomic_enable = dcpib_crtc_atomic_enable, + .atomic_disable = dcpib_crtc_atomic_disable, +}; + +static const struct drm_crtc_funcs dcpib_crtc_funcs = { + .reset = drm_atomic_helper_crtc_reset, + .destroy = drm_crtc_cleanup, + .set_config = drm_atomic_helper_set_config, + .page_flip = drm_atomic_helper_page_flip, + .atomic_duplicate_state = drm_atomic_helper_crtc_duplicate_state, + .atomic_destroy_state = drm_atomic_helper_crtc_destroy_state, +}; + +static const struct drm_encoder_funcs dcpib_encoder_funcs = { + .destroy = drm_encoder_cleanup, +}; + +/* Modeset before the plane update, which copies at the new geometry. */ +static const struct drm_mode_config_helper_funcs dcpib_mode_config_helpers = { + .atomic_commit_tail = drm_atomic_helper_commit_tail_rpm, +}; + +static const struct drm_mode_config_funcs dcpib_mode_config_funcs = { + /* fbdev writes reach the scanout buffer through the dirty damage */ + .fb_create = drm_gem_fb_create_with_dirty, + .atomic_check = drm_atomic_helper_check, + .atomic_commit = drm_atomic_helper_commit, +}; + +static void dcpib_pd_reset_work(struct work_struct *work) +{ + struct dcpib *dcp = container_of(work, struct dcpib, pd_reset_work); + int ret; + + dev_info(dcp->dev, "USB-C sink does not answer, entering DP alt mode again\n"); + ret = dcpib_pd_reconnect(dcp); + if (ret) { + dev_err(dcp->dev, "DP alt mode entry failed: %d\n", ret); + return; + } + /* The controller unplugged and plugged: run the plug path. */ + mutex_lock(&dcp->lock); + dcp->hpd = 0; + mutex_unlock(&dcp->lock); + drm_kms_helper_hotplug_event(&dcp->drm); +} + +static void dcpib_swap_stop(void *data) +{ + struct dcpib *dcp = data; + + hrtimer_cancel(&dcp->swap_timer); + cancel_work_sync(&dcp->swap_work); +} + +int dcpib_kms_init(struct dcpib *dcp) +{ + struct drm_device *drm = &dcp->drm; + u32 max_w = 1920, max_h = 1080; + int ret; + + dcp->pending = -1; + INIT_WORK(&dcp->swap_work, dcpib_swap_work); + hrtimer_setup(&dcp->swap_timer, dcpib_swap_timer, CLOCK_MONOTONIC, + HRTIMER_MODE_ABS); + ret = devm_add_action_or_reset(dcp->dev, dcpib_swap_stop, dcp); + if (ret) + return ret; + + for (unsigned int i = 0; i < dcp->nr_tmodes; i++) { + max_w = max(max_w, le32_to_cpu(dcp->tmodes[i].width)); + max_h = max(max_h, le32_to_cpu(dcp->tmodes[i].height)); + } + + ret = drmm_mode_config_init(drm); + if (ret) + return ret; + INIT_WORK(&dcp->pd_reset_work, dcpib_pd_reset_work); + dcp->pd_reset_time = jiffies - 61 * HZ; + + drm->mode_config.min_width = 320; + drm->mode_config.min_height = 240; + drm->mode_config.max_width = max_w; + drm->mode_config.max_height = max_h; + drm->mode_config.preferred_depth = 24; + drm->mode_config.funcs = &dcpib_mode_config_funcs; + drm->mode_config.helper_private = &dcpib_mode_config_helpers; + + ret = drm_universal_plane_init(drm, &dcp->plane, 0, &dcpib_plane_funcs, + dcpib_formats, ARRAY_SIZE(dcpib_formats), + NULL, DRM_PLANE_TYPE_PRIMARY, NULL); + if (ret) + return ret; + drm_plane_helper_add(&dcp->plane, &dcpib_plane_helper); + drm_plane_enable_fb_damage_clips(&dcp->plane); + + ret = drm_crtc_init_with_planes(drm, &dcp->crtc, &dcp->plane, NULL, + &dcpib_crtc_funcs, NULL); + if (ret) + return ret; + drm_crtc_helper_add(&dcp->crtc, &dcpib_crtc_helper); + + ret = drm_encoder_init(drm, &dcp->encoder, &dcpib_encoder_funcs, + DRM_MODE_ENCODER_TMDS, NULL); + if (ret) + return ret; + dcp->encoder.possible_crtcs = drm_crtc_mask(&dcp->crtc); + + ret = drm_connector_init(drm, &dcp->connector, &dcpib_connector_funcs, + dcp->hdmi ? DRM_MODE_CONNECTOR_HDMIA : + DRM_MODE_CONNECTOR_DisplayPort); + if (ret) + return ret; + drm_connector_helper_add(&dcp->connector, &dcpib_connector_helper); + if (dcp->hpd_gpio || dcp->pd) + dcp->connector.polled = DRM_CONNECTOR_POLL_CONNECT | + DRM_CONNECTOR_POLL_DISCONNECT; + ret = drm_connector_attach_encoder(&dcp->connector, &dcp->encoder); + if (ret) + return ret; + + drm_mode_config_reset(drm); + return 0; +} diff --git a/drivers/gpu/drm/apple_dcpib/pd.c b/drivers/gpu/drm/apple_dcpib/pd.c new file mode 100644 index 0000000000000..84ea367feb1cb --- /dev/null +++ b/drivers/gpu/drm/apple_dcpib/pd.c @@ -0,0 +1,176 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +/* + * USB-C plug state from the port's ACE3 PD controller (J873g hpm3) over + * SPMI, for the USB-C display's hotplug: the DP alt mode connection and the + * plug orientation. Logical registers are read through a window (aura + * src/ace3.c): select with a register-zero write, wait for the busy bit to + * clear, read the size at 0x1f and the data from 0x20. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "dcpib.h" + +#define ACE3_SELECT 0x00 +#define ACE3_SELECT_BUSY BIT(7) +#define ACE3_SIZE 0x1f +#define ACE3_DATA 0x20 +#define ACE3_BURST 16 + +#define ACE3_REG_CMD1 0x08 +#define ACE3_REG_DATA1 0x09 +#define ACE3_REG_STATUS 0x1a +#define ACE3_REG_DATA_STATUS 0x5f + +#define STATUS_PLUG_PRESENT BIT(0) +#define STATUS_UPSIDE_DOWN BIT(4) +#define DATA_DP_CONNECTION BIT(8) +#define DATA_HPD_LEVEL BIT(15) /* CD321x, as tipd */ + +static int ace3_read(struct spmi_device *sdev, u8 reg, u8 *buf, size_t len) +{ + u8 sel, size; + int ret, err = 0; + + ret = spmi_register_zero_write(sdev, reg); + if (ret) + return ret; + ret = read_poll_timeout(spmi_ext_register_read, err, + err || !(sel & ACE3_SELECT_BUSY), 50, 20000, + false, sdev, ACE3_SELECT, &sel, 1); + if (ret || err) + return ret ?: err; + ret = spmi_ext_register_read(sdev, ACE3_SIZE, &size, 1); + if (ret) + return ret; + if (size < len) + return -EIO; + for (size_t done = 0; done < len; done += ACE3_BURST) { + ret = spmi_ext_register_read(sdev, ACE3_DATA + done, buf + done, + min_t(size_t, len - done, ACE3_BURST)); + if (ret) + return ret; + } + return 0; +} + +static int ace3_select(struct spmi_device *sdev, u8 reg) +{ + u8 sel; + int ret, err = 0; + + ret = spmi_register_zero_write(sdev, reg); + if (ret) + return ret; + ret = read_poll_timeout(spmi_ext_register_read, err, + err || !(sel & ACE3_SELECT_BUSY), 50, 20000, + false, sdev, ACE3_SELECT, &sel, 1); + return ret ?: err; +} + +static int ace3_write(struct spmi_device *sdev, u8 reg, const u8 *buf, + size_t len) +{ + int ret = ace3_select(sdev, reg); + + return ret ?: spmi_ext_register_write(sdev, ACE3_DATA, (u8 *)buf, len); +} + +/* A 4CC command with an optional data byte; 0 once the controller took it. */ +static int ace3_cmd(struct spmi_device *sdev, const char *cmd, int data) +{ + u8 val[4]; + int ret; + + ret = spmi_command_wakeup(sdev); + if (ret) + return ret; + if (data >= 0) { + u8 d = data; + + ret = ace3_write(sdev, ACE3_REG_DATA1, &d, 1); + if (ret) + return ret; + } + ret = ace3_write(sdev, ACE3_REG_CMD1, cmd, 4); + if (ret) + return ret; + for (int i = 0; i < 1000; i++) { + ret = ace3_read(sdev, ACE3_REG_CMD1, val, sizeof(val)); + if (ret) + return ret; + if (!memcmp(val, "!CMD", 4)) + return -EIO; + if (!memcmp(val, "\0\0\0\0", 4)) + return 0; + usleep_range(1000, 2000); + } + return -ETIMEDOUT; +} + +/* + * Leave and enter DP alt mode again, as unplugging and plugging the cable + * does: reset the controller (Gaid), then power the port up again once it + * takes commands (SSPS 0, as aura). A sink that stopped answering AUX after + * a monitor's deep sleep comes back this way. + */ +int dcpib_pd_reconnect(struct dcpib *dcp) +{ + int ret; + + /* Gaid resets the controller before it can report completion. */ + ret = spmi_command_wakeup(dcp->pd); + if (!ret) + ret = ace3_write(dcp->pd, ACE3_REG_CMD1, "Gaid", 4); + if (ret) + return ret; + for (int i = 0; i < 60; i++) { + msleep(500); + ret = ace3_cmd(dcp->pd, "SSPS", 0); + if (!ret) + return 0; + } + return ret; +} + +/* 1 with a DisplayPort sink in DP alt mode and HPD high, 0 without. */ +int dcpib_pd_sink(struct spmi_device *pd, bool *flipped) +{ + u8 status[4], data[4]; + int ret; + + /* Without a plug the controller sleeps and ignores selects. */ + ret = spmi_command_wakeup(pd); + if (ret) + return ret; + ret = ace3_read(pd, ACE3_REG_STATUS, status, sizeof(status)); + if (!ret) + ret = ace3_read(pd, ACE3_REG_DATA_STATUS, data, sizeof(data)); + if (ret) + return ret; + *flipped = status[0] & STATUS_UPSIDE_DOWN; + /* The sink answers AUX once it raised HPD, not at alt mode entry. */ + return (status[0] & STATUS_PLUG_PRESENT) && + (get_unaligned_le32(data) & DATA_DP_CONNECTION) && + (get_unaligned_le32(data) & DATA_HPD_LEVEL); +} + +/* The PD controller @np's apple,usb-pd points at, NULL without one. */ +struct spmi_device *dcpib_pd_get(struct dcpib *dcp, struct device_node *np) +{ + struct device_node *pd_np = of_parse_phandle(np, "apple,usb-pd", 0); + struct spmi_device *pd; + + if (!pd_np) + return NULL; + /* Holds a reference for the lifetime of the (unbindable) device. */ + pd = spmi_find_device_by_of_node(pd_np); + of_node_put(pd_np); + return pd ?: ERR_PTR(-EPROBE_DEFER); +} diff --git a/drivers/iommu/apple-dart.c b/drivers/iommu/apple-dart.c index 1234b4a771e7a..2cfe618e4c279 100644 --- a/drivers/iommu/apple-dart.c +++ b/drivers/iommu/apple-dart.c @@ -46,6 +46,9 @@ #include #include +#include +#include + #include "dma-iommu.h" #if IS_ENABLED(CONFIG_APPLE_DART_KUNIT_TEST) @@ -221,6 +224,8 @@ struct apple_dart; struct apple_dart_hw { enum dart_type type; + /* T8110 register layout, SPTM-owned TLB invalidation (T8152) */ + bool gen3; irqreturn_t (*irq_handler)(int irq, void *dev); int (*invalidate_tlb)(struct apple_dart_stream_map *stream_map); void (*enable_remap)(struct apple_dart *dart, int sid, int remap_to); @@ -346,6 +351,10 @@ struct apple_dart { /* Serialize owned TCR publication against IRQ quarantine. */ spinlock_t managed_lock; + /* Gen3: shared TLBI aperture and this DART's route in it */ + void __iomem *tlbi; + u8 tlbi_route; + DECLARE_BITMAP(remapped, DART_MAX_STREAMS); u8 remap_target[DART_MAX_STREAMS]; @@ -1503,6 +1512,60 @@ static int apple_dart_fw_invalidate(struct apple_dart_stream_map *stream_map, return 0; } +#define DART_GEN3_TLBI_ROUTE 0x4604 +#define DART_GEN3_TLBI_VALID BIT_ULL(60) +#define DART_GEN3_TLBI_ROUTE_FIELD GENMASK_ULL(59, 56) +#define DART_GEN3_TLBI_SID GENMASK_ULL(55, 48) + +/* + * Gen3 DARTs (T8152) are owned by the SPTM, and their TLB command register at + * 0x80 answers translation queries instead of flushing. A stream is + * invalidated the way iBoot does it: write a 128-bit payload (valid, route, + * SID) to the shared TLBI aperture, then broadcast a TLBI, which the DART + * snoops and applies to that stream. + */ +static int +apple_dart_gen3_hw_invalidate_tlb(struct apple_dart_stream_map *stream_map) +{ + struct apple_dart *dart = stream_map->dart; + unsigned long flags; + int sid; + + u64 hcr = 0; + + spin_lock_irqsave(&dart->lock, flags); + /* + * The DART snoops an EL1&0 TLBI, as iBoot and aura (EL2 without VHE) + * issue it. A VHE kernel's TLBI VALE1OS targets EL2&0 while + * HCR_EL2.TGE is set, so clear TGE around it, as KVM does to reach + * the EL1&0 regime. + */ + if (is_kernel_in_hyp_mode()) { + hcr = read_sysreg(hcr_el2); + write_sysreg(hcr & ~HCR_TGE, hcr_el2); + isb(); + } + for_each_set_bit(sid, stream_map->sidmap, dart->num_streams) { + u64 lo = DART_GEN3_TLBI_VALID | + FIELD_PREP(DART_GEN3_TLBI_ROUTE_FIELD, dart->tlbi_route) | + FIELD_PREP(DART_GEN3_TLBI_SID, sid); + + dma_wmb(); + asm volatile("stp %0, xzr, [%1]\n" + "dsb ish\n" + ".inst 0xd50881bf\n" /* tlbi vale1os, xzr */ + "dsb osh\n" + : : "r"(lo), "r"(dart->tlbi) : "memory"); + } + if (is_kernel_in_hyp_mode()) { + write_sysreg(hcr, hcr_el2); + isb(); + } + spin_unlock_irqrestore(&dart->lock, flags); + + return 0; +} + static int apple_dart_t8020_hw_invalidate_tlb(struct apple_dart_stream_map *stream_map) { @@ -2110,7 +2173,10 @@ static int apple_dart_domain_flush_tlb_range(struct apple_dart_domain *domain, /* The range registers take the DVA the device issues. */ start = first + domain->dma_offset; end = last + domain->dma_offset; - if (range && (stream.dart->fw_mirror || stream.dart->fw_handoff) && + /* Gen3 has no range command; invalidate the complete stream. */ + if (stream.dart->hw->gen3) + ret = stream.dart->hw->invalidate_tlb(&stream); + else if (range && (stream.dart->fw_mirror || stream.dart->fw_handoff) && stream.dart->version >= 0x0202) ret = apple_dart_fw_invalidate(&stream, start, end); else if (range && stream.dart->locked && stream.dart->version >= 0x0202) @@ -4289,6 +4355,13 @@ static int apple_dart_probe(struct platform_device *pdev) * not admitted: never enable that level IRQ for one block alone. */ dart->irq = -1; + } else if (dart->hw->gen3) { + /* + * The T8110 fault handler acknowledges through the TLB command + * register, which a Gen3 DART uses for translation queries. + * Leave its faults to the firmware for now. + */ + dart->irq = -1; } else if (dart->preserve_display || j713_radio) { dart->irq = platform_get_irq_optional(pdev, 0); if (dart->irq == -ENXIO) @@ -4600,8 +4673,34 @@ static int apple_dart_probe(struct platform_device *pdev) goto err_clk_disable; } + if (dart->hw->gen3) { + struct resource *tlbi; + + /* Only an iBoot-provisioned (locked) Gen3 stream is supported. */ + if (!dart->locked) { + dev_err(dev, "Gen3 DART is not locked; cold setup is not supported\n"); + ret = -ENODEV; + goto err_clk_disable; + } + /* The aperture is shared by every Gen3 DART: map, do not claim. */ + tlbi = platform_get_resource_byname(pdev, IORESOURCE_MEM, "tlbi"); + if (!tlbi || resource_size(tlbi) < 16) { + dev_err(dev, "Gen3 DART needs its TLBI aperture\n"); + ret = -EINVAL; + goto err_clk_disable; + } + dart->tlbi = devm_ioremap(dev, tlbi->start, resource_size(tlbi)); + if (!dart->tlbi) { + ret = -ENOMEM; + goto err_clk_disable; + } + dart->tlbi_route = apple_dart_readl(dart, DART_GEN3_TLBI_ROUTE) & 0xf; + dev_info(dev, "Gen3 DART, version %#x, TLBI route %u\n", + dart->version, dart->tlbi_route); + } + /* Borrowed display state: never acknowledge someone else's fault. */ - if (dart->locked && !dart->preserve_display) + if (dart->locked && !dart->preserve_display && !dart->hw->gen3) apple_dart_drain_stale_exceptions(dart); /* Refuse an already pending inherited fault before joining the shared IRQ. */ @@ -4939,6 +5038,33 @@ static const struct apple_dart_hw apple_dart_hw_t6000 = { .ttbr_count = 4, }; +static const struct apple_dart_hw apple_dart_hw_gen3 = { + .type = DART_T8110, + .gen3 = true, + .irq_handler = apple_dart_t8110_irq, + .invalidate_tlb = apple_dart_gen3_hw_invalidate_tlb, + .fmt = APPLE_DART2, + .max_sid_count = 256, + + .enable_streams = DART_T8110_ENABLE_STREAMS, + .lock = DART_T8110_PROTECT, + .lock_bit = DART_T8110_PROTECT_TTBR_TCR, + + .error = DART_T8110_ERROR, + + .tcr = DART_T8110_TCR, + .tcr_enabled = DART_T8110_TCR_TRANSLATE_ENABLE, + .tcr_disabled = 0, + .tcr_bypass = DART_T8110_TCR_BYPASS_DAPF | DART_T8110_TCR_BYPASS_DART, + .tcr_4level = DART_T8110_TCR_FOUR_LEVEL, + + .ttbr = DART_T8110_TTBR, + .ttbr_valid = DART_T8110_TTBR_VALID, + .ttbr_addr_field_shift = DART_T8110_TTBR_ADDR_FIELD_SHIFT, + .ttbr_shift = DART_T8110_TTBR_SHIFT, + .ttbr_count = 1, +}; + static const struct apple_dart_hw apple_dart_hw_t8110 = { .type = DART_T8110, .irq_handler = apple_dart_t8110_irq, @@ -5202,6 +5328,7 @@ static __maybe_unused int apple_dart_resume(struct device *dev) static DEFINE_RUNTIME_DEV_PM_OPS(apple_dart_pm_ops, apple_dart_suspend, apple_dart_resume, NULL); static const struct of_device_id apple_dart_of_match[] = { + { .compatible = "apple,t8152-dart-gen3", .data = &apple_dart_hw_gen3 }, { .compatible = "apple,t8103-dart", .data = &apple_dart_hw_t8103 }, { .compatible = "apple,t8103-usb4-dart", .data = &apple_dart_hw_t8103_usb4 }, { .compatible = "apple,t8110-dart", .data = &apple_dart_hw_t8110 }, diff --git a/drivers/soc/apple/rtkit.c b/drivers/soc/apple/rtkit.c index 536ba515f826f..c968e4847e996 100644 --- a/drivers/soc/apple/rtkit.c +++ b/drivers/soc/apple/rtkit.c @@ -1222,6 +1222,19 @@ bool apple_rtkit_has_endpoint(struct apple_rtkit *rtk, u8 ep) } EXPORT_SYMBOL_GPL(apple_rtkit_has_endpoint); +/* + * A co-processor adopted from the bootloader has already sent its endpoint + * map. A client that starts one of its application endpoints by hand + * registers it here, so its messages are not reported as undiscovered. + */ +void apple_rtkit_adopt_ep(struct apple_rtkit *rtk, u8 ep) +{ + if (ep >= APPLE_RTKIT_APP_ENDPOINT_START && + ep < APPLE_RTKIT_MAX_ENDPOINTS) + set_bit(ep, rtk->endpoints); +} +EXPORT_SYMBOL_GPL(apple_rtkit_adopt_ep); + int apple_rtkit_start_ep(struct apple_rtkit *rtk, u8 endpoint) { u64 msg; diff --git a/include/linux/soc/apple/rtkit.h b/include/linux/soc/apple/rtkit.h index 35508001fbea4..ac08b80c30042 100644 --- a/include/linux/soc/apple/rtkit.h +++ b/include/linux/soc/apple/rtkit.h @@ -311,4 +311,14 @@ void apple_rtkit_flush_rx(struct apple_rtkit *rtk); */ bool apple_rtkit_has_endpoint(struct apple_rtkit *rtk, u8 ep); +/* + * Registers an application endpoint the client started itself on a + * co-processor whose endpoint map the bootloader consumed, so that its + * messages are not reported as undiscovered + * + * @rtk: RTKit reference + * @ep: endpoint to register + */ +void apple_rtkit_adopt_ep(struct apple_rtkit *rtk, u8 ep); + #endif /* _LINUX_APPLE_RTKIT_H_ */ From de833a104f3e1dc083e4a25696c8b8169bd959c9 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Thu, 5 Nov 2026 07:24:40 +0100 Subject: [PATCH 10/17] drm/apple_dcpib: re-send SET_MODE when the DCPAV video interface starts The DCPAV service starts its idle power off timer when its video interface starts; a powered-on display without a following SET_MODE counts as idle and the firmware takes the link down 30 s later. At boot the probe's SET_MODE comes before the interface is (re)announced, so HDMI went dark 30 s after boot while sway still reported the output active. Re-send SET_MODE for the current mode a second after a dcpav-video-interface opens, from a delayed work on system_wq: dcp->wq is ordered and runs the AFK receive work that completes the SET_MODE reply. Signed-off-by: Thomas Glanzmann (cherry picked from commit b075f67d11eb15e2bd37fad1f7f858896ee1e952) --- drivers/gpu/drm/apple_dcpib/afk.c | 9 ++++++++- drivers/gpu/drm/apple_dcpib/dcpib.h | 5 +++++ drivers/gpu/drm/apple_dcpib/drv.c | 9 +++++++++ drivers/gpu/drm/apple_dcpib/kms.c | 23 +++++++++++++++++++++++ 4 files changed, 45 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/apple_dcpib/afk.c b/drivers/gpu/drm/apple_dcpib/afk.c index 01ca8668e44f6..ce450456f42b2 100644 --- a/drivers/gpu/drm/apple_dcpib/afk.c +++ b/drivers/gpu/drm/apple_dcpib/afk.c @@ -267,9 +267,16 @@ static void dcpib_announce(struct dcpib_ep *ep, u16 interface, dev_info(ep->dcp->dev, "AFK[%02x]: opening %s on interface %u\n", ep->ep, service->name, interface); - if (dcpib_tx_v2(ep, interface, &open, sizeof(open))) + if (dcpib_tx_v2(ep, interface, &open, sizeof(open))) { dev_err(ep->dcp->dev, "AFK[%02x]: failed to open %s\n", ep->ep, service->name); + } else if (strstr(service->name, "dcpav-video-interface")) { + /* + * The AV service powers the display on with this interface and + * starts its idle power off timer: SET_MODE again once it is up. + */ + dcpib_av_started(ep->dcp); + } if (ep->nr_services == ep->expected) complete_all(&ep->services_done); } diff --git a/drivers/gpu/drm/apple_dcpib/dcpib.h b/drivers/gpu/drm/apple_dcpib/dcpib.h index b7449e7a3b2f0..b5d97ec1d8387 100644 --- a/drivers/gpu/drm/apple_dcpib/dcpib.h +++ b/drivers/gpu/drm/apple_dcpib/dcpib.h @@ -256,6 +256,9 @@ struct dcpib { /* USB-C: re-enter DP alt mode when the sink stopped answering */ struct work_struct pd_reset_work; unsigned long pd_reset_time; + /* The DCPAV video interface started: SET_MODE again, or the + * firmware's idle power off timer takes the link down 30 s later. */ + struct delayed_work av_work; struct drm_plane plane; struct drm_crtc crtc; @@ -330,5 +333,7 @@ int dcpib_pd_reconnect(struct dcpib *dcp); int dcpib_kms_init(struct dcpib *dcp); int dcpib_set_boot_mode(struct dcpib *dcp); void dcpib_update_edid(struct dcpib *dcp); +void dcpib_reset_mode(struct dcpib *dcp); +void dcpib_av_started(struct dcpib *dcp); #endif diff --git a/drivers/gpu/drm/apple_dcpib/drv.c b/drivers/gpu/drm/apple_dcpib/drv.c index e6ad3e2fc2767..4a48081e884de 100644 --- a/drivers/gpu/drm/apple_dcpib/drv.c +++ b/drivers/gpu/drm/apple_dcpib/drv.c @@ -351,6 +351,14 @@ int dcpib_wait_hpd(struct dcpib *dcp) return hpd < 0 ? hpd : -ENODEV; } +static void dcpib_av_work(struct work_struct *work) +{ + struct dcpib *dcp = container_of(to_delayed_work(work), struct dcpib, + av_work); + + dcpib_reset_mode(dcp); +} + static int dcpib_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; @@ -378,6 +386,7 @@ static int dcpib_probe(struct platform_device *pdev) dcp->dev = dev; platform_set_drvdata(pdev, dcp); mutex_init(&dcp->lock); + INIT_DELAYED_WORK(&dcp->av_work, dcpib_av_work); of_property_read_u32(dev->of_node, "apple,die", &dcp->die); dcp->wq = devm_alloc_ordered_workqueue(dev, "dcpib", WQ_MEM_RECLAIM); diff --git a/drivers/gpu/drm/apple_dcpib/kms.c b/drivers/gpu/drm/apple_dcpib/kms.c index 788f384016e33..f90e0839e05b3 100644 --- a/drivers/gpu/drm/apple_dcpib/kms.c +++ b/drivers/gpu/drm/apple_dcpib/kms.c @@ -619,6 +619,29 @@ int dcpib_set_boot_mode(struct dcpib *dcp) return ret; } +/* + * The DCPAV service starts the firmware's idle power off timer when its video + * interface comes up: until a SET_MODE follows, it counts the powered-on + * display as idle and takes the link down 30 s later. The probe's SET_MODE + * comes before the interface starts, so one follows it here. + */ +void dcpib_reset_mode(struct dcpib *dcp) +{ + const struct dcpib_color_mode *c; + + mutex_lock(&dcp->lock); + c = dcpib_find_color(dcp); + if (c && dcp->powered && dcp->mode_set) + dcpib_set_mode_locked(dcp, &dcp->cur_mode, c); + mutex_unlock(&dcp->lock); +} + +void dcpib_av_started(struct dcpib *dcp) +{ + /* Let the firmware finish opening the interface before the SET_MODE. */ + queue_delayed_work(system_wq, &dcp->av_work, msecs_to_jiffies(1000)); +} + static void dcpib_crtc_atomic_enable(struct drm_crtc *crtc, struct drm_atomic_state *state) { From 0a97e05344126f63b9d7c2c13515dd58e3a4675a Mon Sep 17 00:00:00 2001 From: Acelogic Date: Fri, 6 Nov 2026 04:24:46 -0500 Subject: [PATCH 11/17] drm/asahi: use current G18 parameter namespace in capture-free path --- drivers/gpu/drm/asahi/g18/runtime/graphics_job.rs | 2 +- drivers/gpu/drm/asahi/g18/runtime/graphics_memory.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/gpu/drm/asahi/g18/runtime/graphics_job.rs b/drivers/gpu/drm/asahi/g18/runtime/graphics_job.rs index 6080cd3bf4b05..01952223a0593 100644 --- a/drivers/gpu/drm/asahi/g18/runtime/graphics_job.rs +++ b/drivers/gpu/drm/asahi/g18/runtime/graphics_job.rs @@ -289,7 +289,7 @@ impl G18GraphicsJob { /// Caller has allocated/retained PB and pool owners and filled all native /// fields. In particular a default resource word is not an admitted policy. #[inline(never)] - pub(crate) fn encode(&mut self, dev: &AsahiDrmDevice, inputs: &cmd::G18RenderCommandInputs) -> Result { + pub(crate) fn encode(&mut self, _dev: &AsahiDrmDevice, inputs: &cmd::G18RenderCommandInputs) -> Result { if self.encoded.is_some() || self.controls.ta.resource_word == 0 || self.controls.fragment.resource_word == 0 || inputs.context_id != u32::from(self.context_id()) diff --git a/drivers/gpu/drm/asahi/g18/runtime/graphics_memory.rs b/drivers/gpu/drm/asahi/g18/runtime/graphics_memory.rs index 10c9573166088..f07b7204b6ab7 100644 --- a/drivers/gpu/drm/asahi/g18/runtime/graphics_memory.rs +++ b/drivers/gpu/drm/asahi/g18/runtime/graphics_memory.rs @@ -36,7 +36,7 @@ impl G18GraphicsMemory { // is rebuilt from Mesa's parameters. It needs only the colour-a // page layout, which is mapped here with zeroed contents. Err(e) if e == ENOENT && profile == 1 - && *crate::module_parameters::g18_initdata_test.value() == 10 => + && *crate::g18::params::g18_initdata_test.value() == 10 => return Self::layout_only(dev, uat), Err(e) => return Err(e), }; From 04df2c3f29b620e5e3dda43762cb001da8ef5564 Mon Sep 17 00:00:00 2001 From: Acelogic Date: Fri, 6 Nov 2026 04:26:19 -0500 Subject: [PATCH 12/17] drm/asahi: read the G18 stage parameter by value --- drivers/gpu/drm/asahi/g18/runtime/graphics_memory.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/gpu/drm/asahi/g18/runtime/graphics_memory.rs b/drivers/gpu/drm/asahi/g18/runtime/graphics_memory.rs index f07b7204b6ab7..3175072b950e8 100644 --- a/drivers/gpu/drm/asahi/g18/runtime/graphics_memory.rs +++ b/drivers/gpu/drm/asahi/g18/runtime/graphics_memory.rs @@ -36,7 +36,7 @@ impl G18GraphicsMemory { // is rebuilt from Mesa's parameters. It needs only the colour-a // page layout, which is mapped here with zeroed contents. Err(e) if e == ENOENT && profile == 1 - && *crate::g18::params::g18_initdata_test.value() == 10 => + && crate::g18::params::g18_initdata_test.value() == 10 => return Self::layout_only(dev, uat), Err(e) => return Err(e), }; From aedad98884d8773e6e9c5540a3aaa5304538005d Mon Sep 17 00:00:00 2001 From: Acelogic Date: Fri, 6 Nov 2026 04:26:19 -0500 Subject: [PATCH 13/17] drm/apple-dcpib: retain adopted firmware resources on failed startup Use the current non-devm RTKit adoption API. Gate incoming endpoint work, drain senders, and detach RTKit while preserving live DMA buffers after a startup failure. Keep the failed device bound to prevent a retry over that session. The driver has no safe firmware shutdown yet, so omit module exit and retain the existing suppressed bind attributes. Initialize and drain the PD reset worker together with the flip workers, and defer AV mode repair until DRM registration. --- drivers/gpu/drm/apple_dcpib/dcpib.h | 7 ++++ drivers/gpu/drm/apple_dcpib/drv.c | 55 ++++++++++++++++++++++++++--- drivers/gpu/drm/apple_dcpib/kms.c | 14 ++++++-- 3 files changed, 68 insertions(+), 8 deletions(-) diff --git a/drivers/gpu/drm/apple_dcpib/dcpib.h b/drivers/gpu/drm/apple_dcpib/dcpib.h index b5d97ec1d8387..177ba148e97a3 100644 --- a/drivers/gpu/drm/apple_dcpib/dcpib.h +++ b/drivers/gpu/drm/apple_dcpib/dcpib.h @@ -180,6 +180,11 @@ struct dcpib_port { }; struct dcpib { + /* Stop new endpoint work before detaching a failed live session. */ + struct mutex rx_gate; + bool stopping; + bool ready; + bool kms_work_ready; struct device *dev; struct drm_device drm; struct apple_rtkit *rtk; @@ -287,6 +292,8 @@ void dcpib_dma_free(struct dcpib *dcp, struct dcpib_dma *dma); /* iboot.c */ int dcpib_iboot_init(struct dcpib *dcp); + +void dcpib_kms_stop(struct dcpib *dcp); int dcpib_ib_set_power(struct dcpib *dcp, bool on); int dcpib_ib_get_hpd(struct dcpib *dcp, u32 *timings, u32 *colors); int dcpib_ib_get_modes(struct dcpib *dcp); diff --git a/drivers/gpu/drm/apple_dcpib/drv.c b/drivers/gpu/drm/apple_dcpib/drv.c index 4a48081e884de..a2280ce875ef4 100644 --- a/drivers/gpu/drm/apple_dcpib/drv.c +++ b/drivers/gpu/drm/apple_dcpib/drv.c @@ -40,6 +40,9 @@ static void dcpib_rtk_recv(void *cookie, u8 ep, u64 message) struct dcpib *dcp = cookie; struct dcpib_ep *e = NULL; + mutex_lock(&dcp->rx_gate); + if (dcp->stopping) + goto out; if (ep >= DCPIB_FIRST_EP && ep < DCPIB_FIRST_EP + DCPIB_NR_EPS) e = dcp->eps[ep - DCPIB_FIRST_EP]; if (e) @@ -47,6 +50,8 @@ static void dcpib_rtk_recv(void *cookie, u8 ep, u64 message) else dev_dbg(dcp->dev, "message %#llx for idle endpoint %#x\n", message, ep); +out: + mutex_unlock(&dcp->rx_gate); } static void dcpib_rtk_crashed(void *cookie, const void *crashlog, size_t size) @@ -130,8 +135,8 @@ static const struct drm_driver dcpib_drm_driver = { */ static int dcpib_boot(struct dcpib *dcp) { - dcp->rtk = devm_apple_rtkit_init_adopted(dcp->dev, dcp, NULL, 0, - &dcpib_rtkit_ops); + dcp->rtk = apple_rtkit_init_adopted(dcp->dev, dcp, NULL, 0, + &dcpib_rtkit_ops); if (IS_ERR(dcp->rtk)) return PTR_ERR(dcp->rtk); return apple_rtkit_adopt_running(dcp->rtk); @@ -356,10 +361,11 @@ static void dcpib_av_work(struct work_struct *work) struct dcpib *dcp = container_of(to_delayed_work(work), struct dcpib, av_work); - dcpib_reset_mode(dcp); + if (READ_ONCE(dcp->ready)) + dcpib_reset_mode(dcp); } -static int dcpib_probe(struct platform_device *pdev) +static int dcpib_probe_inner(struct platform_device *pdev) { struct device *dev = &pdev->dev; struct device_node *hdmi_np __free(device_node) = dcpib_hdmi_node(); @@ -386,6 +392,7 @@ static int dcpib_probe(struct platform_device *pdev) dcp->dev = dev; platform_set_drvdata(pdev, dcp); mutex_init(&dcp->lock); + mutex_init(&dcp->rx_gate); INIT_DELAYED_WORK(&dcp->av_work, dcpib_av_work); of_property_read_u32(dev->of_node, "apple,die", &dcp->die); @@ -486,12 +493,45 @@ static int dcpib_probe(struct platform_device *pdev) ret = drm_dev_register(&dcp->drm, 0); if (ret) return ret; + WRITE_ONCE(dcp->ready, true); if (dcp->hpd_gpio || dcp->pd) drmm_kms_helper_poll_init(&dcp->drm); drm_client_setup(&dcp->drm, NULL); return 0; } +static int dcpib_probe(struct platform_device *pdev) +{ + struct dcpib *dcp; + int ret; + + platform_set_drvdata(pdev, NULL); + ret = dcpib_probe_inner(pdev); + dcp = platform_get_drvdata(pdev); + if (!ret || !dcp || IS_ERR_OR_NULL(dcp->rtk)) + return ret; + + /* + * Once adopted, the DCP may still DMA into our buffers on any error. + * Gate RX before draining endpoint work; keep RTKit available until + * every sender has stopped. Do not reclaim firmware-visible storage + * or let driver-core retry a partially started session. + */ + WRITE_ONCE(dcp->ready, false); + mutex_lock(&dcp->rx_gate); + dcp->stopping = true; + mutex_unlock(&dcp->rx_gate); + flush_workqueue(dcp->wq); + cancel_delayed_work_sync(&dcp->av_work); + dcpib_kms_stop(dcp); + apple_rtkit_free_retaining_buffers(dcp->rtk); + dcp->rtk = NULL; + dev_err(dcp->dev, + "initialization failed (%d); live DMA resources retained until reboot\n", + ret); + return 0; +} + static const struct of_device_id dcpib_of_match[] = { { .compatible = "apple,t8152-dcp-iboot" }, {} @@ -507,7 +547,12 @@ static struct platform_driver dcpib_platform_driver = { }, .probe = dcpib_probe, }; -module_platform_driver(dcpib_platform_driver); +/* There is no safe firmware shutdown/handoff yet; never unload this module. */ +static int __init dcpib_init(void) +{ + return platform_driver_register(&dcpib_platform_driver); +} +module_init(dcpib_init); MODULE_DESCRIPTION("Apple T8152 DCP display driver (iBoot interface)"); MODULE_LICENSE("Dual MIT/GPL"); diff --git a/drivers/gpu/drm/apple_dcpib/kms.c b/drivers/gpu/drm/apple_dcpib/kms.c index f90e0839e05b3..1d95925577120 100644 --- a/drivers/gpu/drm/apple_dcpib/kms.c +++ b/drivers/gpu/drm/apple_dcpib/kms.c @@ -777,12 +777,19 @@ static void dcpib_pd_reset_work(struct work_struct *work) drm_kms_helper_hotplug_event(&dcp->drm); } -static void dcpib_swap_stop(void *data) +void dcpib_kms_stop(struct dcpib *dcp) { - struct dcpib *dcp = data; + if (!dcp->kms_work_ready) + return; hrtimer_cancel(&dcp->swap_timer); cancel_work_sync(&dcp->swap_work); + cancel_work_sync(&dcp->pd_reset_work); +} + +static void dcpib_swap_stop(void *data) +{ + dcpib_kms_stop(data); } int dcpib_kms_init(struct dcpib *dcp) @@ -793,8 +800,10 @@ int dcpib_kms_init(struct dcpib *dcp) dcp->pending = -1; INIT_WORK(&dcp->swap_work, dcpib_swap_work); + INIT_WORK(&dcp->pd_reset_work, dcpib_pd_reset_work); hrtimer_setup(&dcp->swap_timer, dcpib_swap_timer, CLOCK_MONOTONIC, HRTIMER_MODE_ABS); + dcp->kms_work_ready = true; ret = devm_add_action_or_reset(dcp->dev, dcpib_swap_stop, dcp); if (ret) return ret; @@ -807,7 +816,6 @@ int dcpib_kms_init(struct dcpib *dcp) ret = drmm_mode_config_init(drm); if (ret) return ret; - INIT_WORK(&dcp->pd_reset_work, dcpib_pd_reset_work); dcp->pd_reset_time = jiffies - 61 * HZ; drm->mode_config.min_width = 320; From 3b2519311904c41aef7cdc134e592f39e50bb89b Mon Sep 17 00:00:00 2001 From: Acelogic Date: Fri, 6 Nov 2026 04:27:24 -0500 Subject: [PATCH 14/17] drm/apple-dcpib: use current DRM atomic commit callback types --- drivers/gpu/drm/apple_dcpib/kms.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/drivers/gpu/drm/apple_dcpib/kms.c b/drivers/gpu/drm/apple_dcpib/kms.c index 1d95925577120..e5a6746aba8f7 100644 --- a/drivers/gpu/drm/apple_dcpib/kms.c +++ b/drivers/gpu/drm/apple_dcpib/kms.c @@ -348,7 +348,7 @@ static const struct drm_connector_funcs dcpib_connector_funcs = { }; static int dcpib_plane_atomic_check(struct drm_plane *plane, - struct drm_atomic_state *state) + struct drm_atomic_commit *state) { struct drm_plane_state *new = drm_atomic_get_new_plane_state(state, plane); struct drm_crtc_state *crtc_state = NULL; @@ -438,7 +438,7 @@ static enum hrtimer_restart dcpib_swap_timer(struct hrtimer *t) * commit itself never waits, so a client's next frame is not held up. */ static void dcpib_plane_atomic_update(struct drm_plane *plane, - struct drm_atomic_state *state) + struct drm_atomic_commit *state) { struct drm_plane_state *old = drm_atomic_get_old_plane_state(state, plane); struct drm_plane_state *new = drm_atomic_get_new_plane_state(state, plane); @@ -529,7 +529,7 @@ static const struct drm_plane_funcs dcpib_plane_funcs = { }; static int dcpib_crtc_atomic_check(struct drm_crtc *crtc, - struct drm_atomic_state *state) + struct drm_atomic_commit *state) { struct drm_crtc_state *new = drm_atomic_get_new_crtc_state(state, crtc); struct dcpib *dcp = to_dcpib(crtc->dev); @@ -643,7 +643,7 @@ void dcpib_av_started(struct dcpib *dcp) } static void dcpib_crtc_atomic_enable(struct drm_crtc *crtc, - struct drm_atomic_state *state) + struct drm_atomic_commit *state) { struct drm_crtc_state *new = drm_atomic_get_new_crtc_state(state, crtc); struct dcpib *dcp = to_dcpib(crtc->dev); @@ -698,7 +698,7 @@ static void dcpib_crtc_atomic_enable(struct drm_crtc *crtc, } static void dcpib_crtc_atomic_disable(struct drm_crtc *crtc, - struct drm_atomic_state *state) + struct drm_atomic_commit *state) { struct drm_crtc_state *new = drm_atomic_get_new_crtc_state(state, crtc); struct dcpib *dcp = to_dcpib(crtc->dev); From b58ef0f15a6ef78135d0131ce21eeb24f0133f5e Mon Sep 17 00:00:00 2001 From: Acelogic Date: Fri, 6 Nov 2026 04:49:18 -0500 Subject: [PATCH 15/17] tests: keep the DART range fixture aligned with gen3 metadata --- tools/asahi/apple-dart-range-tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/asahi/apple-dart-range-tests.py b/tools/asahi/apple-dart-range-tests.py index 5d311fef0813d..7ffa652e204ae 100644 --- a/tools/asahi/apple-dart-range-tests.py +++ b/tools/asahi/apple-dart-range-tests.py @@ -42,7 +42,7 @@ def main(): #define io_pgtable_ops_to_pgtable(x) ((struct io_pgtable *)(x)) struct apple_dart; struct apple_dart_stream_map { struct apple_dart *dart; unsigned long sidmap[1]; }; -struct apple_dart_hw { int (*invalidate_tlb)(struct apple_dart_stream_map *); }; +struct apple_dart_hw { int (*invalidate_tlb)(struct apple_dart_stream_map *); bool gen3; }; struct apple_dart { int num_streams, locked, version; bool fw_mirror, fw_handoff, managed_sids; void *dev; struct apple_dart_hw *hw; }; struct apple_dart_atomic_stream_map { struct apple_dart *dart; unsigned long sidmap[1]; }; struct apple_dart_domain { From 241cdf60f1f3bfe8626d929e3238d85b966025cf Mon Sep 17 00:00:00 2001 From: Acelogic Date: Fri, 6 Nov 2026 05:00:57 -0500 Subject: [PATCH 16/17] drm/apple-dcpib: reject an unpowered HDMI PHY before register access A USB-C-only J873 boot leaves DPTX_PHY off. Reading its PLL register while discovering the HDMI source raises an SError before the main DCP can probe. Check the always-on PMGR requested and actual states before touching the PHY, and only adopt DCPEXT when a powered HDMI link selects it. Missing state resources fail closed. --- .../display/apple,t8152-dcp-iboot.yaml | 9 ++++--- arch/arm64/boot/dts/apple/t8152-j873-dcp.dtsi | 8 ++++-- drivers/gpu/drm/apple_dcpib/drv.c | 5 ++-- drivers/gpu/drm/apple_dcpib/hdmi.c | 26 +++++++++++++++++-- 4 files changed, 39 insertions(+), 9 deletions(-) diff --git a/Documentation/devicetree/bindings/display/apple,t8152-dcp-iboot.yaml b/Documentation/devicetree/bindings/display/apple,t8152-dcp-iboot.yaml index 37b14961bbe77..20b5b31e6b0a4 100644 --- a/Documentation/devicetree/bindings/display/apple,t8152-dcp-iboot.yaml +++ b/Documentation/devicetree/bindings/display/apple,t8152-dcp-iboot.yaml @@ -48,11 +48,14 @@ properties: - const: phy-apb - const: aux - const: aux-ps - minItems: 3 + - const: phy-ps + - const: coproc-ps description: - DCP coprocessor, the HDMI dptx-phy blocks, and optionally the AP + DCP coprocessor, the HDMI dptx-phy blocks, the AP DisplayPort AUX controller that reads the sink's EDID with its - PMGR power state + PMGR power state, and the read-only PMGR power states for the + HDMI PHY and DCPEXT CPU. The driver only adopts the bootloader's + powered link and must check these states before accessing the PHY. - items: - const: coproc - const: dp-clock diff --git a/arch/arm64/boot/dts/apple/t8152-j873-dcp.dtsi b/arch/arm64/boot/dts/apple/t8152-j873-dcp.dtsi index 9ab6bbb0591ab..7d46e3666d4b9 100644 --- a/arch/arm64/boot/dts/apple/t8152-j873-dcp.dtsi +++ b/arch/arm64/boot/dts/apple/t8152-j873-dcp.dtsi @@ -465,9 +465,13 @@ <0x3 0x1a740000 0x0 0x10000>, <0x3 0x1a700000 0x0 0x4000>, <0x3 0x1a764000 0x0 0x4000>, - <0x3 0x00700480 0x0 0x4>; + <0x3 0x00700480 0x0 0x4>, + <0x3 0x007016c0 0x0 0x4>, + <0x3 0x00700e00 0x0 0x4>; /* aux: ADT dptx1-aux, aux-ps: PMGR GLOBAL_AUX_CTRL1 */ - reg-names = "coproc", "phy-core", "phy-apb", "aux", "aux-ps"; + /* Read-only PMGR DPTX_PHY and DISPEXT0_CPU admission checks. */ + reg-names = "coproc", "phy-core", "phy-apb", "aux", "aux-ps", + "phy-ps", "coproc-ps"; mboxes = <&dcpext_mbox>; iommus = <&dcpext_dart 23>; dma-coherent; diff --git a/drivers/gpu/drm/apple_dcpib/drv.c b/drivers/gpu/drm/apple_dcpib/drv.c index a2280ce875ef4..f1ea13dd26963 100644 --- a/drivers/gpu/drm/apple_dcpib/drv.c +++ b/drivers/gpu/drm/apple_dcpib/drv.c @@ -377,8 +377,9 @@ static int dcpib_probe_inner(struct platform_device *pdev) * Without a USB-C display the bootloader drives HDMI from the main DCP * (source 0) and never starts DCPEXT0. */ - if (dev->of_node == hdmi_np && source == 0) { - dev_info(dev, "HDMI is on the main DCP\n"); + if (dev->of_node == hdmi_np && source != 1) { + dev_info(dev, "%s\n", source == 0 ? "HDMI is on the main DCP" : + "no bootloader HDMI link to adopt"); return -ENODEV; } diff --git a/drivers/gpu/drm/apple_dcpib/hdmi.c b/drivers/gpu/drm/apple_dcpib/hdmi.c index 36d7ee6135573..1a154b2c3c162 100644 --- a/drivers/gpu/drm/apple_dcpib/hdmi.c +++ b/drivers/gpu/drm/apple_dcpib/hdmi.c @@ -282,10 +282,27 @@ static void __iomem *hdmi_map(struct device *dev, struct device_node *np, return devm_of_iomap(dev, np, index, NULL); } +static bool hdmi_power_active(struct device_node *np, const char *name) +{ + int index = of_property_match_string(np, "reg-names", name); + void __iomem *regs; + u32 state; + + if (index < 0) + return false; + regs = of_iomap(np, index); + if (!regs) + return false; + state = readl(regs); + iounmap(regs); + /* Both the requested and actual PMGR states must be active. */ + return (state & GENMASK(7, 0)) == GENMASK(7, 0); +} + /* * The DCP the bootloader wired to the phy (aura writes its DCP index: 0 for * the main DCP on the HDMI-only route, 1 for DCPEXT0 next to a USB-C - * display), or -ENODEV if it left the PLL unlocked. + * display), or -ENODEV if it left the PHY off or the PLL unlocked. */ int dcpib_hdmi_source(struct device_node *np) { @@ -294,7 +311,8 @@ int dcpib_hdmi_source(struct device_node *np) void __iomem *core_regs, *apb_regs; int ret = -ENODEV; - if (core < 0 || apb < 0) + /* The PHY aperture aborts while gated, including on a USB-C-only boot. */ + if (core < 0 || apb < 0 || !hdmi_power_active(np, "phy-ps")) return -ENODEV; core_regs = of_iomap(np, core); apb_regs = of_iomap(np, apb); @@ -304,6 +322,10 @@ int dcpib_hdmi_source(struct device_node *np) iounmap(apb_regs); if (core_regs) iounmap(core_regs); + if (ret == 1 && !hdmi_power_active(np, "coproc-ps")) + return -ENODEV; + if (ret != 0 && ret != 1) + return -ENODEV; return ret; } From 79169729e5a38a81e7660aab6245ae6212dd92ea Mon Sep 17 00:00:00 2001 From: Acelogic Date: Fri, 6 Nov 2026 05:08:51 -0500 Subject: [PATCH 17/17] drm/apple_dcpib: stop advertised live AFK sessions before reinitializing --- .../display/apple,t8152-dcp-iboot.yaml | 11 +++++++++++ drivers/gpu/drm/apple_dcpib/afk.c | 18 +++++++++++++++++- drivers/gpu/drm/apple_dcpib/dcpib.h | 1 + drivers/gpu/drm/apple_dcpib/drv.c | 8 ++++++++ 4 files changed, 37 insertions(+), 1 deletion(-) diff --git a/Documentation/devicetree/bindings/display/apple,t8152-dcp-iboot.yaml b/Documentation/devicetree/bindings/display/apple,t8152-dcp-iboot.yaml index 20b5b31e6b0a4..f1396f44990db 100644 --- a/Documentation/devicetree/bindings/display/apple,t8152-dcp-iboot.yaml +++ b/Documentation/devicetree/bindings/display/apple,t8152-dcp-iboot.yaml @@ -22,6 +22,17 @@ properties: compatible: const: apple,t8152-dcp-iboot + apple,afk-active-endpoints: + $ref: /schemas/types.yaml#/definitions/uint32 + maximum: 65535 + description: + Runtime handoff from the bootloader. Bit N records that AFK endpoint + 0x20 + N is running and must acknowledge shutdown before a new session + starts. A zero bit means stopped. The bootloader inserts this only + for controllers whose complete endpoint state it owns, and reserves + the old session's buffers. The driver refuses takeover if absent. + This property must not be hard-coded in a board device tree. + reg: minItems: 2 maxItems: 7 diff --git a/drivers/gpu/drm/apple_dcpib/afk.c b/drivers/gpu/drm/apple_dcpib/afk.c index ce450456f42b2..036272171079a 100644 --- a/drivers/gpu/drm/apple_dcpib/afk.c +++ b/drivers/gpu/drm/apple_dcpib/afk.c @@ -595,6 +595,23 @@ struct dcpib_ep *dcpib_ep_start(struct dcpib *dcp, u8 epnum, goto err; dcp->eps[epnum - DCPIB_FIRST_EP] = ep; + apple_rtkit_adopt_ep(dcp->rtk, epnum); + /* Aura leaves these sessions running with its scanout. INIT while the + * messenger is active asserts in firmware. Stop only an explicitly + * advertised live endpoint and require its acknowledgment before INIT. + * Its old buffers remain reserved by the bootloader throughout. */ + if (dcp->inherited_afk_endpoints & BIT(epnum - DCPIB_FIRST_EP)) { + ret = dcpib_send(ep, FIELD_PREP(RBEP_TYPE, RBEP_SHUTDOWN)); + if (ret) + goto err_unregister; + if (!wait_for_completion_timeout(&ep->stopped, DCPIB_TIMEOUT)) { + dev_err(dcp->dev, "inherited endpoint %#x did not stop\n", epnum); + ret = -ETIMEDOUT; + goto err_unregister; + } + dcp->inherited_afk_endpoints &= ~BIT(epnum - DCPIB_FIRST_EP); + dev_info(dcp->dev, "inherited endpoint %#x stopped\n", epnum); + } /* * START_EP for an application endpoint of the adopted session; the * bootloader consumed the endpoint map, so apple_rtkit_start_ep() @@ -608,7 +625,6 @@ struct dcpib_ep *dcpib_ep_start(struct dcpib *dcp, u8 epnum, dev_err(dcp->dev, "failed to start endpoint %#x: %d\n", epnum, ret); goto err_unregister; } - apple_rtkit_adopt_ep(dcp->rtk, epnum); ret = dcpib_send(ep, FIELD_PREP(RBEP_TYPE, RBEP_INIT)); if (ret) goto err_unregister; diff --git a/drivers/gpu/drm/apple_dcpib/dcpib.h b/drivers/gpu/drm/apple_dcpib/dcpib.h index 177ba148e97a3..1ee609e4233d0 100644 --- a/drivers/gpu/drm/apple_dcpib/dcpib.h +++ b/drivers/gpu/drm/apple_dcpib/dcpib.h @@ -189,6 +189,7 @@ struct dcpib { struct drm_device drm; struct apple_rtkit *rtk; struct workqueue_struct *wq; + u32 inherited_afk_endpoints; struct dcpib_ep *eps[DCPIB_NR_EPS]; struct dcpib_ep *iboot_ep; diff --git a/drivers/gpu/drm/apple_dcpib/drv.c b/drivers/gpu/drm/apple_dcpib/drv.c index f1ea13dd26963..e80f54816a062 100644 --- a/drivers/gpu/drm/apple_dcpib/drv.c +++ b/drivers/gpu/drm/apple_dcpib/drv.c @@ -371,6 +371,7 @@ static int dcpib_probe_inner(struct platform_device *pdev) struct device_node *hdmi_np __free(device_node) = dcpib_hdmi_node(); int source = hdmi_np ? dcpib_hdmi_source(hdmi_np) : -ENODEV; struct dcpib *dcp; + u32 inherited_afk_endpoints; int ret; /* @@ -383,6 +384,12 @@ static int dcpib_probe_inner(struct platform_device *pdev) return -ENODEV; } + ret = of_property_read_u32(dev->of_node, "apple,afk-active-endpoints", + &inherited_afk_endpoints); + if (ret || inherited_afk_endpoints & ~GENMASK(15, 0)) + return dev_err_probe(dev, -EINVAL, + "bootloader AFK endpoint handoff required\n"); + ret = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64)); if (ret) return ret; @@ -391,6 +398,7 @@ static int dcpib_probe_inner(struct platform_device *pdev) if (IS_ERR(dcp)) return PTR_ERR(dcp); dcp->dev = dev; + dcp->inherited_afk_endpoints = inherited_afk_endpoints; platform_set_drvdata(pdev, dcp); mutex_init(&dcp->lock); mutex_init(&dcp->rx_gate);