From 2b714ed4808bd52503715080a2a998284122d722 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 23 Oct 2026 15:03:19 +1100 Subject: [PATCH 001/388] arm64: configs: asahi: use 48-bit VA and enable 4K userspace Over plain ARM64 defconfig the 16K kernel resolves to 52-bit VA with LPA2. ARM64_USER4K_EXPERIMENTAL depends on 48-bit VA and PA without LPA2, so it drops out and the user4k=on boot option has nothing to turn on. Steam and FEX on the Neo rely on it for x86 games that assume 4K pages. Apple Silicon has no LPA2 and needs nothing past 48-bit VA, so request 48-bit VA here and keep the 4K userspace support enabled. Fedora and the other distribution bases already use 48-bit VA, so this only changes the defconfig result, which now matches them. Checked with defconfig + asahi.config and with the Fedora Asahi base + asahi.config: both resolve to VA_BITS=48, PA_BITS=48 and ARM64_USER4K_EXPERIMENTAL=y. Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/configs/asahi.config | 2 ++ 1 file changed, 2 insertions(+) diff --git a/arch/arm64/configs/asahi.config b/arch/arm64/configs/asahi.config index 560ef5b147491..17783fc2b182d 100644 --- a/arch/arm64/configs/asahi.config +++ b/arch/arm64/configs/asahi.config @@ -8,6 +8,8 @@ CONFIG_ARCH_APPLE=y # CONFIG_ARM64_4K_PAGES is not set CONFIG_ARM64_16K_PAGES=y # CONFIG_ARM64_64K_PAGES is not set +CONFIG_ARM64_VA_BITS_48=y +CONFIG_ARM64_USER4K_EXPERIMENTAL=y CONFIG_ARM64_MEMORY_MODEL_CONTROL=y CONFIG_ARM_APPLE_CPUIDLE=y CONFIG_ARM_APPLE_SOC_CPUFREQ=m From 8800a5887f7f85c28bfbfdfb42cfeb2f4fcff367 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 23 Oct 2026 15:18:57 +1100 Subject: [PATCH 002/388] arm64: configs: asahi: enable the power allocator governor it defaults to asahi.config makes the power allocator the default thermal governor but never enables the governor itself. Over defconfig that happens to work, since defconfig already builds it. Over a distribution base that leaves THERMAL_GOV_POWER_ALLOCATOR off, Fedora's Asahi config for one, the default cannot be selected and the base's step_wise default wins, so the setting does not survive resolution. Enable the governor and switch the base's step_wise default off, so the requested default holds over either base. Checked with defconfig + asahi.config and with the Fedora Asahi base + asahi.config: every asahi.config setting survives olddefconfig. Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/configs/asahi.config | 2 ++ 1 file changed, 2 insertions(+) diff --git a/arch/arm64/configs/asahi.config b/arch/arm64/configs/asahi.config index 17783fc2b182d..a3f4068b53439 100644 --- a/arch/arm64/configs/asahi.config +++ b/arch/arm64/configs/asahi.config @@ -13,6 +13,8 @@ CONFIG_ARM64_USER4K_EXPERIMENTAL=y CONFIG_ARM64_MEMORY_MODEL_CONTROL=y CONFIG_ARM_APPLE_CPUIDLE=y CONFIG_ARM_APPLE_SOC_CPUFREQ=m +CONFIG_THERMAL_GOV_POWER_ALLOCATOR=y +# CONFIG_THERMAL_DEFAULT_GOV_STEP_WISE is not set CONFIG_THERMAL_DEFAULT_GOV_POWER_ALLOCATOR=y CONFIG_BT_BRCMEXT=y CONFIG_BT_HCIBCM4377=m From d80847118522567a39106da6bc6fb1ba16b57275 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Sat, 24 Oct 2026 23:16:40 +1100 Subject: [PATCH 003/388] Bluetooth: bcm4377: abort suspend if wake IRQ arming fails With controller wake enabled, bcm4377_suspend() returns success even when enable_irq_wake() fails. The MSI is then not a wake source, although HCI has been suspended for remote wake and the function remains in D0. A fault-injected callback test reproduces a zero return with HCI still suspended and no wake reference after enable_irq_wake() returns -ENXIO. Return the arming error and undo HCI suspension using the existing resume path. Clear the powered-resume epoch before that rollback, since no system sleep completed, and preserve the original error if HCI rollback also fails. Arm the IRQ before saving PCI state, so an aborted suspend does not leave a saved-state marker. The PM core does not invoke resume for the device whose suspend callback failed. The extracted PM callbacks pass host tests with mocked kernel APIs for arming failure, rollback failure, repeated successful wake cycles, wake-disabled quiescence and HCI suspend failure, with recovery support on and off. ARM64 kernel build and hardware wake qualification remain pending; this does not establish the cause of a particular machine hang. Fixes: 0609ca7fa79a ("Bluetooth: hci_bcm4377: Support remote wakeup") Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/bluetooth/hci_bcm4377.c | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/drivers/bluetooth/hci_bcm4377.c b/drivers/bluetooth/hci_bcm4377.c index 69b87ed4fa6fe..9d3bc53d54607 100644 --- a/drivers/bluetooth/hci_bcm4377.c +++ b/drivers/bluetooth/hci_bcm4377.c @@ -3049,6 +3049,8 @@ static int bcm4377_probe(struct pci_dev *pdev, const struct pci_device_id *id) hdev); } +static int bcm4377_resume(struct device *dev); + static int bcm4377_suspend(struct device *dev) { struct pci_dev *pdev = to_pci_dev(dev); @@ -3071,6 +3073,21 @@ static int bcm4377_suspend(struct device *dev) } if (device_may_wakeup(dev)) { + ret = enable_irq_wake(pdev->irq); + if (ret) { + int resume_ret; + + dev_err(dev, "Failed to arm Bluetooth wake IRQ: %d\n", ret); + /* The PM core will not resume a device that failed suspend. */ + bcm4377->resume_powered = false; + resume_ret = bcm4377_resume(dev); + if (resume_ret) + dev_err(dev, "Failed to undo Bluetooth suspend: %d\n", + resume_ret); + return ret; + } + bcm4377->wake_armed = true; + /* * Remote wakeup: leave the firmware running and the function * in D0, so a connection attempt from a wake-allowed device @@ -3078,7 +3095,6 @@ static int bcm4377_suspend(struct device *dev) * core from putting the function into D3hot. */ pci_save_state(pdev); - bcm4377->wake_armed = !enable_irq_wake(pdev->irq); return 0; } From 1ea4c92a7e4b44c65153b65db82c4ce73632f1c4 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Sat, 24 Oct 2026 23:18:58 +1100 Subject: [PATCH 004/388] docs: explain Apple s2idle wake paths and qualification Distinguish ordinary armed wake IRQs from filtered NO_SUSPEND transport interrupts, and describe how to identify a suspend-entry or resume stall. Cover SMC, dockchannel, USB, Bluetooth and RTC prerequisites without assuming one radio or PMIC across Apple Silicon boards. Document a source-by-source test matrix, wake-policy checks and recovery requirements. Restrict s2idle timed PM tests to checkpoints reached before the idle loop; processors and core can otherwise cause real sleep. Assisted-by: LLM Signed-off-by: Ryan Murray --- Documentation/power/apple-s2idle.rst | 109 +++++++++++++++++++++++++++ Documentation/power/index.rst | 1 + MAINTAINERS | 1 + 3 files changed, 111 insertions(+) create mode 100644 Documentation/power/apple-s2idle.rst diff --git a/Documentation/power/apple-s2idle.rst b/Documentation/power/apple-s2idle.rst new file mode 100644 index 0000000000000..aeceefe3fabff --- /dev/null +++ b/Documentation/power/apple-s2idle.rst @@ -0,0 +1,109 @@ +.. SPDX-License-Identifier: GPL-2.0 + +Apple Silicon suspend-to-idle wake qualification +=============================================== + +A missing wake IRQ number does not establish that an Apple Silicon system +has no wake source. There are two interrupt paths to distinguish: + +* An ordinary wake interrupt is enabled with ``enable_irq_wake()``. The IRQ + core keeps it enabled during device IRQ suspension and reports a system + wakeup when it arrives. AIC and AIC2/AIC3 use ``IRQCHIP_SKIP_SET_WAKE``: + retained-power suspend-to-idle needs normal interrupt delivery rather + than a second hardware wake routing configuration. +* An ``IRQF_NO_SUSPEND`` interrupt continues to run its handler. The handler + or its consumer must identify a genuine wake event and report it to the + PM core. The interrupt alone does not end system suspend. This permits + transports with unrelated traffic to filter their wake events. + +SMC mailbox interrupts use the second path. The SMC input driver makes +power-button presses and lid-open events hard wake events during its sleep +window. The RTKit notification worker must remain runnable. Do not arm the +whole SMC mailbox as an ordinary wake IRQ: unrelated notifications and +command replies are not user wake requests. + +Dockchannel HID also uses the second path. Its RX child interrupt stays +live below a chained AIC parent. The HID transport classifies keyboard +reports and trackpad clicks before reporting a hard wake event; pointer +motion, touch and other interface traffic do not wake the system. On +systems using a receive DMA ring, both the coprocessor and its translations +must remain usable while the system sleeps. + +Apple platform xHCI can keep running during suspend-to-idle and report +filtered USB wake events. Peripheral remote-wake support, hub policy and +controller retention all matter. An unarmed xHCI IRQ by itself does not +prove that this path is broken. + +Bluetooth depends on the fitted radio and its transport driver. In +particular, the MT7932 driver is not used by a board with a Broadcom radio. +Check the bound driver, device wake policy, HCI event filters, PCI/MSI wake +propagation and the return value of wake IRQ arming. A driver must undo its +own suspend state if it returns an error after suspending HCI: the PM core +will not invoke its normal resume callback for that failed suspend. + +An SMC RTC that provides timekeeping is not necessarily an alarm wake +source. Enumerate RTC names, alarm support and wake policy rather than +assuming that ``rtc0`` or a PMU driver enabled in the kernel configuration +provides a working recovery alarm. A PMIC's compatible string, register +layout, offset cell and interrupt wiring must all match its RTC driver. + +Establish the failing phase +-------------------------- + +The ``PM: suspend entry (s2idle)`` message is printed before process +freezing and device suspend. It does not prove entry into the idle loop. +Likewise, loss of SSH can follow network suspension while another device +is still suspending. A black display does not establish whether CPUs have +resumed. + +Capture the console independently of the display and network under test. +Use the existing ``power:suspend_resume`` and +``power:device_pm_callback_start``/``power:device_pm_callback_end`` trace +points, PM debug messages and callback timing. A ``machine_suspend`` entry +for suspend-to-idle establishes that the idle path was reached; callback +start/end pairs locate a device suspend or resume stall. See +:doc:`basic-pm-debugging` for staged PM tests. For suspend-to-idle, use +``freezer``, ``devices`` and ``platform``. The idle loop runs before the +``processors`` and ``core`` test checkpoints, so those levels do not +provide an automatic return from suspend-to-idle. + +Before actual sleep, record: + +* Board compatible strings, kernel release, source commit, configuration + and the DTB used by the boot loader. +* ``/sys/power/mem_sleep`` and ``/sys/power/pm_test``. +* The wake policy of the SMC input, dockchannel HID, Bluetooth PCI function + and USB devices; the bound drivers; relevant IRQ names and counts. +* ``/sys/kernel/debug/wakeup_sources`` and + ``/sys/power/suspend_stats`` where available. +* Each RTC's name, alarm support and device wake policy. + +Use ``/sys/power/pm_wakeup_irq`` for ordinary armed-IRQ attribution. For a +filtered transport, also compare its wakeup-source counters and interrupt +trace. An empty IRQ result is compatible with a valid filtered wake. + +Qualification matrix +-------------------- + +Test one wake stimulus at a time, first after a short sleep and then after +repeated cycles: + +* A short power-button press, and a lid close/open cycle. +* The built-in keyboard and a trackpad click. Separately verify that + motion and touch do not cause an unwanted wake. +* A wake-enabled, paired Bluetooth keyboard with the fitted radio driver. + Repeat with the PCI function's wake policy disabled. +* A directly connected USB keyboard, followed by the intended hub/dock, + with the necessary device and hub wake policies enabled. +* A timed RTC alarm only on a device known to provide that functionality. + +Also test a wake during entry, an aborted suspend, Bluetooth wake IRQ +arming failure, and suspend with Bluetooth wake disabled. A wake-arming +failure must abort suspend and leave the running transport recoverable; +successful cycles must balance wake IRQ enable/disable calls. + +For every cycle record the stimulus, idle entry, wake event, resume +completion and post-resume input, storage and network health separately. +Preserve a failed cycle's console and trace before recovery. Keep a known +bootable fallback and an independently available recovery transport, and +restore any temporary debug settings and wake policies after testing. diff --git a/Documentation/power/index.rst b/Documentation/power/index.rst index b4581e4ae785b..ce6d50693040f 100644 --- a/Documentation/power/index.rst +++ b/Documentation/power/index.rst @@ -8,6 +8,7 @@ Power Management :maxdepth: 1 apm-acpi + apple-s2idle basic-pm-debugging charger-manager drivers-testing diff --git a/MAINTAINERS b/MAINTAINERS index 0a50e30dd7f82..6eb8b5aab2bff 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -2740,6 +2740,7 @@ F: Documentation/devicetree/bindings/usb/apple,dwc3.yaml F: Documentation/devicetree/bindings/usb/apple,sn201202x.yaml F: Documentation/devicetree/bindings/watchdog/apple,wdt.yaml F: Documentation/hwmon/macsmc-hwmon.rst +F: Documentation/power/apple-s2idle.rst F: arch/arm64/boot/dts/apple/ F: drivers/bluetooth/hci_bcm4377.c F: drivers/clk/clk-apple-nco.c From d5ee0bd28d7e75edfb2acceef113138c457e5c76 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Sun, 25 Oct 2026 04:55:18 +1100 Subject: [PATCH 005/388] drm/asahi: keep work-queue firmware structures until the context is invalidated When a queue is destroyed while it still has jobs on the GPU, drm_sched_fini() cancels the scheduler jobs, but the firmware keeps running the commands it already holds. Each work queue then stays alive only through its event slot and is dropped from the completion path as soon as its last stamp is seen. Dropping it frees its queue info (ring state, ring and notifier list), and the last command frees the queue's notifier and threshold. Those come from the shared and private kernel allocators, so the memory can be reused right away. The GPU context that the firmware still associates those structures with is only invalidated at the next alloc(), so until then the firmware can still refer to a work queue whose memory has already been handed out. The context data already keeps the tile vertex buffer alive until it is invalidated. Do the same for the queue structures. A GpuContext now keeps a list of retained objects. A work queue moves its queue info there when it is dropped, and the queue adds its notifier when it is created. The list travels with the context data to the garbage list and is freed once DestroyContext has completed. The queue info points to the context weakly now, since the context owns it from then on. This closes the lifetime window for those two objects. On its own it does not stop the sustained-teardown firmware crash on the M1 Pro (G13X): that crash still reproduces with this change, so at least one other kernel object on the teardown path is read by the firmware after it is freed and reused. Investigation continues. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/asahi/fw/workqueue.rs | 7 +- drivers/gpu/drm/asahi/gpu.rs | 14 +- drivers/gpu/drm/asahi/queue/mod.rs | 15 +- drivers/gpu/drm/asahi/workqueue.rs | 202 +++++++++++++++++--------- 4 files changed, 160 insertions(+), 78 deletions(-) diff --git a/drivers/gpu/drm/asahi/fw/workqueue.rs b/drivers/gpu/drm/asahi/fw/workqueue.rs index b86bd3ff3757a..78ac6c676a6eb 100644 --- a/drivers/gpu/drm/asahi/fw/workqueue.rs +++ b/drivers/gpu/drm/asahi/fw/workqueue.rs @@ -152,7 +152,7 @@ pub(crate) mod raw { pub(crate) unk_94: u32, pub(crate) pending: AtomicU32, pub(crate) unk_9c: u32, - pub(crate) gpu_context: GpuPointer<'a, super::GpuContextData>, + pub(crate) gpu_context: GpuWeakPointer, pub(crate) unk_a8: U64, #[ver(V >= V13_2 && G < G14X)] pub(crate) unk_b0: u32, @@ -171,6 +171,10 @@ impl GpuStruct for GpuContextData { type Raw<'a> = raw::GpuContextData; } +/// The firmware structures of a work queue. +/// +/// The raw structure points to the queue's GPU context weakly. The `GpuContext` keeps these +/// structures until it has been invalidated, and only then frees its own data. #[versions(AGX)] #[derive(Debug)] pub(crate) struct QueueInfo { @@ -178,7 +182,6 @@ pub(crate) struct QueueInfo { pub(crate) ring: GpuArray, pub(crate) gpu_buf: GpuArray, pub(crate) notifier_list: Arc>, - pub(crate) gpu_context: Arc, } #[versions(AGX)] diff --git a/drivers/gpu/drm/asahi/gpu.rs b/drivers/gpu/drm/asahi/gpu.rs index b68381ce60b30..62c049ff7dddc 100644 --- a/drivers/gpu/drm/asahi/gpu.rs +++ b/drivers/gpu/drm/asahi/gpu.rs @@ -260,9 +260,8 @@ pub(crate) struct GpuManager { stats: Arc, buffer_mgr: buffer::BufferManager::ver, ids: SequenceIDs, - #[allow(clippy::vec_box)] #[pin] - garbage_contexts: Mutex>>>, + garbage_contexts: Mutex>, } /// Operations the DRM driver performs on a GPU, independent of its firmware interface. @@ -401,8 +400,8 @@ pub(crate) trait GpuManager: Send + Sync { fn get_cfg(&self) -> &'static hw::HwConfig; /// Get the dynamic GPU configuration for this SoC. fn get_dyncfg(&self) -> &hw::DynConfig; - /// Register an unused context as garbage - fn free_context(&self, data: KBox>); + /// Register an unused context, with the objects it retains, for invalidation. + fn free_context(&self, ctx: workqueue::RetiredContext); } /// Private generic trait for functions that don't need to escape this module. @@ -1456,7 +1455,7 @@ impl GpuManager for GpuManager::ver { core::mem::swap(&mut *self.garbage_contexts.lock(), &mut garbage_ctx); for ctx in garbage_ctx { - if self.invalidate_context(&ctx).is_err() { + if self.invalidate_context(&ctx.data).is_err() { dev_err!( self.dev.as_ref(), "GpuContext: Failed to invalidate GPU context!\n" @@ -1465,6 +1464,9 @@ impl GpuManager for GpuManager::ver { panic!("GPU firmware timed out"); } } + // The firmware no longer knows the context's queues, so their structures may go now, + // ahead of the context data they point to. + core::mem::drop(ctx.retained); } let mut guard = self.alloc.lock(); @@ -1733,7 +1735,7 @@ impl GpuManager for GpuManager::ver { &self.dyncfg } - fn free_context(&self, ctx: KBox>) { + fn free_context(&self, ctx: workqueue::RetiredContext) { let mut garbage = self.garbage_contexts.lock(); if garbage.push(ctx, GFP_KERNEL).is_err() { diff --git a/drivers/gpu/drm/asahi/queue/mod.rs b/drivers/gpu/drm/asahi/queue/mod.rs index 2b32b97b5e169..2d9052c2edb51 100644 --- a/drivers/gpu/drm/asahi/queue/mod.rs +++ b/drivers/gpu/drm/asahi/queue/mod.rs @@ -525,6 +525,15 @@ impl Queue::ver { let buffer = buffer::Buffer::ver::new(gpu, alloc, ualloc.clone(), ualloc_priv, mgr)?; + let gpu_context = Arc::new( + workqueue::GpuContext::new(dev.clone(), alloc, buffer.any_ref())?, + GFP_KERNEL, + )?; + // Every command of this queue points the firmware at the notifier, and the notifier links + // into the notifier list of the work queues. Keep it as long as they are kept, until the + // context is invalidated, instead of freeing it with the last command. + gpu_context.retain(KBox::new(notifier.clone(), GFP_KERNEL)?)?; + let mut ret = Queue::ver { dev: dev.clone(), _sched: sched, @@ -538,11 +547,7 @@ impl Queue::ver { inner: QueueInner::ver { dev: dev.clone(), ualloc, - gpu_context: Arc::new( - workqueue::GpuContext::new(dev.clone(), alloc, buffer.any_ref())?, - GFP_KERNEL, - )?, - + gpu_context, buffer, notifier_list: Arc::new(notifier_list, GFP_KERNEL)?, notifier, diff --git a/drivers/gpu/drm/asahi/workqueue.rs b/drivers/gpu/drm/asahi/workqueue.rs index 6281ef684e251..d6695a1b3ee96 100644 --- a/drivers/gpu/drm/asahi/workqueue.rs +++ b/drivers/gpu/drm/asahi/workqueue.rs @@ -95,10 +95,31 @@ impl From for kernel::error::Error { } } +/// An object the firmware may still reference while its GPU context is live. +pub(crate) type RetainedObject = KBox; + +/// Objects retained by every context: the notifier and the vertex, fragment and compute work +/// queues of the owning queue. +const CONTEXT_RETAINED_OBJECTS: usize = 4; + +/// A dropped GPU context waiting for the firmware to invalidate it. +pub(crate) struct RetiredContext { + /// The context data named in the invalidation request. + pub(crate) data: KBox>, + /// Firmware structures of the context's queues. They must not be freed, and so must not be + /// reused, until the invalidation has completed. + pub(crate) retained: KVec, +} + /// A GPU context tracking structure, which must be explicitly invalidated when dropped. +/// +/// The firmware learns about the work queues of a context as they are run, and only forgets them +/// when the context is invalidated. Their firmware structures are therefore handed to the context +/// when their owner drops them, and freed after the invalidation. pub(crate) struct GpuContext { dev: driver::AsahiDevice, data: Option>>, + retained: Pin>>>, } no_debug!(GpuContext); @@ -118,27 +139,42 @@ impl GpuContext { )?, GFP_KERNEL, )?), + retained: KBox::pin_init( + new_mutex!( + KVec::with_capacity(CONTEXT_RETAINED_OBJECTS, GFP_KERNEL)?, + "GpuContext::retained" + ), + GFP_KERNEL, + )?, }) } - /// Returns the GPU pointer to the inner GPU context data structure. - pub(crate) fn gpu_pointer(&self) -> GpuPointer<'_, fw::workqueue::GpuContextData> { - self.data.as_ref().unwrap().gpu_pointer() + /// Returns a weak GPU pointer to the inner GPU context data structure. + pub(crate) fn weak_pointer(&self) -> GpuWeakPointer { + self.data.as_ref().unwrap().weak_pointer() + } + + /// Keep `object` allocated until the firmware has invalidated this context. + pub(crate) fn retain(&self, object: RetainedObject) -> Result { + self.retained.lock().push(object, GFP_KERNEL)?; + Ok(()) } } impl Drop for GpuContext { fn drop(&mut self) { + let data = self.data.take().unwrap(); + let retained = core::mem::take(&mut *self.retained.lock()); + let Some(guard) = self.dev.registration_guard() else { return; }; guard.registration_data_with(|reg_data| { - let data = self.data.take().unwrap(); // A context only exists for the manager whose allocators created it, so the manager is // always present. if let Some(gpu) = reg_data.gpu.manager() { - gpu.free_context(data); + gpu.free_context(RetiredContext { data, retained }); } }) } @@ -237,7 +273,10 @@ impl) + Send + Sync> GenSubm struct WorkQueueInner { dev: driver::AsahiDevice, event_manager: Arc, - info: GpuObject, + /// The firmware structures of this queue. Always `Some` until the queue is dropped, when they + /// move to `gpu_context`. + info: Option>>, + gpu_context: Arc, new: bool, pipe_type: PipeType, size: u32, @@ -265,10 +304,22 @@ pub(crate) struct WorkQueue { #[versions(AGX)] impl WorkQueueInner::ver { + fn info(&self) -> &GpuObject { + self.info + .as_deref() + .expect("WorkQueue: lost its queue info") + } + + fn info_mut(&mut self) -> &mut GpuObject { + self.info + .as_deref_mut() + .expect("WorkQueue: lost its queue info") + } + /// Return the GPU done pointer, representing how many work items have been completed by the /// GPU. fn doneptr(&self) -> u32 { - self.info + self.info() .state .with(|raw, _inner| raw.gpu_doneptr.load(Ordering::Acquire)) } @@ -485,7 +536,7 @@ impl Job::ver { let next_wptr = (wptr + 1) % inner.size; assert!(inner.doneptr() != next_wptr); - inner.info.ring[wptr as usize] = command.inner.gpu_va().get(); + inner.info_mut().ring[wptr as usize] = command.inner.gpu_va().get(); wptr = next_wptr; // Cannot fail, since we did a reserve(1) above @@ -515,7 +566,7 @@ impl<'a> JobSubmission::ver<'a> { core::mem::forget(self); inner - .info + .info() .state .with(|raw, _inner| raw.cpu_wptr.store(wptr, Ordering::Release)); @@ -527,7 +578,7 @@ impl<'a> JobSubmission::ver<'a> { let msg = fw::channels::RunWorkQueueMsg::ver { pipe_type: inner.pipe_type, - work_queue: Some(inner.info.weak_pointer()), + work_queue: Some(inner.info().weak_pointer()), wptr: inner.wptr, event_slot, is_new: inner.new, @@ -617,6 +668,23 @@ impl<'a> Drop for JobSubmission::ver<'a> { } } +#[versions(AGX)] +impl Drop for WorkQueueInner::ver { + fn drop(&mut self) { + // The firmware keeps referring to this queue until its context is invalidated. When the + // queue's owner went away with work in flight, the queue is dropped as soon as that work + // completes, which can be well before the invalidation. + if let Some(info) = self.info.take() { + if self.gpu_context.retain(info).is_err() { + pr_err!( + "WorkQueue({:?}): Failed to keep the queue until its context is invalidated\n", + self.pipe_type + ); + } + } + } +} + #[versions(AGX)] impl WorkQueueInner::ver { /// Return the number of free entries in the workqueue @@ -680,61 +748,65 @@ impl WorkQueue::ver { prio.5 = 1; } + let gpu_context_pointer = gpu_context.weak_pointer(); + + // Use shared (coherent) state with verbose faults so we can dump state correctly + let info = if debug_enabled(DebugFlags::VerboseFaults) { + &mut alloc.shared + } else { + &mut alloc.private + } + .new_init( + try_init!(QueueInfo::ver { + state: { + state.with_mut(|raw, _inner| { + raw.rb_size = size; + }); + state + }, + ring, + gpu_buf, + notifier_list: notifier_list, + }), + |inner, _p| { + try_init!(raw::QueueInfo::ver { + state: inner.state.gpu_pointer(), + ring: inner.ring.gpu_pointer(), + notifier_list: inner.notifier_list.gpu_pointer(), + gpu_buf: inner.gpu_buf.gpu_pointer(), + gpu_rptr1: Default::default(), + gpu_rptr2: Default::default(), + gpu_rptr3: Default::default(), + event_id: AtomicI32::new(-1), + priority: prio, + unk_4c: -1, + uuid: id as u32, + unk_54: -1, + unk_58: Default::default(), + busy: Default::default(), + __pad: Default::default(), + #[ver(V >= V13_2 && G < G14X)] + unk_84_0: 0, + unk_84_state: Default::default(), + error_count: Default::default(), + unk_8c: 0, + unk_90: 0, + unk_94: 0, + pending: Default::default(), + unk_9c: 0, + gpu_context: gpu_context_pointer, + unk_a8: Default::default(), + #[ver(V >= V13_2 && G < G14X)] + unk_b0: 0, + }) + }, + )?; + let inner = WorkQueueInner::ver { dev: dev.into(), event_manager, - // Use shared (coherent) state with verbose faults so we can dump state correctly - info: if debug_enabled(DebugFlags::VerboseFaults) { - &mut alloc.shared - } else { - &mut alloc.private - } - .new_init( - try_init!(QueueInfo::ver { - state: { - state.with_mut(|raw, _inner| { - raw.rb_size = size; - }); - state - }, - ring, - gpu_buf, - notifier_list: notifier_list, - gpu_context: gpu_context, - }), - |inner, _p| { - try_init!(raw::QueueInfo::ver { - state: inner.state.gpu_pointer(), - ring: inner.ring.gpu_pointer(), - notifier_list: inner.notifier_list.gpu_pointer(), - gpu_buf: inner.gpu_buf.gpu_pointer(), - gpu_rptr1: Default::default(), - gpu_rptr2: Default::default(), - gpu_rptr3: Default::default(), - event_id: AtomicI32::new(-1), - priority: prio, - unk_4c: -1, - uuid: id as u32, - unk_54: -1, - unk_58: Default::default(), - busy: Default::default(), - __pad: Default::default(), - #[ver(V >= V13_2 && G < G14X)] - unk_84_0: 0, - unk_84_state: Default::default(), - error_count: Default::default(), - unk_8c: 0, - unk_90: 0, - unk_94: 0, - pending: Default::default(), - unk_9c: 0, - gpu_context: inner.gpu_context.gpu_pointer(), - unk_a8: Default::default(), - #[ver(V >= V13_2 && G < G14X)] - unk_b0: 0, - }) - }, - )?, + info: Some(KBox::new(info, GFP_KERNEL)?), + gpu_context, new: true, pipe_type, size, @@ -751,7 +823,7 @@ impl WorkQueue::ver { last_submitted: None, }; - let info_pointer = inner.info.weak_pointer(); + let info_pointer = inner.info().weak_pointer(); Arc::pin_init( pin_init!(Self { @@ -835,7 +907,7 @@ impl WorkQueue::ver { pub(crate) fn dump_info(&self) { pr_info!("WorkQueue @ {:?}:", self.info_pointer); - self.inner.lock().info.with(|raw, _inner| { + self.inner.lock().info().with(|raw, _inner| { pr_info!(" GPU rptr1: {:#x}", raw.gpu_rptr1.load(Ordering::Relaxed)); pr_info!(" GPU rptr1: {:#x}", raw.gpu_rptr2.load(Ordering::Relaxed)); pr_info!(" GPU rptr1: {:#x}", raw.gpu_rptr3.load(Ordering::Relaxed)); @@ -953,7 +1025,7 @@ impl WorkQueue for WorkQueue::ver { ); inner - .info + .info() .state .with(|raw, _inner| raw.cpu_freeptr.store(last_wptr, Ordering::Release)); From a2a5040d95b26704b9ce51b55301944f99c75e1c Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Sun, 25 Oct 2026 11:23:00 +1100 Subject: [PATCH 006/388] arm64: configs: enable the Asahi default thermal governor's dependencies The shared fragment requests the power allocator as the default thermal governor but does not enable the governor itself. A Fedora base explicitly disables THERMAL_GOV_POWER_ALLOCATOR, so olddefconfig drops the requested default and falls back to step-wise. Defconfig happens to enable it. Enable the governor and its energy-model dependency in the common fragment. This keeps the requested hardware default available on both defconfig and distribution bases without a device-specific configuration. Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/configs/asahi.config | 2 ++ 1 file changed, 2 insertions(+) diff --git a/arch/arm64/configs/asahi.config b/arch/arm64/configs/asahi.config index 3c5b8c02f06f7..7ff013f56825b 100644 --- a/arch/arm64/configs/asahi.config +++ b/arch/arm64/configs/asahi.config @@ -11,6 +11,8 @@ CONFIG_ARM64_16K_PAGES=y CONFIG_ARM64_MEMORY_MODEL_CONTROL=y CONFIG_ARM_APPLE_CPUIDLE=y CONFIG_ARM_APPLE_SOC_CPUFREQ=m +CONFIG_ENERGY_MODEL=y +CONFIG_THERMAL_GOV_POWER_ALLOCATOR=y CONFIG_THERMAL_DEFAULT_GOV_POWER_ALLOCATOR=y CONFIG_BT_BRCMEXT=y CONFIG_BT_HCIBCM4377=m From ad691cd5e9f76aa9e486d93b560c58d5c8ff88c4 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Fri, 23 Oct 2026 03:52:09 -0400 Subject: [PATCH 007/388] drm/asahi: publish bound G17 compute work without the admission passes A compute command on a bound queue went through every admission step on each submission. ensure_compute took the device mutex for a replay and witness pass, shared_compute took a preparation lease and the mutex again, and build_compute did both once more to find its queue already bound. compute_packet then took a third lease and the mutex a fourth time, replaying compute again before publishing. Compute also never published from its submit ioctl, so even a command with nothing to wait for went through the scheduler worker first. Its completions never fed the CPU-capacity detector, so the host loop feeding a compute-bound GPU ran at the lowest CPU performance level. Check what ensure_compute establishes under the one device-mutex section that publishes, and run the admission passes only when that check fails. Publish a dependency-free compute command from its submit ioctl when its queue is bound and nothing has to wait, as renders do. Keep a separate backlog per engine so that a command waiting in one engine's scheduler does not stop the other engine's inline publication. Report compute dispatch envelopes to the detector as render passes are. --- drivers/gpu/drm/asahi/g17/completion.rs | 8 +- drivers/gpu/drm/asahi/g17/feed.rs | 2 +- drivers/gpu/drm/asahi/g17/initdata.rs | 6 + drivers/gpu/drm/asahi/g17/job.rs | 2 +- drivers/gpu/drm/asahi/g17/queue/mod.rs | 5 +- drivers/gpu/drm/asahi/g17/runtime.rs | 108 +++++++++++++++++- drivers/gpu/drm/asahi/g17/runtime/backend.rs | 16 ++- drivers/gpu/drm/asahi/g17/runtime/render.rs | 1 + .../drm/asahi/g17/runtime/render/submit.rs | 4 +- 9 files changed, 138 insertions(+), 14 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/completion.rs b/drivers/gpu/drm/asahi/g17/completion.rs index 4f5b87bc853dc..dacd6de9a3a48 100644 --- a/drivers/gpu/drm/asahi/g17/completion.rs +++ b/drivers/gpu/drm/asahi/g17/completion.rs @@ -546,7 +546,13 @@ impl Completion { return; } if let (Some(feed), Ok(ticks)) = (&self.feed, &result) { - feed.note_render_pass(ticks[2], ticks[3]); + // Compute reports its dispatch envelope in the first pair, a + // render its fragment pass in the second. + if self.compute.is_some() { + feed.note_pass(ticks[0], ticks[1]); + } else { + feed.note_pass(ticks[2], ticks[3]); + } } if let Err(error) = result { // Spared work cancelled after another VM's recovery is not this client's device loss. diff --git a/drivers/gpu/drm/asahi/g17/feed.rs b/drivers/gpu/drm/asahi/g17/feed.rs index 0045dff413922..760f352046192 100644 --- a/drivers/gpu/drm/asahi/g17/feed.rs +++ b/drivers/gpu/drm/asahi/g17/feed.rs @@ -410,7 +410,7 @@ impl Feed { Monotonic::ktime_get() as u64 } - pub(crate) fn note_render_pass(&self, start: u64, end: u64) { + pub(crate) fn note_pass(&self, start: u64, end: u64) { if cfg!(CONFIG_CPU_FREQ) { self.detector.note(start, end, Self::now()); } diff --git a/drivers/gpu/drm/asahi/g17/initdata.rs b/drivers/gpu/drm/asahi/g17/initdata.rs index 10af261aeb3ac..049c93b6a466a 100644 --- a/drivers/gpu/drm/asahi/g17/initdata.rs +++ b/drivers/gpu/drm/asahi/g17/initdata.rs @@ -394,6 +394,12 @@ impl InitData { Ok(()) } + pub(super) fn compute_vm_cached(&self, vm: &mmu::Vm) -> bool { + self.compute_shared + .as_ref() + .is_some_and(|shared| vm.compute_pool_mappings_match(shared.id)) + } + pub(super) fn cache_compute_vm(&self, vm: &mmu::Vm) -> Result { let shared = self.compute_shared.as_ref().ok_or(EIO)?; if vm.compute_pool_mappings_match(shared.id) { diff --git a/drivers/gpu/drm/asahi/g17/job.rs b/drivers/gpu/drm/asahi/g17/job.rs index 3b47d361c2ef9..fd2cc878b534e 100644 --- a/drivers/gpu/drm/asahi/g17/job.rs +++ b/drivers/gpu/drm/asahi/g17/job.rs @@ -429,7 +429,7 @@ impl Scheduler { }; // All fallible scheduler setup precedes early hardware publication. The // scheduler still owns watchdog, credits and cancellation after push. - if packet.engine() == Engine::Render && !(early && self.backend.try_early(&packet)) { + if !(early && self.backend.try_early(&packet)) { self.backend.count_backlog(&packet); } job.push(); diff --git a/drivers/gpu/drm/asahi/g17/queue/mod.rs b/drivers/gpu/drm/asahi/g17/queue/mod.rs index 6a9b15789339f..1f57f2913a020 100644 --- a/drivers/gpu/drm/asahi/g17/queue/mod.rs +++ b/drivers/gpu/drm/asahi/g17/queue/mod.rs @@ -216,7 +216,7 @@ impl Queue { destinations, engine == Engine::Compute, host_predecessors.clone().next().is_some(), - if engine == Engine::Render { self.backend.feed() } else { None }, + self.backend.feed(), )?; let publication = completion.publication() .map(|target| PreparedPublication::new(target, host_predecessors)) @@ -298,8 +298,7 @@ impl Queue { checked_inputs.push(fence.clone(), GFP_KERNEL)?; } } - let early = engine == Engine::Render - && dependencies.is_empty() + let early = dependencies.is_empty() && self.frontiers[index].len() < EARLY_DEPTH; let packet = Packet::new( id, diff --git a/drivers/gpu/drm/asahi/g17/runtime.rs b/drivers/gpu/drm/asahi/g17/runtime.rs index 6f4a5355d6eb3..284c5d7bc1eb9 100644 --- a/drivers/gpu/drm/asahi/g17/runtime.rs +++ b/drivers/gpu/drm/asahi/g17/runtime.rs @@ -441,6 +441,7 @@ pub(super) struct Backend { pub(super) context: Arc, owner: u64, backlog: core::sync::atomic::AtomicU32, + compute_backlog: core::sync::atomic::AtomicU32, closed: core::sync::atomic::AtomicBool, } @@ -521,6 +522,7 @@ impl Backend { context, owner, backlog: core::sync::atomic::AtomicU32::new(0), + compute_backlog: core::sync::atomic::AtomicU32::new(0), closed: core::sync::atomic::AtomicBool::new(false), }, GFP_KERNEL, @@ -1163,6 +1165,18 @@ impl super::Firmware { } impl Registry { + /// Everything ensure_compute establishes for `owner`: the shared compute + /// objects mapped into its VM, and its queue bound to `context` with a + /// pool that fits and no spared witness to release. + fn compute_bound(&self, owner: u64, context: &Arc, required: usize) -> Result { + let Some(queue) = self.compute(owner) else { + return Ok(false); + }; + Ok(!queue.spared_pending() + && queue.pool_fits(required) + && queue.matches(owner, queue.qid(), context)?) + } + fn release_compute_witness( &mut self, init: &super::initdata::InitData, @@ -1317,6 +1331,85 @@ impl Backend { Err(EBUSY) } + /// Publishes a dependency-free compute command from its submit ioctl when + /// its queue is already bound and nothing has to wait, as renders do. Any + /// wait leaves the command, unpublished, to its scheduler. + pub(super) fn try_compute_early(&self, packet: &Arc) -> bool { + if self.compute_backlog.load(Ordering::Acquire) != 0 || packet.has_dependencies() { + return false; + } + if !packet.begin_publication() { + packet.completion.fail_unpublished(ECANCELED); + packet.set_early_result(Err(ECANCELED)); + return true; + } + let mut deferred = match DeferredBatch::publication() { + Ok(deferred) => deferred, + Err(error) => { + if packet.completion.status().get() != 0 { + ::fail_vm(self, error); + } + packet.completion.fail_unpublished(error); + packet.set_early_result(Err(error)); + return true; + } + }; + let result = self.compute_inline(packet, &mut deferred); + if matches!(result, Err(error) if error == render::FALLBACK) && !packet.is_published() { + deferred.finish(); + packet.abandon_publication(); + return false; + } + let result = self + .finish_publication(packet, result, &mut deferred) + .map(|_| ()); + packet.set_early_result(result); + true + } + + /// compute_packet's publication section without its waits: every condition + /// it would sleep or retry for returns FALLBACK instead. + fn compute_inline( + &self, + packet: &Arc, + deferred: &mut DeferredBatch, + ) -> Result { + let super::command::Validated::Compute { scratch, .. } = &packet.command else { return Err(EINVAL); }; + let required = super::fw::compute_pool::required_blocks(self.shared.active_cores, scratch.pages())?; + if packet.cancel_requested() || self.admission_open().is_err() || packet.completion.status().get() != 0 { + return Err(render::FALLBACK); + } + let lease = self.enter_preparation(false)?; + match packet.completion.work_state()?.acquire() { + Err(error) if error == EBUSY => return Err(render::FALLBACK), + result => { + result?; + } + } + let mut state = self.shared.state.lock(); + let firmware = (*state).as_deref_mut().ok_or(ENODEV)?; + if !lease.is_current() + || self.shared.crashed.load(Ordering::Acquire) + || firmware.recovery.pending() + || super::recovery::Memory::recovery_state(&firmware.init)? != 0 + { + return Err(render::FALLBACK); + } + firmware.replay_compute(deferred)?; + if !firmware.init.compute_vm_cached(self.context.vm()) + || !firmware.queues.compute_bound(self.owner, &self.context, required)? + { + return Err(render::FALLBACK); + } + let dependencies = firmware.queues.render_prefix(self.owner, packet)?; + match firmware.publish_compute(self.owner, packet.clone(), dependencies.as_slice(), deferred) { + Err(error) if (error == EBUSY || error == EAGAIN) && !packet.is_published() => { + Err(render::FALLBACK) + } + result => result, + } + } + /// Called after taking this packet's publication claim. All waits and work /// state allocation precede the device mutex; only the checked epoch commits. fn compute_packet( @@ -1326,6 +1419,10 @@ impl Backend { ) -> Result { packet.check_dependencies()?; let super::command::Validated::Compute { scratch, .. } = &packet.command else { return Err(EINVAL); }; + let required = super::fw::compute_pool::required_blocks(self.shared.active_cores, scratch.pages())?; + // A bound queue needs none of ensure_compute's admission passes. Try + // without them; the publication section below checks the same state. + let mut bound = true; let mut recovery_polls = 0; let mut state_delay = 20i64; let deadline = now_ns().saturating_add(WORK_RING_WAIT_NS); @@ -1341,8 +1438,10 @@ impl Backend { // All DRM dependencies were satisfied before this scheduler callback. // Recheck on every retry: pressure may yield an idle binding between // admission and publication without changing this logical queue. - Backend::ensure_compute(self, scratch.pages(), &admission)?; - admission.check()?; + if !bound { + Backend::ensure_compute(self, scratch.pages(), &admission)?; + admission.check()?; + } let lease = self.shared.preparations.enter_until(&admission)?; // Work states free as this context's earlier commands retire. // Wait for one under the packet's admission deadline, as render @@ -1387,7 +1486,10 @@ impl Backend { continue; } firmware.replay_compute(deferred)?; - if firmware.queues.compute(self.owner).is_none() { + if !firmware.init.compute_vm_cached(self.context.vm()) + || !firmware.queues.compute_bound(self.owner, &self.context, required)? + { + bound = false; drop(state); drop(lease); continue; diff --git a/drivers/gpu/drm/asahi/g17/runtime/backend.rs b/drivers/gpu/drm/asahi/g17/runtime/backend.rs index 7d690470cd7b5..c75b525413ee8 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/backend.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/backend.rs @@ -13,7 +13,14 @@ use kernel::{dma_fence::Fence, drm::sched, prelude::*, sync::Arc}; impl Backend { pub(super) fn release_backlog(&self, packet: &Packet) { if packet.take_backlog() { - self.backlog.fetch_sub(1, Ordering::AcqRel); + self.backlog_of(packet).fetch_sub(1, Ordering::AcqRel); + } + } + + pub(super) fn backlog_of(&self, packet: &Packet) -> &core::sync::atomic::AtomicU32 { + match packet.engine() { + Engine::Render => &self.backlog, + Engine::Compute => &self.compute_backlog, } } @@ -274,11 +281,14 @@ impl job::Backend for Backend { } fn try_early(&self, packet: &Arc) -> bool { - self.try_render_early(packet) + match packet.engine() { + Engine::Render => self.try_render_early(packet), + Engine::Compute => self.try_compute_early(packet), + } } fn count_backlog(&self, packet: &Packet) { packet.count_backlog(); - self.backlog.fetch_add(1, Ordering::AcqRel); + self.backlog_of(packet).fetch_add(1, Ordering::AcqRel); } } diff --git a/drivers/gpu/drm/asahi/g17/runtime/render.rs b/drivers/gpu/drm/asahi/g17/runtime/render.rs index 59db76b9f109a..c657a45bdfa48 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render.rs @@ -16,6 +16,7 @@ mod settle; mod submit; pub(super) mod teardown; +pub(super) use submit::FALLBACK; use super::{DeferredBatch, Registry}; use crate::g17::{context::Context, fw::queue::DataMaster, job::Packet, kick, queue::render::Pair}; use crate::hw::t8140::queues::RENDER_SLOTS; diff --git a/drivers/gpu/drm/asahi/g17/runtime/render/submit.rs b/drivers/gpu/drm/asahi/g17/runtime/render/submit.rs index b77fc13b7ed2f..6c5710554e7f5 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render/submit.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render/submit.rs @@ -17,13 +17,13 @@ use kernel::{ /// Internal result of a publication attempted from the submitting ioctl that /// met contention before anything became visible: the scheduler publishes the /// command instead. Never returned to userspace. -pub(super) const FALLBACK: Error = EINPROGRESS; +pub(in crate::g17::runtime) const FALLBACK: Error = EINPROGRESS; impl Backend { /// The submitting ioctl holds the logical queue's mutex and never sleeps /// for contention or a closed preparation gate: it returns FALLBACK, and /// the scheduler's worker publishes the command and owns the waits. - pub(super) fn enter_preparation(&self, sleep: bool) -> Result { + pub(in crate::g17::runtime) fn enter_preparation(&self, sleep: bool) -> Result { if sleep { return self.shared.preparations.enter(false); } From 1121ae36b8d426be24c41ae360281b5f0709a69d Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Fri, 23 Oct 2026 03:53:48 -0400 Subject: [PATCH 008/388] drm/asahi: wake G17 render publication when its pair is returned A render whose pair was out on loan slept 200 us before trying again. The event worker lends each pair with completions out for an off-lock observation after every completion event. At queue depths of two and more, completions and submissions interleave, so a submission often found its pair on loan for a few microseconds and then slept the full 200 us. Count the device's change notifications. Every pair return already notifies under the device mutex. Wait for the notification under that mutex, unless one arrived since the attempt started, with the same deadline as before. --- drivers/gpu/drm/asahi/g17/mod.rs | 15 +++++++++++++++ drivers/gpu/drm/asahi/g17/runtime.rs | 4 ++-- drivers/gpu/drm/asahi/g17/runtime/render/grow.rs | 2 +- .../gpu/drm/asahi/g17/runtime/render/observe.rs | 2 +- .../gpu/drm/asahi/g17/runtime/render/prepare.rs | 6 +++--- .../gpu/drm/asahi/g17/runtime/render/submit.rs | 14 ++++++++++---- 6 files changed, 32 insertions(+), 11 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/mod.rs b/drivers/gpu/drm/asahi/g17/mod.rs index 282ddcc5e6b0a..3e03c40442c9d 100644 --- a/drivers/gpu/drm/asahi/g17/mod.rs +++ b/drivers/gpu/drm/asahi/g17/mod.rs @@ -53,6 +53,7 @@ use core::any::Any; use core::ops::Range; use core::sync::atomic::{ AtomicBool, + AtomicU64, Ordering, // }; @@ -793,6 +794,9 @@ struct Shared { state: Mutex>>, #[pin] changed: CondVar, + /// Counts `changed` notifications, so that a waiter which checked under + /// the device mutex can tell whether it already missed one. + changes: AtomicU64, } impl_has_work! { @@ -882,6 +886,16 @@ impl Shared { drop(retired); } + /// Wakes the waiters for a returned pair, binding or capacity change. + fn note_changed(&self) { + self.changes.fetch_add(1, Ordering::AcqRel); + self.changed.notify_all(); + } + + fn changes(&self) -> u64 { + self.changes.load(Ordering::Acquire) + } + /// Queues the event worker. Callable from any context. fn queue_events(self: &Arc) { // Already queued if this fails, which is just as good. @@ -1061,6 +1075,7 @@ impl Gpu { stop <- new_mutex!((), "g17::Shared::stop"), state <- new_mutex!(None, "g17::Shared::state"), changed <- new_condvar!("g17::Shared::changed"), + changes: AtomicU64::new(0), }), GFP_KERNEL, )?; diff --git a/drivers/gpu/drm/asahi/g17/runtime.rs b/drivers/gpu/drm/asahi/g17/runtime.rs index 284c5d7bc1eb9..3a496d2728f76 100644 --- a/drivers/gpu/drm/asahi/g17/runtime.rs +++ b/drivers/gpu/drm/asahi/g17/runtime.rs @@ -594,7 +594,7 @@ impl Backend { .queues .compute_building = false; } - self.shared.changed.notify_all(); + self.shared.note_changed(); drop(lease); if result != Err(EAGAIN) { return result; @@ -654,7 +654,7 @@ impl Backend { .queues .compute_scheduler_building = false; } - self.shared.changed.notify_all(); + self.shared.note_changed(); result } diff --git a/drivers/gpu/drm/asahi/g17/runtime/render/grow.rs b/drivers/gpu/drm/asahi/g17/runtime/render/grow.rs index e3cb174721aaa..20569af84bc7b 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render/grow.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render/grow.rs @@ -280,7 +280,7 @@ impl crate::g17::Shared { return Err(EIO); } firmware.queues.render.grow.active = None; - self.changed.notify_all(); + self.note_changed(); if !lease.is_current() || request.halt_count != firmware.recovery.generation() { continue; } diff --git a/drivers/gpu/drm/asahi/g17/runtime/render/observe.rs b/drivers/gpu/drm/asahi/g17/runtime/render/observe.rs index 4ede20d316c64..52bdd964ca4ae 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render/observe.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render/observe.rs @@ -164,7 +164,7 @@ impl crate::g17::Shared { return Err(error); } let firmware = (*state).as_deref_mut().ok_or(EIO)?; - self.changed.notify_all(); + self.note_changed(); if firmware.render_grow_pending() || firmware.queues.render.take_missed_event(slot as u8) { self.queue_events(); } diff --git a/drivers/gpu/drm/asahi/g17/runtime/render/prepare.rs b/drivers/gpu/drm/asahi/g17/runtime/render/prepare.rs index e2034c3176595..04afe2e05c107 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render/prepare.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render/prepare.rs @@ -426,7 +426,7 @@ impl super::super::Backend { } firmware.render_grow_ready() }; - self.shared.changed.notify_all(); + self.shared.note_changed(); if grow_ready { self.shared.queue_grow(); } @@ -548,7 +548,7 @@ impl super::super::Backend { .release(id)?; } } - self.shared.changed.notify_all(); + self.shared.note_changed(); if grow_ready { self.shared.queue_grow(); } @@ -711,7 +711,7 @@ impl super::super::Backend { .ok_or(EIO)? .release_owner(); } - self.shared.changed.notify_all(); + self.shared.note_changed(); let published = (|| { let entry = firmware.queues.render.entry(slot)?; if entry.pending_error.is_some() || entry.pending_timeout { diff --git a/drivers/gpu/drm/asahi/g17/runtime/render/submit.rs b/drivers/gpu/drm/asahi/g17/runtime/render/submit.rs index 6c5710554e7f5..e7d0cf1168c98 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render/submit.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render/submit.rs @@ -144,7 +144,7 @@ impl Backend { /// scheduler it last ran for to be consumed and the pair parked. The event /// worker observes both, so kick it on every poll; time out only after the /// same bound as a capacity wait. - fn wait_render_busy(&self, since: &mut Option, sleep: bool) -> Result { + fn wait_render_busy(&self, since: &mut Option, seen: u64, sleep: bool) -> Result { if !sleep { return Err(FALLBACK); } @@ -153,7 +153,12 @@ impl Backend { return Err(ETIMEDOUT); } self.shared.queue_events(); - fsleep(Delta::from_micros(200)); + // Every pair return notifies under the device mutex: wake on it rather + // than sleeping out a fixed interval behind a short observation loan. + let mut state = self.shared.state.lock(); + if self.shared.changes() == seen { + let _ = self.shared.changed.wait_timeout(&mut state, kernel::time::msecs_to_jiffies(1)); + } Ok(()) } @@ -194,6 +199,7 @@ impl Backend { let mut recovery_polls = 0; let capacity_started = super::super::now_ns(); loop { + let seen = self.shared.changes(); if packet.cancel_requested() { return Err(ECANCELED); } @@ -242,7 +248,7 @@ impl Backend { } Err(error) if error == EAGAIN => continue, Err(error) if error == EBUSY => { - self.wait_render_busy(&mut busy_since, sleep)?; + self.wait_render_busy(&mut busy_since, seen, sleep)?; continue; } result => result?, @@ -270,7 +276,7 @@ impl Backend { self.shared.queue_events(); fsleep(Delta::from_millis(1)); } else if error == EBUSY { - self.wait_render_busy(&mut busy_since, sleep)?; + self.wait_render_busy(&mut busy_since, seen, sleep)?; } else if !sleep { return Err(FALLBACK); } From abd85935bd4db7db6be23b8194b1173c9a8988c5 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Fri, 23 Oct 2026 11:05:39 -0400 Subject: [PATCH 009/388] drm/asahi: clear each G17 compute descriptor only once Object::initialize already clears the complete descriptor before calling its encoder. Fill that zeroed representation directly instead of clearing the same 16 KiB again. Name the encoder write_zeroed to make its precondition explicit. The firmware bytes and validation remain unchanged. The matched module passed the targeted compute/synchronization gate and checked GPU readbacks. --- drivers/gpu/drm/asahi/g17/fw/compute.rs | 8 ++++---- drivers/gpu/drm/asahi/g17/queue/compute.rs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/fw/compute.rs b/drivers/gpu/drm/asahi/g17/fw/compute.rs index 0cb7304e30677..b1afdad816b6b 100644 --- a/drivers/gpu/drm/asahi/g17/fw/compute.rs +++ b/drivers/gpu/drm/asahi/g17/fw/compute.rs @@ -12,7 +12,7 @@ use kernel::prelude::*; use super::kick::{KickTimestamp, RegisterArray, RegisterArrayBinding, RegisterWrite}; use super::queue::CommandTag; use super::{ - clear, offset_va, sampler_max, stamp_value, timestamp_end_va, work_key, ObjectIds, UNK_0A599, + offset_va, sampler_max, stamp_value, timestamp_end_va, work_key, ObjectIds, UNK_0A599, UNK_0D411, UNK_101D9, UNK_10791, UNK_1A0E9, UNK_1C8F8, }; @@ -507,8 +507,9 @@ impl ComputeDescriptor { const UNK_FBA: u32 = 0xe0a0_0001; const UNK_FC5: u8 = 0x9f; - /// Writes the complete descriptor. - pub(crate) fn write(&mut self, args: &ComputeArgs) -> Result { + /// Fills a descriptor cleared by `Object::initialize`. The unused firmware + /// fields and padding retain that zero initialization. + pub(crate) fn write_zeroed(&mut self, args: &ComputeArgs) -> Result { if args.cdm_end_va < args.cdm_va || (args.sampler_heap_va == 0) != (args.sampler_count == 0) || args.sampler_heap_va & 7 != 0 @@ -522,7 +523,6 @@ impl ComputeDescriptor { let timestamp_end_va = timestamp_end_va(args.timestamp_va)?; let sampler_max = sampler_max(args.sampler_count)?; - clear(self); self.tag = CommandTag::Compute as u32; self.context_id = args.context_id.into(); self.work_state_va = args.work_state_va; diff --git a/drivers/gpu/drm/asahi/g17/queue/compute.rs b/drivers/gpu/drm/asahi/g17/queue/compute.rs index 713809907dd4b..b75e00da0dad4 100644 --- a/drivers/gpu/drm/asahi/g17/queue/compute.rs +++ b/drivers/gpu/drm/asahi/g17/queue/compute.rs @@ -1904,7 +1904,7 @@ impl Queue { .object .initialize::( kick_timestamp.slot() % DESCRIPTORS * COMPUTE_DESCRIPTOR_SIZE, - |descriptor| descriptor.write(&args), + |descriptor| descriptor.write_zeroed(&args), )?; if first { self.graph.context_high.write( From 3ef3720890f8c085f18e4f5e35db6853075b121c Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Fri, 23 Oct 2026 11:05:53 -0400 Subject: [PATCH 010/388] drm/asahi: retain compute pool backing through pending channel admission A publisher waiting for a physical compute channel reclaimed idle USC backing even though freeing backing cannot create another QID. The normal warm-pool limit could also evict a retired channel while another runnable publisher was waiting to reuse it. Subsequent handoffs repeatedly allocated and cleared fresh pool backing. Distinguish channel pressure from backing allocation pressure. Track unbound compute admission through publication, cancellation and error, and retain backing for the existing compute channels while that demand remains. The last publisher schedules normal idle reclamation. Allocation-pressure reclamation, the idle warm-pool limit and expiry remain in force. Expose the admission count in the capacity diagnostics. On Neo, the matched 16-client probe drops pool allocations from 4888 to 53 and median submission time from 257 to 109 us. The Asahi KUnit suites pass 67/67; the targeted compute/synchronization gate passes 116 with 18 skips. Full mustpass qualification remains pending. --- drivers/gpu/drm/asahi/g17/capacity.rs | 1 + drivers/gpu/drm/asahi/g17/mod.rs | 4 ++ drivers/gpu/drm/asahi/g17/runtime.rs | 57 ++++++++++++++++++++++----- 3 files changed, 52 insertions(+), 10 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/capacity.rs b/drivers/gpu/drm/asahi/g17/capacity.rs index bcbbb2e7e875d..8a7f48d8a3f35 100644 --- a/drivers/gpu/drm/asahi/g17/capacity.rs +++ b/drivers/gpu/drm/asahi/g17/capacity.rs @@ -55,6 +55,7 @@ impl kernel::debugfs::Writer for View { let state = self.0.state.lock(); let Some(firmware) = state.as_deref() else { return writeln!(f, "online=0"); }; writeln!(f, "online=1")?; + writeln!(f, "compute_admission_demand={}", self.0.compute_demand.load(Ordering::Acquire))?; firmware.queues.write_capacity(f)?; firmware.pool.write_capacity(f)?; writeln!(f, "render_pool_ids used={} total=127", firmware.render_ids.used())?; diff --git a/drivers/gpu/drm/asahi/g17/mod.rs b/drivers/gpu/drm/asahi/g17/mod.rs index 3e03c40442c9d..6b9e9903f3a0d 100644 --- a/drivers/gpu/drm/asahi/g17/mod.rs +++ b/drivers/gpu/drm/asahi/g17/mod.rs @@ -53,6 +53,7 @@ use core::any::Any; use core::ops::Range; use core::sync::atomic::{ AtomicBool, + AtomicU32, AtomicU64, Ordering, // }; @@ -772,6 +773,8 @@ struct Shared { #[pin] feed_work: Work, preparations: Arc, + /// Unbound compute publishers which can reuse a retired channel. + compute_demand: AtomicU32, #[pin] work: Work, #[pin] @@ -1066,6 +1069,7 @@ impl Gpu { feed, feed_work <- new_work!("g17::Shared::feed_work"), preparations, + compute_demand: AtomicU32::new(0), work <- new_work!("g17::Shared::work"), poll <- new_delayed_work!("g17::Shared::poll"), idle <- new_delayed_work!("g17::Shared::idle"), diff --git a/drivers/gpu/drm/asahi/g17/runtime.rs b/drivers/gpu/drm/asahi/g17/runtime.rs index 3a496d2728f76..beffae857a034 100644 --- a/drivers/gpu/drm/asahi/g17/runtime.rs +++ b/drivers/gpu/drm/asahi/g17/runtime.rs @@ -34,6 +34,30 @@ const FIRST_COMPUTE_QID: u8 = 4; const RECOVERY_SETTLE_POLLS: usize = 200; const WORK_RING_WAIT_NS: u64 = super::job::TIMEOUT_MS as u64 * 1_000_000; +/// Pending admission keeps exactly retired compute backing available for reuse. +/// The guard spans retries and disappears on publication, cancellation or error. +/// It borrows Shared, so the accounting cannot retain the device after teardown. +struct ComputeDemand<'a>(&'a Arc); + +impl<'a> ComputeDemand<'a> { + fn new(shared: &'a Arc) -> Self { + // Each admitted logical queue has at most one scheduler publisher. + // The execution-context bound is far below the counter's range. + shared.compute_demand.fetch_add(1, Ordering::AcqRel); + Self(shared) + } +} + +impl Drop for ComputeDemand<'_> { + fn drop(&mut self) { + if self.0.compute_demand.fetch_sub(1, Ordering::AcqRel) == 1 { + // Reinstate normal idle retention even if the last publisher was + // cancelled before a kick could produce another event. + self.0.queue_events(); + } + } +} + /// A reservation remains identifiable while its backing is built without the /// device mutex. Failed construction returns the slot, never its host identity. #[derive(Clone, Copy)] @@ -782,7 +806,7 @@ impl Backend { Err(error) if error == ENOSPC => { drop(state); drop(lease); - self.wait_compute_capacity(admission)?; + self.wait_compute_capacity(admission, true)?; continue; } result => result?, @@ -795,11 +819,12 @@ impl Backend { let reservation = match firmware.queues.reserve_compute(self.owner) { Ok(reservation) => reservation, Err(error) if error == EAGAIN => { - // The event worker owns release observation and - // drops vacated aliases/backing off this mutex. + // All physical channels are occupied. Retain their + // USC backing for exact-retirement handoff; releasing + // it cannot create a QID and forces fresh allocation. drop(state); drop(lease); - self.wait_compute_capacity(admission)?; + self.wait_compute_capacity(admission, false)?; continue; } Err(error) if error == ENOSPC => { @@ -833,7 +858,7 @@ impl Backend { let (pages, ring) = match built { Err(error) if error == ENOSPC => { drop(lease); - self.wait_compute_capacity(admission)?; + self.wait_compute_capacity(admission, true)?; continue; } result => result?, @@ -922,7 +947,7 @@ impl Backend { } drop(lease); if result == Err(ENOSPC) { - self.wait_compute_capacity(admission)?; + self.wait_compute_capacity(admission, true)?; continue; } if result != Err(EAGAIN) { return result; } @@ -1423,6 +1448,7 @@ impl Backend { // A bound queue needs none of ensure_compute's admission passes. Try // without them; the publication section below checks the same state. let mut bound = true; + let mut demand = None; let mut recovery_polls = 0; let mut state_delay = 20i64; let deadline = now_ns().saturating_add(WORK_RING_WAIT_NS); @@ -1439,6 +1465,7 @@ impl Backend { // Recheck on every retry: pressure may yield an idle binding between // admission and publication without changing this logical queue. if !bound { + demand.get_or_insert_with(|| ComputeDemand::new(&self.shared)); Backend::ensure_compute(self, scratch.pages(), &admission)?; admission.check()?; } @@ -1670,6 +1697,11 @@ impl super::Firmware { // retained compute pool stays warm: render and new compute need it more. let cap = if super::freelist::lower_pressure() { 0 + } else if self.primary.state.shared.compute_demand.load(Ordering::Acquire) != 0 { + // A retired channel being requested by another runnable publisher + // is not an unused warm pool. Keep its existing backing through + // handoff; real allocation pressure above still forces reclamation. + super::kick::COMPUTE_CHANNELS } else { COMPUTE_WARM_CAP }; @@ -1714,10 +1746,10 @@ impl Backend { pub(super) fn wait_capacity(&self, started: u64) -> Result { let check = || self.admission_open(); let admission = Admission::new(started.saturating_add(WORK_RING_WAIT_NS), &check); - self.wait_compute_capacity(&admission) + self.wait_compute_capacity(&admission, true) } - fn wait_compute_capacity(&self, admission: &Admission<'_>) -> Result { + fn wait_compute_capacity(&self, admission: &Admission<'_>, reclaim_backing: bool) -> Result { admission.check()?; let mut deferred = DeferredBatch::publication()?; let result: Result = (|| { @@ -1738,8 +1770,13 @@ impl Backend { } // Consumption, not publication or an elapsed timeout, frees backing. firmware.queues.observe_pool_releases(&firmware.init, &mut deferred)?; - firmware.queues.reclaim_idle_render_pools(&firmware.init, &mut deferred)?; - if !firmware.render_control_backpressured() { + // Pool eviction addresses backing allocation pressure, not channel + // occupancy. Normal idle expiry and the retained-pool cap still run + // in the event worker for either kind of admission wait. + if reclaim_backing { + firmware.queues.reclaim_idle_render_pools(&firmware.init, &mut deferred)?; + } + if reclaim_backing && !firmware.render_control_backpressured() { let (published, _) = firmware.queues.vacate_idle_pools(&firmware.init, 0, now_ns(), 0)?; if published != 0 { firmware.primary.notify(super::MSG_CONTROL_NOTIFY)?; } } From f35d7ea1b37395a9bc1241d0fafcb698e0c12d46 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Fri, 23 Oct 2026 11:11:30 -0400 Subject: [PATCH 011/388] dma-buf: stop polling an array after its first pending member Fence arrays require every member to signal. During syncobj timeline walks, a pending predecessor already proves that the array is incomplete, yet the lazy signaled callback polls every successor too. Return false immediately at that predecessor, keeping callback accounting and the enabled-signaling path unchanged. Add checks for out-of-order completion, avoiding successor polls, and callback installation after a partial lazy poll. Remove the obsolete signal-on-any description from the constructor documentation. --- drivers/dma-buf/dma-fence-array.c | 29 ++++++----- drivers/dma-buf/st-dma-fence-unwrap.c | 75 +++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 14 deletions(-) diff --git a/drivers/dma-buf/dma-fence-array.c b/drivers/dma-buf/dma-fence-array.c index 5e10e8df372f1..64abdcc430580 100644 --- a/drivers/dma-buf/dma-fence-array.c +++ b/drivers/dma-buf/dma-fence-array.c @@ -107,17 +107,11 @@ static bool dma_fence_array_signaled(struct dma_fence *fence) unsigned int i; /* - * We need to read num_pending before checking the enable_signal bit - * to avoid racing with the enable_signaling() implementation, which - * might decrement the counter, and cause a partial check. - * atomic_read_acquire() pairs with atomic_dec_and_test() in - * dma_fence_array_enable_signaling() - * - * The !--num_pending check is here to account for the any_signaled case - * if we race with enable_signaling(), that means the !num_pending check - * in the is_signalling_enabled branch might be outdated (num_pending - * might have been decremented), but that's fine. The user will get the - * right value when testing again later. + * Read the counter before the enable bit so callback installation + * cannot make a partial count look complete. The acquire pairs with + * the counter decrements in dma_fence_array_enable_signaling(). + * If enabling races with the lazy scan, every child must still be + * observed complete before that scan can return true. */ num_pending = atomic_read_acquire(&array->num_pending); if (test_bit(DMA_FENCE_FLAG_ENABLE_SIGNAL_BIT, &array->base.flags)) { @@ -126,8 +120,16 @@ static bool dma_fence_array_signaled(struct dma_fence *fence) return false; } + /* + * Arrays require every member to complete. A pending member already + * proves the array incomplete; polling its successors only repeats work + * during timeline-chain walks. Keep the completion counter and the + * enabled-signaling path unchanged. + */ for (i = 0; i < array->num_fences; ++i) { - if (dma_fence_is_signaled(array->fences[i]) && !--num_pending) + if (!dma_fence_is_signaled(array->fences[i])) + return false; + if (!--num_pending) goto signal; } return false; @@ -256,8 +258,7 @@ EXPORT_SYMBOL(dma_fence_array_init); * and fill it with the fences it wants to add to the object. Ownership of this * array is taken and dma_fence_put() is used on each fence on release. * - * If @signal_on_any is true the fence array signals if any fence in the array - * signals, otherwise it signals when all fences in the array signal. + * The array signals when every fence in the array signals. */ struct dma_fence_array *dma_fence_array_create(int num_fences, struct dma_fence **fences, diff --git a/drivers/dma-buf/st-dma-fence-unwrap.c b/drivers/dma-buf/st-dma-fence-unwrap.c index 4d9d313b460cb..17761f8de04a7 100644 --- a/drivers/dma-buf/st-dma-fence-unwrap.c +++ b/drivers/dma-buf/st-dma-fence-unwrap.c @@ -15,6 +15,7 @@ struct mock_fence { struct dma_fence base; spinlock_t lock; + atomic_t polls; }; static const char *mock_name(struct dma_fence *f) @@ -22,9 +23,18 @@ static const char *mock_name(struct dma_fence *f) return "mock"; } +static bool mock_signaled(struct dma_fence *fence) +{ + struct mock_fence *f = container_of(fence, struct mock_fence, base); + + atomic_inc(&f->polls); + return false; +} + static const struct dma_fence_ops mock_ops = { .get_driver_name = mock_name, .get_timeline_name = mock_name, + .signaled = mock_signaled, }; static struct dma_fence *__mock_fence(u64 context, u64 seqno) @@ -36,6 +46,7 @@ static struct dma_fence *__mock_fence(u64 context, u64 seqno) return NULL; spin_lock_init(&f->lock); + atomic_set(&f->polls, 0); dma_fence_init(&f->base, &mock_ops, &f->lock, context, seqno); return &f->base; @@ -584,7 +595,71 @@ static void test_unwrap_merge_complex_seqno(struct kunit *test) dma_fence_put(f1); } +/* + * A pending all-of array needs no polls of later members. Keep its + * independent member fences so their polling and out-of-order completion + * remain observable without enabling callbacks on the array. + */ +static void test_array_pending_prefix(struct kunit *test) +{ + struct dma_fence *first = mock_fence(), *last = mock_fence(); + struct dma_fence *array; + struct mock_fence *last_mock; + + if (!first || !last) { + KUNIT_FAIL(test, "Could not allocate member fences"); + goto out; + } + last_mock = container_of(last, struct mock_fence, base); + array = mock_array(2, dma_fence_get(first), dma_fence_get(last)); + if (!array) { + KUNIT_FAIL(test, "Could not allocate fence array"); + goto out; + } + KUNIT_EXPECT_FALSE(test, dma_fence_is_signaled(array)); + KUNIT_EXPECT_EQ(test, atomic_read(&last_mock->polls), 0); + dma_fence_signal(last); + KUNIT_EXPECT_FALSE(test, dma_fence_is_signaled(array)); + dma_fence_signal(first); + KUNIT_EXPECT_TRUE(test, dma_fence_is_signaled(array)); + dma_fence_put(array); +out: + dma_fence_put(last); + dma_fence_put(first); +} + +static void test_array_pending_suffix(struct kunit *test) +{ + struct dma_fence *first = mock_fence(), *last = mock_fence(); + struct dma_fence *array; + + if (!first || !last) { + KUNIT_FAIL(test, "Could not allocate member fences"); + goto out; + } + array = mock_array(2, dma_fence_get(first), dma_fence_get(last)); + if (!array) { + KUNIT_FAIL(test, "Could not allocate fence array"); + goto out; + } + dma_fence_signal(first); + KUNIT_EXPECT_FALSE(test, dma_fence_is_signaled(array)); + /* Installing callbacks after a lazy partial poll must still account + * for the already signaled first member and wait for the last. + */ + dma_fence_enable_signaling(array); + KUNIT_EXPECT_FALSE(test, dma_fence_is_signaled(array)); + dma_fence_signal(last); + KUNIT_EXPECT_GT(test, dma_fence_wait_timeout(array, false, HZ), 0L); + dma_fence_put(array); +out: + dma_fence_put(last); + dma_fence_put(first); +} + static struct kunit_case dma_fence_unwrap_cases[] = { + KUNIT_CASE(test_array_pending_prefix), + KUNIT_CASE(test_array_pending_suffix), KUNIT_CASE(test_sanitycheck), KUNIT_CASE(test_unwrap_array), KUNIT_CASE(test_unwrap_chain), From 319805fed41bb181db8e90378beaf905f38f6297 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Fri, 23 Oct 2026 14:38:24 -0400 Subject: [PATCH 012/388] drm/asahi: avoid unbound compute inline attempts on compute-only queues Compute-only contexts that yield their physical channel still take the device mutex in the inline submit path before falling back to admission. Track an advisory binding hint to skip that attempt. Actual publication retains the locked VM, epoch and binding checks, and scheduler admission ordering is unchanged. Mark contexts with validated rendering before any batch member is queued, so mixed-engine queues retain the original inline path. Validated with 69 Asahi KUnit cases, targeted compute/sync CTS (116 pass, 18 skip), mixed device churn and 16-client concurrent GPU readbacks. Matched 16-process quick submission time improves from 117.27 to 89.55 us; paired render follow-ups remain at control performance. --- drivers/gpu/drm/asahi/g17/context.rs | 19 +++++++++++++++++++ drivers/gpu/drm/asahi/g17/queue/compute.rs | 5 +++++ drivers/gpu/drm/asahi/g17/queue/mod.rs | 3 +++ drivers/gpu/drm/asahi/g17/runtime.rs | 3 +++ 4 files changed, 30 insertions(+) diff --git a/drivers/gpu/drm/asahi/g17/context.rs b/drivers/gpu/drm/asahi/g17/context.rs index c93ab25d55ce3..445b282dcf5b6 100644 --- a/drivers/gpu/drm/asahi/g17/context.rs +++ b/drivers/gpu/drm/asahi/g17/context.rs @@ -114,6 +114,11 @@ pub(crate) struct Context { scheduler: Arc, scheduler_generation: u32, published: AtomicBool, + /// Advisory only: publication still checks the physical binding under the + /// device mutex. False skips an inline attempt known to need admission. + compute_binding_hint: AtomicBool, + /// Mixed-engine contexts keep the original inline publication path. + render_seen: AtomicBool, qos: QosLease, owner_pid: u32, states: WorkStates, @@ -189,6 +194,8 @@ impl Context { scheduler, scheduler_generation, published: AtomicBool::new(false), + compute_binding_hint: AtomicBool::new(false), + render_seen: AtomicBool::new(false), qos, owner_pid, states: WorkStates::new(), @@ -203,6 +210,18 @@ impl Context { pub(crate) fn vm(&self) -> &Vm { self.execution.vm() } + pub(crate) fn compute_binding_hint(&self) -> bool { + self.render_seen.load(Ordering::Acquire) + || self.compute_binding_hint.load(Ordering::Relaxed) + } + pub(crate) fn note_render(&self) { + if !self.render_seen.load(Ordering::Relaxed) { + self.render_seen.store(true, Ordering::Release); + } + } + pub(crate) fn note_compute_binding(&self, owned: bool) { + self.compute_binding_hint.store(owned, Ordering::Relaxed); + } pub(crate) fn compute_timestamps(&self) -> &Arc { &self.compute_timestamps } diff --git a/drivers/gpu/drm/asahi/g17/queue/compute.rs b/drivers/gpu/drm/asahi/g17/queue/compute.rs index b75e00da0dad4..d2ec8da9c6082 100644 --- a/drivers/gpu/drm/asahi/g17/queue/compute.rs +++ b/drivers/gpu/drm/asahi/g17/queue/compute.rs @@ -557,6 +557,7 @@ impl Queue { let pool_alias = pool.map_client(context.vm())?; let binding = graph.bind(context.clone(), kick_alias, pool_alias)?; context.mark_published(); + context.note_compute_binding(true); Ok(Self { owner: Some(owner), retained_owner: None, @@ -1274,6 +1275,7 @@ impl Queue { self.retained_owner = None; } context.mark_published(); + context.note_compute_binding(true); self.owner = Some(owner); self.unpublished_binding = true; self.released = false; @@ -1284,6 +1286,9 @@ impl Queue { /// True allows the registry to cancel the never-published QID and drop this graph. An /// installed graph remains retained even after the logical owner exits without work. pub(crate) fn release_owner(&mut self) -> Result { + if let Some(binding) = &self.binding { + binding.context.note_compute_binding(false); + } if !self.installed && self.active.len == 0 && !self.quarantined && self.previous.is_none() { return Ok(true); } diff --git a/drivers/gpu/drm/asahi/g17/queue/mod.rs b/drivers/gpu/drm/asahi/g17/queue/mod.rs index 1f57f2913a020..96e566d3971ff 100644 --- a/drivers/gpu/drm/asahi/g17/queue/mod.rs +++ b/drivers/gpu/drm/asahi/g17/queue/mod.rs @@ -409,6 +409,9 @@ impl crate::queue::Queue for Queue { return Err(EIO); } let (mut pending, guards) = self.prepare_submission(bytes, objects)?; + if pending.iter().any(|(command, _, _)| matches!(command, Validated::Render { .. })) { + self.context.note_render(); + } if super::timeout::select_submission(pending.iter().map(|(cmd, _, _)| cmd.long_job())) { for (command, _, _) in pending.iter_mut() { command.allow_long_job(); } } diff --git a/drivers/gpu/drm/asahi/g17/runtime.rs b/drivers/gpu/drm/asahi/g17/runtime.rs index beffae857a034..dbae7ea2d4021 100644 --- a/drivers/gpu/drm/asahi/g17/runtime.rs +++ b/drivers/gpu/drm/asahi/g17/runtime.rs @@ -1411,6 +1411,9 @@ impl Backend { result?; } } + if !self.context.compute_binding_hint() { + return Err(render::FALLBACK); + } let mut state = self.shared.state.lock(); let firmware = (*state).as_deref_mut().ok_or(ENODEV)?; if !lease.is_current() From 5a4822c6fb3d1fc0a83d9b754b477bf1db35d782 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Fri, 23 Oct 2026 15:05:59 -0400 Subject: [PATCH 013/388] drm/asahi: preserve low-pressure compute inline probes Only bypass an unbound compute-only inline attempt when multiple publishers are waiting for admission. A lone admission retains the original probe and its latency behavior. Actual publication still uses the existing locked state checks. Matched quick controls retain queue signaling latency (174.1 to 174.5 us) while 16-process submission time improves from 117.3 to 90.7 us. All 20 performance rows complete successfully. Checked mixed/concurrent GPU readbacks, 69 Asahi KUnit cases and targeted compute/sync CTS also pass. --- drivers/gpu/drm/asahi/g17/runtime.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/asahi/g17/runtime.rs b/drivers/gpu/drm/asahi/g17/runtime.rs index dbae7ea2d4021..bd95148e9eaf0 100644 --- a/drivers/gpu/drm/asahi/g17/runtime.rs +++ b/drivers/gpu/drm/asahi/g17/runtime.rs @@ -1411,7 +1411,11 @@ impl Backend { result?; } } - if !self.context.compute_binding_hint() { + // A lone admission keeps the original latency-sensitive probe. + // Competing publishers otherwise spin on a binding they do not own. + if !self.context.compute_binding_hint() + && self.shared.compute_demand.load(Ordering::Relaxed) > 1 + { return Err(render::FALLBACK); } let mut state = self.shared.state.lock(); From 026065c435260844e39d05a182e99b128c5f9b2d Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Fri, 23 Oct 2026 20:29:12 -0400 Subject: [PATCH 014/388] drm/asahi: submit same-VM compute input waits through firmware Recognize eligible singleton compute producers through their exported aggregate fences. Order queued consumers after actual producer publication using the existing DRM scheduled fence, then validate the current physical ticket and emit firmware waits. Preserve CPU waits for unsupported dependencies and host outputs, and revalidate recovery epochs before acquiring consumer capacity. Retain source binding loans through consumer retirement, propagate source errors before dependent completion, and release failed submission pins when the last runtime owner retires even if exported fence references survive. Keep bounded member counters in byte atomics so submission state retains its original allocation class. Validated with 78 KUnit tests, the compute/synchronization CTS gate, chained readback, process exits and mixed compute/render pressure. Fixed-core ordinary compute matches the control while the queue-hop path improves. Existing intermittent unrelated-client loss during forced recovery remains unresolved. --- drivers/gpu/drm/asahi/Makefile | 2 +- drivers/gpu/drm/asahi/g17/capacity.rs | 4 + drivers/gpu/drm/asahi/g17/completion.rs | 32 +- drivers/gpu/drm/asahi/g17/context.rs | 1 + drivers/gpu/drm/asahi/g17/fence.rs | 366 +++++++++++++------ drivers/gpu/drm/asahi/g17/job.rs | 8 + drivers/gpu/drm/asahi/g17/mod.rs | 1 + drivers/gpu/drm/asahi/g17/queue/compute.rs | 112 +++++- drivers/gpu/drm/asahi/g17/queue/mod.rs | 35 +- drivers/gpu/drm/asahi/g17/runtime.rs | 127 ++++++- drivers/gpu/drm/asahi/g17/runtime/backend.rs | 49 +++ drivers/gpu/drm/asahi/g17/sync_input.c | 42 +++ drivers/gpu/drm/asahi/g17/sync_input.h | 18 + drivers/gpu/drm/asahi/g17/sync_input.rs | 245 +++++++++++++ 14 files changed, 905 insertions(+), 137 deletions(-) create mode 100644 drivers/gpu/drm/asahi/g17/sync_input.c create mode 100644 drivers/gpu/drm/asahi/g17/sync_input.h create mode 100644 drivers/gpu/drm/asahi/g17/sync_input.rs diff --git a/drivers/gpu/drm/asahi/Makefile b/drivers/gpu/drm/asahi/Makefile index bc0daf03a04d1..8f8f679532d2b 100644 --- a/drivers/gpu/drm/asahi/Makefile +++ b/drivers/gpu/drm/asahi/Makefile @@ -1,5 +1,5 @@ # SPDX-License-Identifier: GPL-2.0 obj-$(CONFIG_DRM_ASAHI) += asahi.o -asahi-y := asahi_core.o sysfs.o +asahi-y := asahi_core.o sysfs.o g17/sync_input.o asahi-$(CONFIG_DRM_ASAHI_G16) += g16/glue.o diff --git a/drivers/gpu/drm/asahi/g17/capacity.rs b/drivers/gpu/drm/asahi/g17/capacity.rs index 8a7f48d8a3f35..57406e5487a37 100644 --- a/drivers/gpu/drm/asahi/g17/capacity.rs +++ b/drivers/gpu/drm/asahi/g17/capacity.rs @@ -55,6 +55,10 @@ impl kernel::debugfs::Writer for View { let state = self.0.state.lock(); let Some(firmware) = state.as_deref() else { return writeln!(f, "online=0"); }; writeln!(f, "online=1")?; + let (converted, published) = super::sync_input::counters(); + writeln!(f, "fw_inputs converted={} published={} native={} pending={}", + converted, published, super::sync_input::native_count(), + super::sync_input::pending_count())?; writeln!(f, "compute_admission_demand={}", self.0.compute_demand.load(Ordering::Acquire))?; firmware.queues.write_capacity(f)?; firmware.pool.write_capacity(f)?; diff --git a/drivers/gpu/drm/asahi/g17/completion.rs b/drivers/gpu/drm/asahi/g17/completion.rs index dacd6de9a3a48..406efc0e221fe 100644 --- a/drivers/gpu/drm/asahi/g17/completion.rs +++ b/drivers/gpu/drm/asahi/g17/completion.rs @@ -92,6 +92,8 @@ impl Destinations { Ok(Self(retained)) } + pub(crate) fn has_outputs(&self) -> bool { self.0.is_some() } + fn iter(&self) -> core::slice::Iter<'_, Option> { let destinations: &[Option] = self.0.as_deref().map_or(&[], |destinations| destinations.as_slice()); @@ -254,6 +256,7 @@ pub(crate) struct Completion { terminal: Option>, compute: Option, feed: Option>, + sources: Option>, #[pin] vm_job: Mutex>, #[pin] @@ -286,6 +289,7 @@ impl Completion { compute: bool, host_predecessors: bool, feed: Option>, + sources: Option>, ) -> Result> { let fence = fence::independent(contexts)?; let ordered = !compute @@ -313,6 +317,7 @@ impl Completion { terminal, compute, feed, + sources, vm_job <- new_mutex!(Some(vm_job), "G17 command VM pin"), render <- new_mutex!(None, "G17 command render lease"), signalled: AtomicBool::new(false), @@ -357,6 +362,26 @@ impl Completion { pub(crate) fn status(&self) -> &Arc { &self.status } + pub(super) fn sources(&self) -> Option<&Arc> { + self.sources.as_ref() + } + pub(super) fn is_input_source(&self, source: &Arc) -> bool { + self.output.output().member.is_source(source) + } + pub(super) fn set_source_ready(&self, ready: Fence) { + self.output.output().member.set_source_ready(ready); + } + pub(super) fn needs_source_ready(&self) -> bool { + self.output.output().member.needs_source_ready() + } + pub(super) fn note_input_error(&self, error: Error) -> bool { + self.output.output().member.note_error(error) + } + pub(super) fn check_source_inputs(&self) -> Result { + if let Some(sources) = &self.sources { sources.check()?; } + Ok(()) + } + pub(crate) fn work_state(&self) -> Result> { self.output.output().member.work_state() } @@ -525,6 +550,7 @@ impl Completion { /// An exact whole-pair drain or processor stop allows rehabilitation. The /// VM pin goes first; the still-held lease excludes reuse until both settle. pub(crate) fn release_after_retirement(&self) { + if let Some(sources) = &self.sources { sources.release(); } drop(self.vm_job.lock().take()); let lease = self.render.lock().take(); if let Some(lease) = lease.as_ref() { @@ -534,6 +560,7 @@ impl Completion { } fn release_healthy(&self) { + if let Some(sources) = &self.sources { sources.release(); } drop(self.render.lock().take()); drop(self.vm_job.lock().take()); } @@ -541,7 +568,10 @@ impl Completion { /// Exact retirement permits the physical lease and VM pin to be released /// before waking userspace. A failure fence alone never establishes this. #[track_caller] - pub(crate) fn complete(&self, result: Result<[u64; 4]>) { + pub(crate) fn complete(&self, mut result: Result<[u64; 4]>) { + if result.is_ok() { + if let Err(error) = self.check_source_inputs() { result = Err(error); } + } if self.signalled.swap(true, Ordering::AcqRel) { return; } diff --git a/drivers/gpu/drm/asahi/g17/context.rs b/drivers/gpu/drm/asahi/g17/context.rs index 445b282dcf5b6..1818fd8c6922d 100644 --- a/drivers/gpu/drm/asahi/g17/context.rs +++ b/drivers/gpu/drm/asahi/g17/context.rs @@ -354,6 +354,7 @@ pub(crate) struct WorkStateLease { word: AtomicU32, } impl WorkStateLease { + pub(super) fn status(&self) -> &Arc { self.context.status() } pub(crate) fn deferred(context: Arc) -> Self { Self { context, diff --git a/drivers/gpu/drm/asahi/g17/fence.rs b/drivers/gpu/drm/asahi/g17/fence.rs index 3414e36db9d36..25e5f6b04d558 100644 --- a/drivers/gpu/drm/asahi/g17/fence.rs +++ b/drivers/gpu/drm/asahi/g17/fence.rs @@ -7,10 +7,10 @@ use super::context::{Context, WorkStateLease}; use crate::file::SyncItem; -use core::sync::atomic::{AtomicBool, AtomicI32, AtomicU32, Ordering}; +use core::sync::atomic::{AtomicBool, AtomicI32, AtomicU8, AtomicUsize, Ordering}; use kernel::{ bindings, c_str, - dma_fence::{Fence, FenceContexts, FenceObject, FenceOps, RawDmaFence, UserFence}, + dma_fence::{Fence, FenceContexts, FenceObject, FenceOps, RawDmaFence, UniqueFence, UserFence}, new_mutex, prelude::*, str::CStr, @@ -48,106 +48,263 @@ pub(crate) fn allocation_failure(contexts: &FenceContexts) -> Result { Ok(Fence::from_fence(&fence)) } -/// All command members of an ioctl share its work-state node and aggregate fence. +/// The existing aggregate-fence allocation owns the existing submission state. +/// Submission never owns this fence, so exported references cannot form a cycle. +pub(crate) struct ExportFence { + source: Option>, +} +static EXPORT_OPS: AtomicUsize = AtomicUsize::new(0); + +#[vtable] +impl FenceOps for ExportFence { + fn get_driver_name<'a>(self: &'a FenceObject) -> &'a CStr { c_str!("asahi") } + fn get_timeline_name<'a>(self: &'a FenceObject) -> &'a CStr { + c_str!("g17-aggregate") + } +} + +#[inline(never)] +fn export_fallback(contexts: &FenceContexts) -> Result> { + let fence = contexts.new_fence(0, ExportFence { source: None })?; + // SAFETY: Unique, initialized and still private. Only this constructor + // publishes ExportFence objects; its payload is immutable after sharing. + let ops = unsafe { + (*fence.raw()).context = bindings::dma_fence_context_alloc(1); + (*fence.raw()).ops as usize + }; + let previous = EXPORT_OPS.load(Ordering::Acquire); + if previous == 0 { + match EXPORT_OPS.compare_exchange(0, ops, Ordering::AcqRel, Ordering::Acquire) { + Ok(_) => {}, + Err(previous) if previous == ops => {}, + Err(_) => return Err(EIO), + } + } else if previous != ops { + return Err(EIO); + } + Ok(fence) +} + +/// Caller retains the leaf fence throughout lookup. Exact registered operations +/// identify the repr(C) FenceObject payload, not a driver or timeline name. +pub(super) unsafe fn exported_source_raw(raw: *mut bindings::dma_fence) + -> Option> +{ + let expected = EXPORT_OPS.load(Ordering::Acquire); + if expected == 0 { return None; } + // SAFETY: The caller owns a live fence reference. ExportFence has a unique + // timeline callback and nontrivial payload drop. No cast precedes the exact + // registered vtable check. FenceObject begins with its dma_fence. + let object = unsafe { + if (*raw).ops as usize != expected { return None; } + &*(raw as *const FenceObject) + }; + object.source.as_ref().filter(|source| source.firmware_source).cloned() +} + +struct State { + work: Option>, + ready: Option, +} + +// One Member per parsed command, plus the enqueue guard. Exported references +// retain the allocation but do not contribute to either of these counters. +kernel::static_assert!(crate::file::MAX_COMMANDS_PER_SUBMISSION < u8::MAX as u32); + /// The initial member keeps the aggregate unsignalled until enqueue finishes. #[pin_data] pub(crate) struct Submission { - fence: UserFence, - /// Successful retirement drops only this bookkeeping owner; runtime owners - /// and failed submissions keep their work-state allocation. #[pin] - work_state: Mutex>>, - remaining: AtomicU32, + state: Mutex, + remaining: AtomicU8, error: AtomicI32, + /// Live Member objects, plus the enqueue guard, independently of signalling. + owners: AtomicU8, + firmware_source: bool, } impl Submission { - fn new(fence: UserFence, context: Arc) -> Result> { - let work_state = Arc::new(WorkStateLease::deferred(context), GFP_KERNEL)?; - Arc::pin_init( - pin_init!(Self { - fence, - work_state <- new_mutex!(Some(work_state), "G17 submission work state"), - remaining: AtomicU32::new(1), - error: AtomicI32::new(0), - }), - GFP_KERNEL, - ) - } - - pub(crate) fn fence(&self) -> Fence { - Fence::from_fence(&self.fence) - } - - /// Add one command before enqueue ends. The parser bounds the count to 64. - pub(crate) fn member(self: &Arc) -> Member { - self.remaining.fetch_add(1, Ordering::Relaxed); - Member { - submission: self.clone(), - complete: AtomicBool::new(false), - } + fn new(context: Arc, firmware_source: bool) -> Result> { + let work = Arc::new(WorkStateLease::deferred(context), GFP_KERNEL)?; + Self::with_work(Some(work), firmware_source) } - - fn member_done(&self, result: Result) { - if let Err(error) = result { - let _ = self.error.compare_exchange( - 0, - error.to_errno(), - Ordering::AcqRel, - Ordering::Acquire, - ); - } + fn with_work(work: Option>, firmware_source: bool) -> Result> { + Arc::pin_init(pin_init!(Self { + state <- new_mutex!(State { work, ready: None }, "G17 submission work state"), + remaining: AtomicU8::new(1), + error: AtomicI32::new(0), + owners: AtomicU8::new(1), + firmware_source, + }), GFP_KERNEL) + } + pub(super) fn source_status(&self) -> Result { + let complete = self.remaining.load(Ordering::Acquire) == 0; + let error = self.error.load(Ordering::Acquire); + if error != 0 { return Err(super::dependency::inherited_error(Error::from_errno(error))); } + Ok(complete) + } + pub(super) fn same_vm(&self, status: &Arc) -> bool { + self.state.lock().work.as_ref().is_some_and(|work| Arc::ptr_eq(work.status(), status)) + } + pub(super) fn ready(&self) -> Option { self.state.lock().ready.clone() } + fn set_ready(&self, ready: Fence) { + if !self.firmware_source { return; } + let mut state = self.state.lock(); + if state.work.is_some() && state.ready.is_none() { state.ready = Some(ready); } + } + fn note_error(&self, error: Error) -> bool { + self.error.compare_exchange(0, error.to_errno(), Ordering::AcqRel, Ordering::Acquire).is_ok() + } + fn release_state(&self) { + let old = { + let mut state = self.state.lock(); + (state.work.take(), state.ready.take()) + }; + drop(old); + } + fn owner_done(&self) { + // Every hardware owner retains its Completion/Member through exact + // retirement or stop. Exported fence FDs must not retain failed pins. + if self.owners.fetch_sub(1, Ordering::AcqRel) == 1 { self.release_state(); } + } + fn member_done(&self, fence: &UserFence, result: Result) { + if let Err(error) = result { self.note_error(error); } if self.remaining.fetch_sub(1, Ordering::AcqRel) == 1 { let error = self.error.load(Ordering::Acquire); - if error != 0 { - self.fence.set_error(Error::from_errno(error)); - } else { - // Scheduler cleanup can wait behind submission on the same worker. - // Once every member and enqueue have succeeded, retained packets - // must not prevent new submissions from acquiring work-state nodes. - let retired = self.work_state.lock().take(); - drop(retired); - } - self.fence.signal(); + if error != 0 { fence.set_error(Error::from_errno(error)); } + else { self.release_state(); } + fence.signal(); } } +} +#[derive(Clone)] +pub(crate) struct Aggregate(UserFence); +impl Aggregate { + fn source(&self) -> &Arc { + // Only Outputs::publish constructs Aggregate, after setting the payload. + self.0.source.as_ref().expect("published aggregate has submission state") + } + fn fence(&self) -> Fence { Fence::from_fence(&self.0) } + pub(crate) fn member(&self) -> Member { + self.source().remaining.fetch_add(1, Ordering::Relaxed); + self.source().owners.fetch_add(1, Ordering::Relaxed); + Member { aggregate: self.clone(), complete: AtomicBool::new(false) } + } fn finish_enqueue(&self, result: Result) { - // A dropped, never-enqueued member reports cancellation. The ioctl's actual - // failure takes precedence, while published members still retain their work. - if let Err(error) = result { - self.error.store(error.to_errno(), Ordering::Release); - } - self.member_done(result); + if let Err(error) = result { self.source().error.store(error.to_errno(), Ordering::Release); } + self.source().member_done(&self.0, result); + self.source().owner_done(); } } -/// One command's contribution to its submission, including construction failure. pub(crate) struct Member { - submission: Arc, + aggregate: Aggregate, complete: AtomicBool, } - impl Member { + pub(super) fn needs_source_ready(&self) -> bool { self.aggregate.source().firmware_source } pub(crate) fn work_state(&self) -> Result> { - self.submission - .work_state - .lock() - .as_ref() - .cloned() - .ok_or(ECANCELED) + self.aggregate.source().state.lock().work.clone().ok_or(ECANCELED) } - + pub(super) fn is_source(&self, source: &Arc) -> bool { + self.aggregate.source().firmware_source && Arc::ptr_eq(self.aggregate.source(), source) + } + pub(super) fn set_source_ready(&self, ready: Fence) { self.aggregate.source().set_ready(ready); } + pub(super) fn note_error(&self, error: Error) -> bool { self.aggregate.source().note_error(error) } pub(crate) fn complete(&self, result: Result) { if !self.complete.swap(true, Ordering::AcqRel) { - self.submission.member_done(result); + self.aggregate.source().member_done(&self.aggregate.0, result); } } } +#[kunit_tests(asahi_g17_aggregate_promise)] +mod tests { + use super::*; + use kernel::sync::LockClassKey; + static KEY: Pin<&LockClassKey> = kernel::static_lock_class!(); + + fn contexts() -> FenceContexts { + FenceContexts::new(1, c_str!("asahi_promise_test"), KEY).unwrap() + } + fn aggregate(contexts: &FenceContexts) -> Aggregate { + let mut fence = export_fallback(contexts).unwrap(); + fence.source = Some(Submission::with_work(None, true).unwrap()); + Aggregate(fence.into()) + } + + #[test] + fn exact_export_identity_recognizes_unpublished_source() { + let contexts = contexts(); + let aggregate = aggregate(&contexts); + let plain = independent(&contexts).unwrap(); + // SAFETY: Both references remain owned throughout the checked lookup. + assert!(unsafe { exported_source_raw(plain.raw()) }.is_none()); + let source = unsafe { exported_source_raw(aggregate.0.raw()) }.unwrap(); + assert!(Arc::ptr_eq(&source, aggregate.source())); + assert_eq!(source.source_status().unwrap(), false); + aggregate.finish_enqueue(Ok(())); + assert_eq!(source.source_status().unwrap(), true); + } + + #[test] + fn failed_export_releases_state_after_last_runtime_owner() { + let contexts = contexts(); + let aggregate = aggregate(&contexts); + let source = aggregate.source().clone(); + source.state.lock().ready = Some(Fence::from_fence(&independent(&contexts).unwrap())); + let member = aggregate.member(); + aggregate.finish_enqueue(Ok(())); + member.complete(Err(EIO)); + assert_eq!(super::super::job::fence_status(&aggregate.fence()), EIO.to_errno()); + assert!(source.state.lock().ready.is_some()); + drop(member); + // Keep both the exported fence and source state alive, as userspace can. + assert!(source.state.lock().ready.is_none()); + assert_eq!(source.owners.load(Ordering::Acquire), 0); + } + + #[test] + fn actual_enqueue_error_wins_over_dropped_member_cancellation() { + let aggregate = aggregate(&contexts()); + drop(aggregate.member()); + aggregate.finish_enqueue(Err(ENOMEM)); + assert_eq!(super::super::job::fence_status(&aggregate.fence()), ENOMEM.to_errno()); + } + + #[test] + fn rejected_private_fence_never_exports_source_state() { + let fence = export_fallback(&contexts()).unwrap(); + fence.set_error(ENOMEM); + fence.signal(); + // SAFETY: The unique fence owns its initialized allocation. + assert!(unsafe { exported_source_raw(fence.raw()) }.is_none()); + } + + #[test] + fn maximum_submission_keeps_all_members_and_enqueue_guard() { + let aggregate = aggregate(&contexts()); + let source = aggregate.source().clone(); + let mut members: [Option; crate::file::MAX_COMMANDS_PER_SUBMISSION as usize] = + core::array::from_fn(|_| Some(aggregate.member())); + assert_eq!(source.remaining.load(Ordering::Acquire), 65); + assert_eq!(source.owners.load(Ordering::Acquire), 65); + aggregate.finish_enqueue(Ok(())); + for member in &members { + assert_eq!(super::super::job::fence_status(&aggregate.fence()), 0); + member.as_ref().unwrap().complete(Ok(())); + } + assert!(super::super::job::fence_status(&aggregate.fence()) > 0); + assert_eq!(source.remaining.load(Ordering::Acquire), 0); + for member in &mut members { drop(member.take()); } + assert_eq!(source.owners.load(Ordering::Acquire), 0); + } +} impl Drop for Member { fn drop(&mut self) { self.complete(Err(ECANCELED)); + self.aggregate.source().owner_done(); } } @@ -155,83 +312,54 @@ fn install(outputs: impl Iterator, fence: &Fence) { for mut sync in outputs { if let Some(chain) = sync.chain_fence.take() { sync.syncobj.add_point(chain, fence, sync.timeline_value); - } else { - sync.syncobj.replace_fence(Some(fence)); - } + } else { sync.syncobj.replace_fence(Some(fence)); } } } -/// Keeps resolved output syncobjs reachable on every fallible submission path. -/// Input syncobjs stay in the same vector, avoiding a separate output allocation. pub(crate) struct Outputs { - fallback: UserFence, + fallback: Option>, syncs: KVec, input_count: usize, - submission: Option>, + submission: Option, result: Result, } - impl Outputs { - pub(crate) fn new( - contexts: &FenceContexts, - mut syncs: KVec, - input_count: usize, - allocation_failure: &Fence, - ) -> Result { - if input_count > syncs.len() { - return Err(EINVAL); - } - let fallback = match independent(contexts) { + pub(crate) fn new(contexts: &FenceContexts, mut syncs: KVec, + input_count: usize, allocation_failure: &Fence) -> Result + { + if input_count > syncs.len() { return Err(EINVAL); } + let fallback = match export_fallback(contexts) { Ok(fence) => fence, Err(error) => { - // Syncobjs and timeline-chain storage are already resolved. - // No work has been accepted; publish the pre-signaled ENOMEM - // fence without another allocation before returning the error. install(syncs.drain(input_count..), allocation_failure); return Err(error); } }; - Ok(Self { - fallback, - syncs, - input_count, - submission: None, - result: Err(ECANCELED), - }) - } - - pub(crate) fn inputs(&self) -> &[SyncItem] { - &self.syncs[..self.input_count] + Ok(Self { fallback: Some(fallback), syncs, input_count, + submission: None, result: Err(ECANCELED) }) } - - /// Publish the actual aggregate before enqueueing any command. - pub(crate) fn publish(&mut self, context: Arc) -> Result> { - let submission = Submission::new(self.fallback.clone(), context)?; + pub(crate) fn inputs(&self) -> &[SyncItem] { &self.syncs[..self.input_count] } + pub(crate) fn publish(&mut self, context: Arc, firmware_source: bool) -> Result { + let source = Submission::new(context, firmware_source)?; + let mut fence = self.fallback.take().ok_or(EINVAL)?; + fence.source = Some(source); + let submission = Aggregate(fence.into()); install(self.syncs.drain(self.input_count..), &submission.fence()); self.submission = Some(submission.clone()); Ok(submission) } - - /// The caller first records terminal VM errors and settles prior failed work. - pub(crate) fn finish(mut self, result: Result) -> Result { - self.result = result; - result - } + pub(crate) fn finish(mut self, result: Result) -> Result { self.result = result; result } } - impl Drop for Outputs { fn drop(&mut self) { if let Some(submission) = self.submission.take() { submission.finish_enqueue(self.result); return; } - if let Err(error) = self.result { - self.fallback.set_error(error); + if let Some(fallback) = self.fallback.take() { + if let Err(error) = self.result { fallback.set_error(error); } + install(self.syncs.drain(self.input_count..), &Fence::from_fence(&fallback)); + fallback.signal(); } - install( - self.syncs.drain(self.input_count..), - &Fence::from_fence(&self.fallback), - ); - self.fallback.signal(); } } diff --git a/drivers/gpu/drm/asahi/g17/job.rs b/drivers/gpu/drm/asahi/g17/job.rs index fd2cc878b534e..4ad4f8e372764 100644 --- a/drivers/gpu/drm/asahi/g17/job.rs +++ b/drivers/gpu/drm/asahi/g17/job.rs @@ -150,6 +150,7 @@ impl Packet { super::dependency::replay_status(fence_status(fence))?; } for fence in &self.firmware { check_status(fence)?; } + self.completion.check_source_inputs()?; Ok(()) } @@ -183,6 +184,7 @@ impl Packet { for fence in &self.firmware { check_status(fence)?; } + self.completion.check_source_inputs()?; Ok(()) } @@ -262,6 +264,9 @@ fn check_status(fence: &Fence) -> Result { /// It performs all completion signalling after releasing the device mutex. pub(crate) trait Backend: Send + Sync { /// Device detector retained by render completions after queue teardown. + fn input_sources(&self, _inputs: &[crate::file::SyncItem]) + -> Option> { None } + fn feed(&self) -> Option> { None } @@ -430,6 +435,9 @@ impl Scheduler { // All fallible scheduler setup precedes early hardware publication. The // scheduler still owns watchdog, credits and cancellation after push. if !(early && self.backend.try_early(&packet)) { + if packet.completion.needs_source_ready() { + packet.completion.set_source_ready(ready.clone()); + } self.backend.count_backlog(&packet); } job.push(); diff --git a/drivers/gpu/drm/asahi/g17/mod.rs b/drivers/gpu/drm/asahi/g17/mod.rs index 6b9e9903f3a0d..a85f68829018e 100644 --- a/drivers/gpu/drm/asahi/g17/mod.rs +++ b/drivers/gpu/drm/asahi/g17/mod.rs @@ -25,6 +25,7 @@ mod completion; pub(crate) mod context; mod data_segment; mod dependency; +mod sync_input; mod event; mod feed; mod fence; diff --git a/drivers/gpu/drm/asahi/g17/queue/compute.rs b/drivers/gpu/drm/asahi/g17/queue/compute.rs index d2ec8da9c6082..85c65a9b22d53 100644 --- a/drivers/gpu/drm/asahi/g17/queue/compute.rs +++ b/drivers/gpu/drm/asahi/g17/queue/compute.rs @@ -538,6 +538,8 @@ pub(crate) struct Queue { quarantine_error: Option, failure_status_pending: bool, retirement_proved: bool, + source_readers: Arc, + source_hint: crate::g17::sync_input::ReaderHint, } impl Queue { @@ -598,6 +600,8 @@ impl Queue { quarantine_error: None, failure_status_pending: false, retirement_proved: false, + source_readers: Arc::new(core::sync::atomic::AtomicU32::new(0), GFP_KERNEL)?, + source_hint: crate::g17::sync_input::ReaderHint::new(), }) } pub(crate) fn qid(&self) -> u8 { @@ -1231,11 +1235,15 @@ impl Queue { && self.active.len == 0 && self.replays.len == 0 && self.previous.is_none() + // Borrow acquisition is serialized by the device mutex. Avoid + // touching the separate loan counter for an otherwise busy queue. + && !self.source_hint.has_readers(&self.source_readers) } pub(crate) fn bind_owner(&mut self, owner: u64, context: Arc) -> Result { if !self.reusable() { return Err(EBUSY); } + self.source_hint.reset_for_owner(&self.source_readers)?; if !context.is_current() { return Err(EFAULT); } @@ -1343,7 +1351,9 @@ impl Queue { } /// An idle retained queue whose USC backing may be given back. pub(crate) fn vacate_candidate(&self) -> bool { - (self.reusable() || self.recycled_pending()) && self.pool_phase == PoolPhase::Populated && self.pool.populated() + self.pool_phase == PoolPhase::Populated && self.pool.populated() + && (self.reusable() || self.recycled_pending()) + && !self.source_hint.has_readers(&self.source_readers) } /// A recycled killed queue still holding the backing its killed work used. pub(crate) fn recycled_pending(&self) -> bool { @@ -1780,6 +1790,17 @@ impl Queue { dependencies: &[crate::g17::fw::kick::KickDependency], host: &mut impl Host, defer: &mut impl FnMut(Deferred) -> Result, + ) -> Result { + self.publish_with_native_waits(packet, dependencies, &[], host, defer) + } + + pub(in crate::g17) fn publish_with_native_waits( + &mut self, + packet: Arc, + dependencies: &[crate::g17::fw::kick::KickDependency], + native: &[crate::g17::sync_input::NativeWait], + host: &mut impl Host, + defer: &mut impl FnMut(Deferred) -> Result, ) -> Result { use crate::g17::fw::{ channels::WorkSlot, @@ -1814,11 +1835,22 @@ impl Queue { let (descriptor_high, descriptor_low) = self.graph.descriptor_vas(kick_timestamp.slot() as u8)?; let first = !self.installed && self.ordinal == 0; + // The first prototype only waits on an installed consumer channel. + // A retry without a published source falls back to completion on CPU. + if first && !native.is_empty() { return Err(EAGAIN); } + if native.len() > crate::g17::kick::COMPUTE_CHANNELS { return Err(EINVAL); } let entry_signal = first || dependencies .iter() .any(|dependency| dependency.qid() != qid); item_capacity(self.graph.cursors()?, entry_signal)?; + if !native.is_empty() { + let cursors = self.graph.cursors()?; + let free = (cursors.consumer + cursors.count - cursors.producer - 1) % cursors.count; + if free < (if entry_signal { 4 } else { 3 }) + native.len() as u32 { + return Err(EAGAIN); + } + } let record_slot = (!first).then_some(self.ordinal as usize % RECORD_SLOTS); if let Some(slot) = record_slot { self.graph.ensure_record_slot(slot)?; @@ -1928,6 +1960,13 @@ impl Queue { } let config_va = self.graph.record(record_slot, CONFIG, 0xc0, config)?; let cursors = self.graph.cursors()?; + let mut wait_vas = KVec::with_capacity(native.len(), GFP_KERNEL)?; + for (index, wait) in native.iter().enumerate() { + let record = wait.bytes(crate::g17::fw::stamp_value(kick_timestamp.get() as u32), + packet.order.sequence); + let address = self.graph.record(record_slot, 0x900 + index * 0x60, 0x60, record)?; + wait_vas.push(address, GFP_KERNEL)?; + } let prepared_kick = self.kick.prepare_entry(&kick_args)?; let mut producer; if first { @@ -1962,15 +2001,22 @@ impl Queue { })?; } else { let target = self.pool.reserve_submission()?; - producer = self - .graph - .append(cursors.producer, descriptor_high, || { - packet.mark_published() + producer = (|| -> Result { + let mut before_descriptor = cursors.producer; + for &wait in &wait_vas { + before_descriptor = self.graph.append(before_descriptor, wait, || { + packet.mark_published(); + visible = true; + })?; + } + self.graph.append(before_descriptor, descriptor_high, || { + packet.mark_published(); + visible = true; }) - .or_else(|error| { - self.pool.cancel_unpublished(target)?; - Err(error) - })?; + })().or_else(|error| { + if !visible { self.pool.cancel_unpublished(target)?; } + Err(error) + })?; visible = true; self.unpublished_binding = false; let qos = host.qos_publish( @@ -2282,6 +2328,7 @@ impl Queue { impl Queue { pub(crate) fn can_rearm(&self) -> bool { self.pool_phase == PoolPhase::Vacant && (self.reusable() || self.recycled_pending()) + && !self.source_hint.has_readers(&self.source_readers) } } @@ -2323,3 +2370,50 @@ impl Queue { Ok((false, retired)) } } + + +impl Queue { + /// Same immutable singleton identity and current physical epoch. + pub(crate) fn has_input_source(&self, source: &Arc, + status: &Arc, epoch: u64) -> bool + { + !self.quarantined && self.replays.len == 0 && self.active.iter().any(|active| + active.ticket.is_some_and(|ticket| ticket.epoch == epoch) + && Arc::ptr_eq(active.packet.completion.status(), status) + && active.packet.completion.is_input_source(source)) + } + + pub(crate) fn source_dependency(&mut self, source: &crate::g17::sync_input::Source, + status: &Arc, epoch: u64) -> Result> + { + for active in self.active.iter() { + if !active.packet.completion.is_input_source(&source.submission) { continue; } + if !Arc::ptr_eq(active.packet.completion.status(), status) { return Err(EINVAL); } + if self.quarantined || self.replays.len != 0 { return Err(EAGAIN); } + let ticket = active.ticket.ok_or(EAGAIN)?; + if ticket.epoch != epoch { return Err(EAGAIN); } + self.valid_ticket(active, &ticket)?; + if active.packet.cancel_requested() { return Err(ECANCELED); } + source.submission.source_status()?; + let dependency = crate::g17::fw::kick::KickDependency::new(self.qid(), + crate::g17::fw::kick::KickTimestamp::new(ticket.kick).ok_or(EIO)?).ok_or(EIO)?; + let hold = self.source_hint.hold(&self.source_readers)?; + return Ok(Some((active.published, dependency, + self.graph.support.gpu_va() + STAMP as u64, + u32::from(self.qid()), hold))); + } + Ok(None) + } + + /// Preparing a possible wait does not create a firmware consumer. Mark it + /// only once the consumer's first item is visible, under the device mutex. + pub(crate) fn note_input_consumer(&self, source: &crate::g17::sync_input::Source) { + for active in self.active.iter() { + if active.packet.completion.is_input_source(&source.submission) { + active.packet.completion.note_firmware_consumer(); + break; + } + } + } +} diff --git a/drivers/gpu/drm/asahi/g17/queue/mod.rs b/drivers/gpu/drm/asahi/g17/queue/mod.rs index 96e566d3971ff..777c4bc1440ac 100644 --- a/drivers/gpu/drm/asahi/g17/queue/mod.rs +++ b/drivers/gpu/drm/asahi/g17/queue/mod.rs @@ -10,7 +10,7 @@ use super::{ command::{Parser, UscWindow, Validated}, completion::{Completion, Destinations, PreparedPublication}, context::Context, - fence::{Outputs, Submission}, + fence::Outputs, job::{self, Backend, Engine, Fences, Order, Packet, Scheduler}, validation::Proof, }; @@ -179,7 +179,7 @@ impl Queue { command: Validated, order: Order, guard: mmu::VmJobGuard, - aggregate: &Arc, + aggregate: &super::fence::Aggregate, inputs: &[file::SyncItem], destinations: Destinations, ) -> Result { @@ -198,6 +198,22 @@ impl Queue { let Validated::Compute { scratch, .. } = &command else { return Err(EINVAL); }; self.ensure_compute(scratch.pages())?; } + // Only convert inputs when no independent host-prefix wait could keep + // the retained source bookkeeping queued behind an external signal. + let sources = if engine == Engine::Compute && !inputs.is_empty() + && !destinations.has_outputs() + && self.frontiers[0].iter().all(|(sequence, fences)| + !Order::contains(order.wait_through[0], *sequence) + || job::fence_status(&fences.completed) > 0) + && self.frontiers[1].iter().all(|(sequence, fences)| + !Order::contains(order.wait_through[1], *sequence) + || fences.ready.raw() != fences.completed.raw() + || job::fence_status(&fences.completed) > 0) + && self.failures.iter().enumerate().all(|(index, failure)| + failure.as_ref().is_none_or(|(sequence, _)| + !Order::contains(order.wait_through[index], *sequence))) + { self.backend.input_sources(inputs) } else { None }; + // Only earlier render ordinals covered by the explicit logical prefix // can delay host publication. These links never become GPU dependencies. let host_predecessors = self.frontiers[index] @@ -217,6 +233,7 @@ impl Queue { engine == Engine::Compute, host_predecessors.clone().next().is_some(), self.backend.feed(), + sources.clone(), )?; let publication = completion.publication() .map(|target| PreparedPublication::new(target, host_predecessors)) @@ -294,10 +311,19 @@ impl Queue { if job::fence_status(fence) > 0 { continue; } + if sources.as_ref().is_some_and(|sources| sources.contains_input(fence)) { + continue; + } dependencies.push(fence.clone(), GFP_KERNEL)?; checked_inputs.push(fence.clone(), GFP_KERNEL)?; } } + if let Some(sources) = &sources { + for ready in sources.publication_waits() { + dependencies.push(ready.clone(), GFP_KERNEL)?; + checked_inputs.push(ready.clone(), GFP_KERNEL)?; + } + } let early = dependencies.is_empty() && self.frontiers[index].len() < EARLY_DEPTH; let packet = Packet::new( @@ -416,7 +442,10 @@ impl crate::queue::Queue for Queue { for (command, _, _) in pending.iter_mut() { command.allow_long_job(); } } let mut guards = guards.into_iter(); - let aggregate = outputs.publish(self.context.clone())?; + let firmware_source = pending.len() == 1 + && matches!(&pending[0].0, Validated::Compute { .. }) + && !pending[0].2.has_outputs(); + let aggregate = outputs.publish(self.context.clone(), firmware_source)?; for (command, order, destinations) in pending { let guard = guards.next().ok_or(EIO)?; self.enqueue( diff --git a/drivers/gpu/drm/asahi/g17/runtime.rs b/drivers/gpu/drm/asahi/g17/runtime.rs index bd95148e9eaf0..d0f3f3cfb2eb2 100644 --- a/drivers/gpu/drm/asahi/g17/runtime.rs +++ b/drivers/gpu/drm/asahi/g17/runtime.rs @@ -959,6 +959,7 @@ impl Backend { /// The physical namespace bounds the batch independently of open logical queues. /// Collection failure leaves the engine's Active owner intact for a stopped retry. pub(super) struct DeferredBatch { + has_source_completions: bool, values: KVVec, /// Exited owners detached from idle retained compute queues and the backing /// of vacated pools: unmapping and freeing happen in `finish`, off-lock. @@ -994,6 +995,7 @@ impl DeferredBatch { 0 }; Ok(Self { + has_source_completions: false, values: KVVec::with_capacity(capacity, GFP_KERNEL)?, bindings: KVVec::with_capacity(detached, GFP_KERNEL)?, vacated: KVVec::with_capacity(detached, GFP_KERNEL)?, @@ -1051,6 +1053,12 @@ impl DeferredBatch { completion.status().record(*error); } } + match &value { + Deferred::Retired(completion, Err(error)) | Deferred::Drained(completion, Err(error)) + | Deferred::FailedOwned(completion, error) | Deferred::Unpublished(completion, error) => + { completion.note_input_error(*error); }, + _ => {}, + } self.values.push_within_capacity(value).map_err(|_| EIO)?; // A later publisher can borrow this pair before finish signals its old // fence. Publish the exact completion's witness only after collection @@ -1062,11 +1070,29 @@ impl DeferredBatch { ) = self.values.last() { completion.note_retirement_queued(); + self.has_source_completions |= completion.sources().is_some(); } Ok(()) } /// Called only after releasing the device mutex. pub(super) fn finish(&mut self) { + use super::completion::Deferred; + if self.has_source_completions { + for _ in 0..self.values.len() { + let mut changed = false; + for value in &self.values { + if let Deferred::Retired(completion, Ok(_)) + | Deferred::Drained(completion, Ok(_)) = value + { + if let Err(error) = completion.check_source_inputs() { + changed |= completion.note_input_error(error); + } + } + } + if !changed { break; } + } + } + self.has_source_completions = false; for value in self.values.drain_all() { value.finish(); } @@ -1140,11 +1166,75 @@ impl compute::Host for ComputeHost<'_> { } impl super::Firmware { - fn publish_compute( + #[inline] + fn publish_compute(&mut self, owner: u64, packet: Arc, + dependencies: &[super::fw::kick::KickDependency], deferred: &mut DeferredBatch) -> Result + { + if packet.completion.sources().is_some() { + self.publish_compute_inputs(owner, packet, dependencies, deferred) + } else { + self.publish_compute_raw(owner, packet, dependencies, &[], deferred) + } + } + + #[inline(never)] + fn publish_compute_inputs(&mut self, owner: u64, packet: Arc, + dependencies: &[super::fw::kick::KickDependency], deferred: &mut DeferredBatch) -> Result + { + if self.recovery.pending() || super::recovery::Memory::recovery_state(&self.init)? != 0 { + return Err(EAGAIN); + } + let sources = packet.completion.sources().ok_or(EINVAL)?.clone(); + sources.check()?; + let mut merged = super::dependency::Frontier::new(); + for dependency in dependencies { merged.insert(0, *dependency)?; } + let mut holds = KVec::with_capacity(sources.sources.len(), GFP_KERNEL)?; + let mut native: KVec = + KVec::with_capacity(sources.sources.len(), GFP_KERNEL)?; + for source in &sources.sources { + if source.submission.source_status()? { continue; } + let found = self.queues.compute.iter_mut().flatten() + .filter_map(|entry| entry.queue.as_deref_mut()) + .find_map(|queue| match queue.source_dependency(source, packet.completion.status(), + self.recovery.generation()) { + Ok(None) => None, + result => Some(result), + }).ok_or(EAGAIN)??; + let Some((sequence, dependency, stamp_va, event_slot, hold)) = found else { + return Err(EAGAIN); + }; + merged.insert(sequence, dependency)?; + let wait = super::sync_input::NativeWait { sequence, dependency, stamp_va, event_slot }; + if let Some(old) = native.iter_mut().find(|old| + old.dependency.qid() == dependency.qid()) { + if sequence > old.sequence { *old = wait; } + } else { + native.push(wait, GFP_KERNEL)?; + } + holds.push(hold, GFP_KERNEL)?; + } + let result = self.publish_compute_raw(owner, packet.clone(), merged.as_slice(), + native.as_slice(), deferred); + if packet.is_published() { + for queue in self.queues.compute.iter().flatten() + .filter_map(|entry| entry.queue.as_deref()) + .filter(|queue| native.iter().any(|wait| wait.dependency.qid() == queue.qid())) + { + for source in &sources.sources { queue.note_input_consumer(source); } + } + sources.set_holds(holds); + super::sync_input::note_published(); + super::sync_input::note_native(native.len()); + } + result + } + + fn publish_compute_raw( &mut self, owner: u64, packet: Arc, dependencies: &[super::fw::kick::KickDependency], + native: &[super::sync_input::NativeWait], deferred: &mut DeferredBatch, ) -> Result { if self.recovery.pending() { @@ -1183,9 +1273,12 @@ impl super::Firmware { accounting: &mut queues.accounting, generation: self.recovery.generation(), }; - queue.publish(packet, dependencies, &mut host, &mut |event| { - deferred.push(event) - }) + if native.is_empty() { + queue.publish(packet, dependencies, &mut host, &mut |event| deferred.push(event)) + } else { + queue.publish_with_native_waits(packet, dependencies, native, + &mut host, &mut |event| deferred.push(event)) + } } } @@ -1442,6 +1535,27 @@ impl Backend { } } + /// A scheduled dependency may refer to a previous recovery epoch. Do not + /// acquire consumer capacity until every live source is published now. + fn sources_published(&self, packet: &super::job::Packet) -> Result { + let Some(sources) = packet.completion.sources() else { return Ok(true); }; + sources.check()?; + let state = self.shared.state.lock(); + let firmware = state.as_deref().ok_or(ENODEV)?; + if firmware.recovery.pending() + || super::recovery::Memory::recovery_state(&firmware.init)? != 0 + { return Ok(false); } + for source in &sources.sources { + if source.submission.source_status()? { continue; } + if !firmware.queues.compute.iter().flatten() + .filter_map(|entry| entry.queue.as_deref()) + .any(|queue| queue.has_input_source(&source.submission, + packet.completion.status(), firmware.recovery.generation())) + { return Ok(false); } + } + Ok(true) + } + /// Called after taking this packet's publication claim. All waits and work /// state allocation precede the device mutex; only the checked epoch commits. fn compute_packet( @@ -1468,6 +1582,11 @@ impl Backend { let admission = Admission::new(deadline, &check); loop { admission.check()?; + if !self.sources_published(packet)? { + self.shared.queue_events(); + kernel::time::delay::fsleep(kernel::time::Delta::from_millis(1)); + continue; + } // All DRM dependencies were satisfied before this scheduler callback. // Recheck on every retry: pressure may yield an idle binding between // admission and publication without changing this logical queue. diff --git a/drivers/gpu/drm/asahi/g17/runtime/backend.rs b/drivers/gpu/drm/asahi/g17/runtime/backend.rs index c75b525413ee8..b393afd9c8df2 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/backend.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/backend.rs @@ -112,6 +112,55 @@ impl Backend { } impl job::Backend for Backend { + fn input_sources(&self, inputs: &[crate::file::SyncItem]) + -> Option> + { + use crate::g17::{fence, sync_input}; + let mut heads = KVec::new(); + let mut sources: KVec = KVec::new(); + let mut publication_waits = KVec::new(); + for input in inputs.iter().filter_map(|input| input.fence.as_ref()) { + let status = job::fence_status(input); + if status > 0 { continue; } + if status < 0 { return None; } + let leaves = sync_input::collect(input)?; + for leaf in &leaves { + if leaf.status > 0 { continue; } + if leaf.status < 0 { return None; } + // SAFETY: Collection owns each leaf until this lookup finishes. + let submission = unsafe { fence::exported_source_raw(leaf.fence) }?; + if submission.source_status().ok()? { continue; } + if !submission.same_vm(self.context.status()) { return None; } + if sources.iter().any(|known| Arc::ptr_eq(&known.submission, &submission)) { + continue; + } + if sources.len() == crate::g17::fw::kick::KICK_BARRIERS_MAX { return None; } + let published = { + let state = self.shared.state.lock(); + let firmware = state.as_deref()?; + if self.shared.crashed.load(Ordering::Acquire) || firmware.recovery.pending() + || crate::g17::recovery::Memory::recovery_state(&firmware.init).ok()? != 0 + { return None; } + firmware.queues.compute.iter().flatten() + .filter_map(|entry| entry.queue.as_deref()) + .any(|queue| queue.has_input_source(&submission, self.context.status(), + firmware.recovery.generation())) + }; + if !published { + if submission.source_status().ok()? { continue; } + // The existing scheduler fence becomes ready after actual + // publication, not GPU completion. No binding loan or + // consumer hardware capacity is acquired while waiting. + publication_waits.push(submission.ready()?, GFP_KERNEL).ok()?; + } + sources.push(sync_input::Source { submission }, GFP_KERNEL).ok()?; + } + heads.push(input.clone(), GFP_KERNEL).ok()?; + } + if sources.is_empty() { return None; } + sync_input::Sources::new(heads, sources, publication_waits).ok() + } + fn feed(&self) -> Option> { Some(self.shared.feed.clone()) } diff --git a/drivers/gpu/drm/asahi/g17/sync_input.c b/drivers/gpu/drm/asahi/g17/sync_input.c new file mode 100644 index 0000000000000..e6b91988c3c28 --- /dev/null +++ b/drivers/gpu/drm/asahi/g17/sync_input.c @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +#include +#include +#include +#include "sync_input.h" + +/* Called without the device mutex: status polling may run fence callbacks. */ +int asahi_g17_collect_fences(struct dma_fence *head, + struct asahi_g17_fence_leaf *out, size_t capacity) +{ + struct dma_fence_unwrap cursor; + struct dma_fence *leaf; + size_t count = 0; + int error = 0; + + dma_fence_unwrap_for_each(leaf, &cursor, head) { + if (dma_fence_is_container(leaf)) { + error = -EOPNOTSUPP; + break; + } + if (count == capacity) { + error = -ENOSPC; + break; + } + out[count].fence = dma_fence_get(leaf); + out[count].status = dma_fence_get_status(leaf); + ++count; + } + /* A bounded early exit still owns the iterator's current chain. */ + dma_fence_put(cursor.chain); + if (error) { + while (count) + dma_fence_put(out[--count].fence); + return error; + } + return count; +} + +void asahi_g17_put_fence(struct dma_fence *fence) +{ + dma_fence_put(fence); +} diff --git a/drivers/gpu/drm/asahi/g17/sync_input.h b/drivers/gpu/drm/asahi/g17/sync_input.h new file mode 100644 index 0000000000000..2b6e80f4b7f66 --- /dev/null +++ b/drivers/gpu/drm/asahi/g17/sync_input.h @@ -0,0 +1,18 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ +#ifndef ASAHI_G17_SYNC_INPUT_H +#define ASAHI_G17_SYNC_INPUT_H + +#include + +struct dma_fence; + +struct asahi_g17_fence_leaf { + struct dma_fence *fence; + int status; +}; + +int asahi_g17_collect_fences(struct dma_fence *head, + struct asahi_g17_fence_leaf *out, size_t capacity); +void asahi_g17_put_fence(struct dma_fence *fence); + +#endif diff --git a/drivers/gpu/drm/asahi/g17/sync_input.rs b/drivers/gpu/drm/asahi/g17/sync_input.rs new file mode 100644 index 0000000000000..1cd9a9ba761aa --- /dev/null +++ b/drivers/gpu/drm/asahi/g17/sync_input.rs @@ -0,0 +1,245 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT + +//! Bounded input-fence components. Collection happens before the device mutex, +//! because polling a container's status can signal it and run callbacks. + +use kernel::{bindings, dma_fence::{Fence, RawDmaFence}, prelude::*}; + +#[repr(C)] +pub(super) struct Leaf { + pub(super) fence: *mut bindings::dma_fence, + pub(super) status: i32, +} + +unsafe extern "C" { + fn asahi_g17_collect_fences(head: *mut bindings::dma_fence, + out: *mut Leaf, capacity: usize) -> i32; + fn asahi_g17_put_fence(fence: *mut bindings::dma_fence); +} + +impl Drop for Leaf { + fn drop(&mut self) { + // SAFETY: Successful collection transfers exactly one owned reference. + unsafe { asahi_g17_put_fence(self.fence) }; + } +} + +/// Allocation, unsupported containers, or a large prefix retain normal waits. +pub(super) fn collect(input: &Fence) -> Option> { + let mut capacity = 4; + loop { + let mut leaves = KVec::with_capacity(capacity, GFP_KERNEL).ok()?; + // SAFETY: The input owns its fence. The C helper writes at most capacity + // fully initialized records and releases every reference on failure. + let count = unsafe { + asahi_g17_collect_fences(input.raw(), leaves.as_mut_ptr(), capacity) + }; + if count == ENOSPC.to_errno() && capacity < 128 { + capacity = (capacity * 4).min(128); + continue; + } + if count < 0 { return None; } + // SAFETY: The helper returned this many initialized, owned records. + unsafe { leaves.set_len(count as usize) }; + return Some(leaves); + } +} + + +use super::fence::Submission; +use core::sync::atomic::{AtomicU32, Ordering}; +use kernel::{new_mutex, sync::{Arc, Mutex}}; + +pub(super) struct Source { + pub(super) submission: Arc, +} + +/// The counter is independent of a physical queue's allocation and survives +/// processor stop until the last consumer releases its binding loan. +pub(super) struct Hold(Arc); +impl Hold { + fn new(readers: &Arc) -> Result { + readers.fetch_update(Ordering::AcqRel, Ordering::Acquire, + |count| count.checked_add(1)).map_err(|_| EOVERFLOW)?; + Ok(Self(readers.clone())) + } +} +impl Drop for Hold { + fn drop(&mut self) { self.0.fetch_sub(1, Ordering::AcqRel); } +} + +/// A new owner starts with no loans. Acquiring a loan requires exclusive access +/// and arms the hint; eligibility queries stay read-only. A validated owner +/// transition can clear the hint after all independent loans have been released. +#[repr(transparent)] +pub(super) struct ReaderHint { + possible: bool, +} +impl ReaderHint { + pub(super) const fn new() -> Self { + Self { possible: false } + } + pub(super) fn hold(&mut self, count: &Arc) -> Result { + let hold = Hold::new(count)?; + self.possible = true; + Ok(hold) + } + pub(super) fn has_readers(&self, count: &AtomicU32) -> bool { + self.possible && count.load(Ordering::Acquire) != 0 + } + pub(super) fn reset_for_owner(&mut self, count: &AtomicU32) -> Result { + if self.has_readers(count) { return Err(EBUSY); } + self.possible = false; + Ok(()) + } +} + +#[pin_data] +pub(super) struct Sources { + inputs: KVec, + publication_waits: KVec, + pub(super) sources: KVec, + #[pin] + holds: Mutex>, +} +impl Sources { + pub(super) fn new(inputs: KVec, sources: KVec, + publication_waits: KVec) -> Result> { + CONVERTED.fetch_add(1, Ordering::Relaxed); + if !publication_waits.is_empty() { PENDING.fetch_add(1, Ordering::Relaxed); } + Arc::pin_init(pin_init!(Self { + inputs, sources, publication_waits, + holds <- new_mutex!(KVec::new(), "G17 source binding loans"), + }), GFP_KERNEL) + } + pub(super) fn contains_input(&self, input: &Fence) -> bool { + self.inputs.iter().any(|fence| fence.raw() == input.raw()) + } + pub(super) fn publication_waits(&self) -> &[Fence] { &self.publication_waits } + pub(super) fn check(&self) -> Result { + for source in &self.sources { source.submission.source_status()?; } + Ok(()) + } + pub(super) fn set_holds(&self, holds: KVec) { + let old = core::mem::replace(&mut *self.holds.lock(), holds); + drop(old); + } + pub(super) fn release(&self) { self.set_holds(KVec::new()); } +} + + +static CONVERTED: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0); +static PUBLISHED: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0); +static PENDING: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0); +pub(super) fn pending_count() -> u64 { PENDING.load(Ordering::Relaxed) } +pub(super) fn note_published() { PUBLISHED.fetch_add(1, Ordering::Relaxed); } +pub(super) fn counters() -> (u64, u64) { + (CONVERTED.load(Ordering::Relaxed), PUBLISHED.load(Ordering::Relaxed)) +} + +/// A revalidated compute completion dependency for a direct work descriptor. +#[derive(Clone, Copy)] +pub(super) struct NativeWait { + pub(super) sequence: u64, + pub(super) dependency: super::fw::kick::KickDependency, + pub(super) stamp_va: u64, + pub(super) event_slot: u32, +} +impl NativeWait { + pub(super) fn bytes(&self, consumer_stamp: u32, uuid: u64) -> [u8; 0x48] { + let mut out = [0; 0x48]; + let timestamp = self.dependency.word() & ((1u64 << 40) - 1); + for (offset, value) in [ + (0, 4u32), + (0x14, u32::from(self.dependency.qid())), + (0x18, super::fw::stamp_value(timestamp as u32)), + (0x24, self.event_slot), + (0x28, consumer_stamp), + (0x34, 1), + ] { + out[offset..offset + 4].copy_from_slice(&value.to_le_bytes()); + } + for (offset, value) in [(4, self.stamp_va), (0xc, timestamp), (0x2c, uuid)] { + out[offset..offset + 8].copy_from_slice(&value.to_le_bytes()); + } + out + } +} + +static NATIVE: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0); +pub(super) fn note_native(count: usize) { NATIVE.fetch_add(count as u64, Ordering::Relaxed); } +pub(super) fn native_count() -> u64 { NATIVE.load(Ordering::Relaxed) } + +#[kunit_tests(asahi_g17_input_wait)] +mod tests { + use super::*; + use crate::g17::fw::kick::{KickDependency, KickTimestamp}; + + #[test] + fn completion_record_preserves_distinct_event_and_queue_domains() { + let wait = NativeWait { + sequence: 1, + dependency: KickDependency::new(37, + KickTimestamp::new(0x12_3456_789a).unwrap()).unwrap(), + stamp_va: 0x1122_3344_5566_7788, + event_slot: 7, + }; + // Protocol fixture includes the unaligned stamp pointer, full 40-bit + // timestamp, wrapped 24-bit stamp and independent legacy event slot. + let expected: [u8; 0x48] = [ + 4, 0, 0, 0, 0x88, 0x77, 0x66, 0x55, + 0x44, 0x33, 0x22, 0x11, 0x9a, 0x78, 0x56, 0x34, + 0x12, 0, 0, 0, 37, 0, 0, 0, + 0, 0x9a, 0x78, 0x56, 0, 0, 0, 0, + 0, 0, 0, 0, 7, 0, 0, 0, + 0, 0xcc, 0xbb, 0xaa, 8, 7, 6, 5, + 4, 3, 2, 1, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + ]; + assert_eq!(wait.bytes(0xaabb_cc00, 0x0102_0304_0506_0708), expected); + } + + #[test] + fn failed_loan_acquisition_never_wraps_or_releases_another_reader() { + let readers = Arc::new(AtomicU32::new(u32::MAX - 1), GFP_KERNEL).unwrap(); + let last = Hold::new(&readers).unwrap(); + assert!(matches!(Hold::new(&readers), Err(error) if error == EOVERFLOW)); + assert_eq!(readers.load(Ordering::Acquire), u32::MAX); + drop(last); + assert_eq!(readers.load(Ordering::Acquire), u32::MAX - 1); + } + + #[test] + fn loan_outlives_original_queue_counter_reference() { + let readers = Arc::new(AtomicU32::new(0), GFP_KERNEL).unwrap(); + let mut hint = ReaderHint::new(); + let first = hint.hold(&readers).unwrap(); + let second = hint.hold(&readers).unwrap(); + drop(readers); + assert_eq!(second.0.load(Ordering::Acquire), 2); + drop(first); + assert_eq!(second.0.load(Ordering::Acquire), 1); + drop(second); + } + + #[test] + fn reader_guard_tracks_partial_release_and_rearms_after_zero() { + let readers = Arc::new(AtomicU32::new(0), GFP_KERNEL).unwrap(); + let mut hint = ReaderHint::new(); + assert!(!hint.has_readers(&readers)); + let first = hint.hold(&readers).unwrap(); + let second = hint.hold(&readers).unwrap(); + assert!(hint.has_readers(&readers)); + assert!(matches!(hint.reset_for_owner(&readers), Err(error) if error == EBUSY)); + drop(first); + assert!(hint.has_readers(&readers)); + drop(second); + assert!(!hint.has_readers(&readers)); + hint.reset_for_owner(&readers).unwrap(); + let next = hint.hold(&readers).unwrap(); + assert!(hint.has_readers(&readers)); + drop(next); + assert!(!hint.has_readers(&readers)); + } +} From 89c8873651b729fdd4e981e87b9d0df0d0ad0017 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Fri, 23 Oct 2026 21:29:21 -0400 Subject: [PATCH 015/388] drm/asahi: treat a firmware source as complete once its retirement is queued Compute retirement removes the producer from its queue's active list under the device mutex, but signals the producer's fence only after the lock is dropped. A consumer published in between found neither an active producer nor a completed one, fell back to the scheduler, and every later command in the chain then waited for its producer's publication through the scheduler. Mark the singleton firmware source retired as its successful retirement is queued (errors are already recorded there), after rechecking its own inputs. sync.signal queue-to-queue (timeline, 2 queues): scheduler fallbacks for chain consumers 57% -> 0.1%. (cherry picked from commit a3be7f63500c76d117000ee1474713e27889aabe) --- drivers/gpu/drm/asahi/g17/completion.rs | 7 +++++++ drivers/gpu/drm/asahi/g17/fence.rs | 12 +++++++++++- drivers/gpu/drm/asahi/g17/runtime.rs | 2 ++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/asahi/g17/completion.rs b/drivers/gpu/drm/asahi/g17/completion.rs index 406efc0e221fe..5936bcb1b933a 100644 --- a/drivers/gpu/drm/asahi/g17/completion.rs +++ b/drivers/gpu/drm/asahi/g17/completion.rs @@ -377,6 +377,13 @@ impl Completion { pub(super) fn note_input_error(&self, error: Error) -> bool { self.output.output().member.note_error(error) } + /// Called under the device mutex as a successful retirement is queued. + pub(super) fn note_source_retired(&self) { + match self.check_source_inputs() { + Ok(()) => self.output.output().member.note_retired(), + Err(error) => { self.note_input_error(error); } + } + } pub(super) fn check_source_inputs(&self) -> Result { if let Some(sources) = &self.sources { sources.check()?; } Ok(()) diff --git a/drivers/gpu/drm/asahi/g17/fence.rs b/drivers/gpu/drm/asahi/g17/fence.rs index 25e5f6b04d558..da883cede52df 100644 --- a/drivers/gpu/drm/asahi/g17/fence.rs +++ b/drivers/gpu/drm/asahi/g17/fence.rs @@ -121,6 +121,10 @@ pub(crate) struct Submission { /// Live Member objects, plus the enqueue guard, independently of signalling. owners: AtomicU8, firmware_source: bool, + /// Set under the device mutex when the only member's successful retirement + /// is queued, before its fence is signalled off-lock: a consumer published + /// in between needs no wait on it. + retired: AtomicBool, } impl Submission { @@ -135,10 +139,12 @@ impl Submission { error: AtomicI32::new(0), owners: AtomicU8::new(1), firmware_source, + retired: AtomicBool::new(false), }), GFP_KERNEL) } pub(super) fn source_status(&self) -> Result { - let complete = self.remaining.load(Ordering::Acquire) == 0; + let complete = self.remaining.load(Ordering::Acquire) == 0 + || self.retired.load(Ordering::Acquire); let error = self.error.load(Ordering::Acquire); if error != 0 { return Err(super::dependency::inherited_error(Error::from_errno(error))); } Ok(complete) @@ -212,6 +218,10 @@ impl Member { } pub(super) fn set_source_ready(&self, ready: Fence) { self.aggregate.source().set_ready(ready); } pub(super) fn note_error(&self, error: Error) -> bool { self.aggregate.source().note_error(error) } + pub(super) fn note_retired(&self) { + let source = self.aggregate.source(); + if source.firmware_source { source.retired.store(true, Ordering::Release); } + } pub(crate) fn complete(&self, result: Result) { if !self.complete.swap(true, Ordering::AcqRel) { self.aggregate.source().member_done(&self.aggregate.0, result); diff --git a/drivers/gpu/drm/asahi/g17/runtime.rs b/drivers/gpu/drm/asahi/g17/runtime.rs index d0f3f3cfb2eb2..736fed78b0cc1 100644 --- a/drivers/gpu/drm/asahi/g17/runtime.rs +++ b/drivers/gpu/drm/asahi/g17/runtime.rs @@ -1057,6 +1057,8 @@ impl DeferredBatch { Deferred::Retired(completion, Err(error)) | Deferred::Drained(completion, Err(error)) | Deferred::FailedOwned(completion, error) | Deferred::Unpublished(completion, error) => { completion.note_input_error(*error); }, + Deferred::Retired(completion, Ok(_)) | Deferred::Drained(completion, Ok(_)) => + completion.note_source_retired(), _ => {}, } self.values.push_within_capacity(value).map_err(|_| EIO)?; From 3ff4f4baa05a88d5f99d2fee76089cf72b5617a4 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Fri, 23 Oct 2026 21:29:21 -0400 Subject: [PATCH 016/388] drm/asahi: publish G17 commands from the ioctl at any queue depth Early publication was limited to queues with fewer than two outstanding commands, so every deeper queue paid a scheduler worker hop per command and cross-queue chains advanced one scheduler round trip per link. The backend already falls back to the scheduler behind any backlog and on every capacity miss, which preserves submission order, so the depth limit only added latency. sync.signal queue-to-queue (timeline, 2 queues) 94.1 -> 56.2 us/hop together with the preceding change; compute and render depth rows unchanged. (cherry picked from commit 8508510641edcedb5ae1417f560cdff4da646bda) --- drivers/gpu/drm/asahi/g17/queue/mod.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/queue/mod.rs b/drivers/gpu/drm/asahi/g17/queue/mod.rs index 777c4bc1440ac..c959f8bc8c4b8 100644 --- a/drivers/gpu/drm/asahi/g17/queue/mod.rs +++ b/drivers/gpu/drm/asahi/g17/queue/mod.rs @@ -27,7 +27,6 @@ use kernel::{ xarray, }; -const EARLY_DEPTH: usize = 2; static FENCE_KEY: Pin<&LockClassKey> = kernel::static_lock_class!(); pub(crate) mod admission; @@ -324,8 +323,10 @@ impl Queue { checked_inputs.push(ready.clone(), GFP_KERNEL)?; } } - let early = dependencies.is_empty() - && self.frontiers[index].len() < EARLY_DEPTH; + // Publish from the ioctl whenever nothing has to wait, at any depth. + // The backend falls back to the scheduler behind any backlog or on any + // capacity miss, so submission order is preserved. + let early = dependencies.is_empty(); let packet = Packet::new( id, self.context.clone(), From a0dfc3c0b5a4fced66119fded07ed141990a9025 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Fri, 23 Oct 2026 21:49:26 -0400 Subject: [PATCH 017/388] drm/asahi: map G17 compute descriptors cached for the firmware Render descriptors already come from the retained object pool with a firmware-cached mapping; compute descriptors were mapped uncached, so the firmware read every compute descriptor from memory on each command. Allocate the compute descriptor ring from the same pool (fully cached prefix, retained until processor stop) and give the hardware an uncached canonical low alias for the descriptor address it fetches. compute.light d64 27.4 -> 24.3 us/submit, queue-to-queue hop 58.1 -> 54.8 us; compute/sync CTS gate, cross-queue order/data 10k, dependent reads under pressure and mixed churn pass. (cherry picked from commit c85c050ae2ef77cbeebddb70c6602db477675090) --- drivers/gpu/drm/asahi/g17/queue/compute.rs | 46 +++++++++++++--------- drivers/gpu/drm/asahi/g17/runtime.rs | 9 +++-- 2 files changed, 33 insertions(+), 22 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/queue/compute.rs b/drivers/gpu/drm/asahi/g17/queue/compute.rs index 85c65a9b22d53..3547f312224b4 100644 --- a/drivers/gpu/drm/asahi/g17/queue/compute.rs +++ b/drivers/gpu/drm/asahi/g17/queue/compute.rs @@ -15,7 +15,7 @@ use crate::g17::{ queue::*, variant::PointerLayout, }, - object::{Allocator, CpuMap, KernelObject}, + object::{Allocator, CpuMap, Family, KernelObject, Pool, PooledObject}, }; use crate::{hw::t8140::dynamic, mmu}; use core::sync::atomic::{fence, Ordering}; @@ -83,24 +83,32 @@ pub(crate) struct Binding { /// Descriptor ring of one compute QID with its kernel-VM alias. Built without /// the device mutex; installed into a vacant graph before an owner binds. pub(crate) struct DescriptorRing { - object: KernelObject, - high: mmu::KernelMapping, + // Drop the GPU alias before the retained storage returns to its pool. + low: mmu::KernelMapping, + object: PooledObject, } impl DescriptorRing { - pub(crate) fn new(alloc: &Allocator<'_>) -> Result { - let page = mmu::UAT_PGSZ as u64; - let prot = mmu::PROT_GPU_FW_SHARED_RW; - let object = alloc.lower( - DESCRIPTORS * COMPUTE_DESCRIPTOR_SIZE, - page, - prot, + /// Firmware reads every descriptor through a cached kernel mapping, as it + /// does for render. The pool keeps that mapping until processor stop; the + /// hardware reads the same pages through an uncached canonical low alias. + pub(crate) fn new(alloc: &Allocator<'_>, objects: &Arc) -> Result { + let size = DESCRIPTORS * COMPUTE_DESCRIPTOR_SIZE; + let object = objects.allocate( + alloc, + Family::Descriptor, + size, + size, + mmu::PROT_GPU_FW_SHARED_RW, CpuMap::WriteCombined, )?; - let mut high = alloc.uat.geometry().kernel_range(); - high.start += dynamic::KERNEL_OFFSET; - let high = object.alias_in(alloc.uat.kernel_vm(), high, page, prot)?; - Ok(Self { object, high }) + let low = object.alias_in( + alloc.uat.kernel_lower_vm(), + dynamic::LOWER, + mmu::UAT_PGSZ as u64, + mmu::PROT_GPU_SHARED_RW, + )?; + Ok(Self { low, object }) } } @@ -127,6 +135,7 @@ struct Graph { impl Graph { fn new( alloc: &Allocator<'_>, + objects: &Arc, context: &Context, pointers: PointerLayout, operand_state: &'static OperandState, @@ -134,7 +143,7 @@ impl Graph { let page = mmu::UAT_PGSZ as u64; let prot = mmu::PROT_GPU_FW_SHARED_RW; let cpu = CpuMap::WriteCombined; - let ring = DescriptorRing::new(alloc)?; + let ring = DescriptorRing::new(alloc, objects)?; let preempt = alloc.kernel(0x4000, page, prot, cpu)?; let usage = alloc.kernel(mmu::UAT_PGSZ, page, prot, cpu)?; let operand = alloc.lower(operand_state.size, page, prot, cpu)?; @@ -250,12 +259,12 @@ impl Graph { fn descriptor_vas(&self, slot: u8) -> Result<(u64, u64)> { let ring = self.ring.as_ref().ok_or(EAGAIN)?; let offset = (usize::from(slot) % DESCRIPTORS * COMPUTE_DESCRIPTOR_SIZE) as u64; - Ok((ring.high.iova() + offset, ring.object.gpu_va() + offset)) + Ok((ring.object.gpu_va() + offset, ring.low.iova() + offset)) } fn descriptor_alias(&self, vm: &mmu::Vm) -> Result { let ring = self.ring.as_ref().ok_or(EAGAIN)?; ring.object - .map_alias(vm, ring.object.gpu_va(), mmu::PROT_GPU_SHARED_RO) + .map_alias(vm, ring.low.iova(), mmu::PROT_GPU_SHARED_RO) } /// Detaches the descriptor ring of an idle vacant queue; the caller frees it /// off-lock. The job list and the context item only name consumed @@ -547,6 +556,7 @@ impl Queue { /// successfully installed queue until processor stop. Shared VM apertures precede this call. pub(crate) fn new( alloc: &Allocator<'_>, + objects: &Arc, backing: Backing, owner: u64, context: Arc, @@ -554,7 +564,7 @@ impl Queue { operand_state: &'static OperandState, ) -> Result { let Backing { mut pool, mut kick } = backing; - let mut graph = Graph::new(alloc, &context, pointers, operand_state)?; + let mut graph = Graph::new(alloc, objects, &context, pointers, operand_state)?; let kick_alias = kick.map_client(context.vm())?; let pool_alias = pool.map_client(context.vm())?; let binding = graph.bind(context.clone(), kick_alias, pool_alias)?; diff --git a/drivers/gpu/drm/asahi/g17/runtime.rs b/drivers/gpu/drm/asahi/g17/runtime.rs index 736fed78b0cc1..d94e344c6f713 100644 --- a/drivers/gpu/drm/asahi/g17/runtime.rs +++ b/drivers/gpu/drm/asahi/g17/runtime.rs @@ -790,7 +790,7 @@ impl Backend { admission.check()?; let lease = self.shared.preparations.enter_until(admission)?; let dev = self.shared.drm()?; - let (uat, reservation) = { + let (uat, objects, reservation) = { let mut state = self.shared.state.lock(); admission.check()?; let firmware = (*state).as_deref_mut().ok_or(ENODEV)?; @@ -814,7 +814,7 @@ impl Backend { match rebound { Rebind::Done => return Ok(()), Rebind::Grow => { drop(state); drop(lease); return self.grow_compute(required, admission); } - Rebind::Rearm(qid) => (firmware.uat.clone(), Err(qid)), + Rebind::Rearm(qid) => (firmware.uat.clone(), firmware.pool.clone(), Err(qid)), Rebind::Fresh => { let reservation = match firmware.queues.reserve_compute(self.owner) { Ok(reservation) => reservation, @@ -838,7 +838,7 @@ impl Backend { } Err(error) => return Err(error), }; - (firmware.uat.clone(), Ok(reservation)) + (firmware.uat.clone(), firmware.pool.clone(), Ok(reservation)) } } }; @@ -853,7 +853,7 @@ impl Backend { // queue may have been taken or re-armed by another owner since. let built = (|| -> Result<_> { let pages = crate::g17::freelist::Pages::compute_sized(&alloc, required)?; - Ok((pages, compute::DescriptorRing::new(&alloc)?)) + Ok((pages, compute::DescriptorRing::new(&alloc, &objects)?)) })(); let (pages, ring) = match built { Err(error) if error == ENOSPC => { @@ -908,6 +908,7 @@ impl Backend { self.compute_scheduler(&alloc, &lease, admission)?; let queue = compute::Queue::new( &alloc, + &objects, backing, self.owner, self.context.clone(), From a36e352d8564947071fef7e11dba709f399cd106 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Fri, 23 Oct 2026 21:38:10 -0400 Subject: [PATCH 018/388] drm/asahi: size the G17 compute QID budget by open logical queues The compute channel budget was static: compute could only take a QID while every one of the 58 possible execution contexts could still install a render pair, which always left exactly ten compute channels. A client without a channel then made every idle channel of every live client shareable before binding one, so sixteen compute processes evicted each other on nearly every submission (map/unmap and TLB invalidation churn, ComputeReserve refused 52k times in one benchmark pass). Installed QIDs never change engine before processor stop, so a compute channel still permanently narrows render. Size the render reserve by the logical queues that are actually open plus 24 queues of headroom, and refuse a new logical queue only when its dedicated render pair would no longer fit. With every context open this is the previous rule. Rebinding now prefers a channel of a closed owner, then a vacant one to re-arm, then a fresh QID, and only then shares the least recently used idle channel of a single live client. gpubench on Neo (us/submit, matched baseline): compute.light procs d16 92.0 -> 33.4; compute.light queue rows unchanged (d64 27.4 -> 26.5). Stress gate passes. (cherry picked from commit bf7ae5bf04b7d3f14b5ec94ef1f8c1434b4fe494) --- drivers/gpu/drm/asahi/g17/kick.rs | 121 ++++++++++++++------- drivers/gpu/drm/asahi/g17/queue/compute.rs | 24 +++- drivers/gpu/drm/asahi/g17/runtime.rs | 95 +++++++++++++--- 3 files changed, 178 insertions(+), 62 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/kick.rs b/drivers/gpu/drm/asahi/g17/kick.rs index c337a8b298f19..87a90516d1b42 100644 --- a/drivers/gpu/drm/asahi/g17/kick.rs +++ b/drivers/gpu/drm/asahi/g17/kick.rs @@ -24,22 +24,38 @@ use crate::{hw::t8140, mem, mmu}; pub(super) const QID_COUNT: usize = QID_MAX as usize + 1; -/// Minimum render-channel capacity protected from compute-only allocation. -// Protect one tiling/fragment pair for each logical execution context, plus -// the two unpublished bootstrap reservations. The remaining ten compute -// channels are shared after exact retirement, rather than pinned per client. -const COMPUTE_RENDER_RESERVE: usize = 2 * crate::mmu::MAX_EXECUTION_CONTEXTS as usize + 2; -pub(super) const COMPUTE_CHANNELS: usize = QID_COUNT - COMPUTE_RENDER_RESERVE; -/// Logical queues that can each hold a render pair: what the render reserve leaves beside the -/// two bootstrap reservations once the QIDs below `floor`, which the firmware keeps, are gone -/// (every execution context with no floor). +/// Render identities that must stay reachable for `queues` logical queues: +/// one tiling/fragment pair each, plus the two bootstrap reservations. +/// +/// Installed identities never change engine before processor stop, so a +/// compute channel permanently narrows what render can still install. Compute +/// may take a free QID only while every open logical queue, and +/// [`RENDER_HEADROOM`] queues opened later, can still install a dedicated +/// pair; logical-queue admission refuses a queue whose pair no longer fits. +pub(super) const fn render_reserve(queues: usize) -> usize { + let queues = queues + RENDER_HEADROOM; + let max = crate::mmu::MAX_EXECUTION_CONTEXTS as usize; + 2 * if queues < max { queues } else { max } + 2 +} +/// Logical queues that may still open after compute growth without losing a +/// dedicated render pair. +const RENDER_HEADROOM: usize = 24; +/// The render reserve with every execution context open. +const FULL_RENDER_RESERVE: usize = render_reserve(crate::mmu::MAX_EXECUTION_CONTEXTS as usize); +/// Compute channels that stay reachable with every execution context open. +#[cfg(CONFIG_KUNIT)] +pub(super) const COMPUTE_FLOOR: usize = QID_COUNT - FULL_RENDER_RESERVE; +/// Upper bound on distinct physical compute channels named by one kick. +pub(super) const COMPUTE_CHANNELS: usize = QID_COUNT; +/// Maximum logical queues after firmware-owned QIDs are excluded. pub(super) const fn queue_limit(floor: usize) -> u32 { - ((COMPUTE_RENDER_RESERVE - 2 - floor) / 2) as u32 + ((FULL_RENDER_RESERVE - 2 - floor) / 2) as u32 } const _: () = { assert!(queue_limit(0) == crate::mmu::MAX_EXECUTION_CONTEXTS); - assert!(COMPUTE_CHANNELS >= 4); + // Compute keeps this many channels even with every execution context open. + assert!(QID_COUNT - FULL_RENDER_RESERVE >= 4); assert!(crate::hw::t8140::queues::RENDER_PAIRS_PER_OWNER == 1); }; @@ -85,7 +101,7 @@ impl QueueIds { publication: 0, leased: 0, // The reserve must keep render capacity above the floor. - floor: if floor < COMPUTE_RENDER_RESERVE { floor } else { return Err(EINVAL) }, + floor: if floor < FULL_RENDER_RESERVE { floor } else { return Err(EINVAL) }, }) } @@ -113,6 +129,53 @@ impl QueueIds { owner: u64, engine: DataMaster, preferred: Option, + ) -> Result { + self.reserve_for(owner, engine, preferred, crate::mmu::MAX_EXECUTION_CONTEXTS as usize) + } + + fn render_ids(&self) -> usize { + self.entries + .iter() + .flatten() + .filter(|entry| matches!(entry.id.engine, DataMaster::Tiling | DataMaster::Fragment)) + .count() + } + + /// Whether `queues` open logical queues can each still install a dedicated + /// render pair, beside the bootstrap reservations and installed compute. + pub(crate) fn render_fits(&self, queues: usize) -> bool { + self.render_ids() + (QID_COUNT - self.floor - self.leased) >= 2 * queues + 2 + } + + /// Whether a fresh compute identity fits beside the render reserve of + /// `queues` open logical queues, without reserving it. + pub(crate) fn compute_fits(&self, queues: usize) -> bool { + let initial = self.leased == QID_COUNT - self.floor + && self.initial_render.iter().flatten().any(|id| self.can_cancel_unpublished(*id)); + self.compute_room(queues, usize::from(initial)) + } + + fn compute_room(&self, queues: usize, replacement: usize) -> bool { + // Existing render identities already satisfy this reservation. Counting + // them twice can refuse compute even while physical QIDs remain free. + // Replacing an initial reservation consumes one render identity. + // Include that transition when protecting render's minimum, and + // count the replacement as available compute capacity. + let render = self.render_ids(); + QID_COUNT - self.floor - self.leased + replacement + > render_reserve(queues) + .min(FULL_RENDER_RESERVE - self.floor) + .saturating_sub(render - replacement) + } + + /// As [`Self::reserve`], with the compute reserve sized for `queues` open + /// logical queues instead of every possible execution context. + pub(crate) fn reserve_for( + &mut self, + owner: u64, + engine: DataMaster, + preferred: Option, + queues: usize, ) -> Result { if owner == 0 { return Err(EINVAL); @@ -129,10 +192,8 @@ impl QueueIds { Err(EINVAL) }; } - // The QIDs below the floor are never the host's; the reserve shrinks - // with them, so the compute channel count stays the same. + // The QIDs below the floor are never available to the host. let total = QID_COUNT - self.floor; - let reserve = COMPUTE_RENDER_RESERVE - self.floor; let initial = if engine == DataMaster::Compute && self.leased == total { self.initial_render .iter() @@ -145,27 +206,9 @@ impl QueueIds { } else { None }; - // Existing render identities already satisfy this reservation. Counting - // them twice can refuse compute even while physical QIDs remain free. // Published identities always retain their original lifetime rules. - if engine == DataMaster::Compute { - let render = self - .entries - .iter() - .flatten() - .filter(|entry| { - matches!(entry.id.engine, DataMaster::Tiling | DataMaster::Fragment) - }) - .count(); - // Replacing an initial reservation consumes one render identity. - // Include that transition when protecting render's minimum, and - // count the replacement as available compute capacity. - let replacement = usize::from(initial.is_some()); - if total - self.leased + replacement - <= reserve.saturating_sub(render - replacement) - { - return Err(crate::g17::capacity::refused(crate::g17::capacity::Site::ComputeReserve)); - } + if engine == DataMaster::Compute && !self.compute_room(queues, usize::from(initial.is_some())) { + return Err(crate::g17::capacity::refused(crate::g17::capacity::Site::ComputeReserve)); } let index = preferred .map(|index| usize::from(index) + self.floor) @@ -249,10 +292,10 @@ mod accounting_tests { #[test] fn render_identities_satisfy_the_compute_reserve() { let mut ids = table(); - for owner in 1..=COMPUTE_RENDER_RESERVE as u64 { + for owner in 1..=FULL_RENDER_RESERVE as u64 { ids.reserve(owner, DataMaster::Tiling, None).unwrap(); } - for owner in COMPUTE_RENDER_RESERVE as u64 + 1..=QID_COUNT as u64 { + for owner in FULL_RENDER_RESERVE as u64 + 1..=QID_COUNT as u64 { ids.reserve(owner, DataMaster::Compute, None).unwrap(); } assert_eq!(ids.leased, QID_COUNT); @@ -263,14 +306,14 @@ mod accounting_tests { #[test] fn compute_cannot_consume_the_render_minimum() { let mut ids = table(); - for owner in 1..=(QID_COUNT - COMPUTE_RENDER_RESERVE) as u64 { + for owner in 1..=(QID_COUNT - FULL_RENDER_RESERVE) as u64 { ids.reserve(owner, DataMaster::Compute, None).unwrap(); } assert_eq!(ids.reserve(1000, DataMaster::Compute, None), Err(ENOSPC)); let tiling = ids.reserve(1001, DataMaster::Tiling, None).unwrap(); assert_eq!(ids.reserve(1000, DataMaster::Compute, None), Err(ENOSPC)); ids.cancel_unpublished(tiling).unwrap(); - assert_eq!(ids.leased, QID_COUNT - COMPUTE_RENDER_RESERVE); + assert_eq!(ids.leased, QID_COUNT - FULL_RENDER_RESERVE); } #[test] diff --git a/drivers/gpu/drm/asahi/g17/queue/compute.rs b/drivers/gpu/drm/asahi/g17/queue/compute.rs index 3547f312224b4..69841e5cd2aeb 100644 --- a/drivers/gpu/drm/asahi/g17/queue/compute.rs +++ b/drivers/gpu/drm/asahi/g17/queue/compute.rs @@ -534,6 +534,7 @@ pub(crate) struct Queue { published_config: Option, ordinal: u32, submitted: u32, + last_published: u64, last_end: u64, released: bool, retirement_ready: bool, @@ -597,6 +598,7 @@ impl Queue { published_config: None, ordinal: 0, submitted: 0, + last_published: 0, last_end: 0, released: false, retirement_ready: false, @@ -1926,6 +1928,7 @@ impl Queue { priority: policy.priority(), }; let published = host.next_compute_publication()?; + self.last_published = published; self.active .push(Active { packet: packet.clone(), @@ -2323,16 +2326,27 @@ impl Queue { /// Logical queue/context ownership is unchanged. The existing binding is /// retained through the successor's exact replacement witness. pub(crate) fn share_idle(&mut self) -> Result { - if !self.installed || self.owner.is_none() || !self.retirement_ready - || self.growing || self.quarantined || self.awaiting_witness - || self.exit.is_some() || self.active.len != 0 || self.replays.len != 0 - || self.previous.is_some() || self.pool_phase != PoolPhase::Populated - { + if !self.shareable() { return Ok(false); } self.release_owner()?; Ok(self.reusable()) } + + /// An open client's idle channel that [`Self::share_idle`] can hand over. + pub(crate) fn shareable(&self) -> bool { + self.installed && self.owner.is_some() && self.retirement_ready + && !self.growing && !self.quarantined && !self.awaiting_witness + && self.exit.is_none() && self.active.len == 0 && self.replays.len == 0 + && self.previous.is_none() && self.pool_phase == PoolPhase::Populated + && !self.source_hint.has_readers(&self.source_readers) + } + + /// Device publication order of this channel's newest command: lower is + /// idle longer. + pub(crate) fn last_published(&self) -> u64 { + self.last_published + } } impl Queue { diff --git a/drivers/gpu/drm/asahi/g17/runtime.rs b/drivers/gpu/drm/asahi/g17/runtime.rs index d94e344c6f713..44770b0cb3041 100644 --- a/drivers/gpu/drm/asahi/g17/runtime.rs +++ b/drivers/gpu/drm/asahi/g17/runtime.rs @@ -155,12 +155,43 @@ impl Registry { Rebind::Fresh }); } - // Physical channels are a device budget, not a permanent entitlement - // of each open logical queue. Only exact idle retirement can yield one. - for queue in self.compute.iter_mut().flatten().filter_map(|entry| entry.queue.as_deref_mut()) { - admission.check()?; - queue.share_idle()?; + // A binding persists for its logical queue's lifetime while physical + // QIDs allow: take a channel of a closed owner, then a fresh QID, and + // only then the least recently idle channel of one open owner. + if let Some(rebind) = self.bind_reusable(owner, context, required, admission)? { + return Ok(rebind); + } + if self.qids.compute_fits(self.admission.queues() as usize) { + return Ok(Rebind::Fresh); } + let teardown = &self.teardown; + let idle = self + .compute + .iter() + .flatten() + .filter_map(|entry| entry.queue.as_deref()) + .filter(|queue| queue.shareable()) + .filter(|queue| !queue.context().is_some_and(|bound| teardown.releasing(bound))) + .min_by_key(|queue| (!queue.pool_fits(required), queue.last_published())) + .map(|queue| queue.qid()); + let Some(qid) = idle else { return Err(ENOSPC); }; + admission.check()?; + let queue = self + .compute + .iter_mut() + .flatten() + .filter_map(|entry| entry.queue.as_deref_mut()) + .find(|queue| queue.qid() == qid) + .ok_or(EIO)?; + if !queue.share_idle()? { + return Err(ENOSPC); + } + Ok(self.bind_reusable(owner, context, required, admission)?.unwrap_or(Rebind::Fresh)) + } + + /// Binds `owner` to a reusable channel whose last owner released it, or + /// names a vacant one to re-arm. None if no channel is reusable. + fn bind_reusable(&mut self, owner: u64, context: &Arc, required: usize, admission: &Admission<'_>) -> Result> { let mut vacant = None; let teardown = &self.teardown; for queue in self @@ -183,9 +214,9 @@ impl Registry { } if !queue.pool_fits(required) { continue; } queue.bind_owner(owner, context.clone())?; - return Ok(Rebind::Done); + return Ok(Some(Rebind::Done)); } - Ok(vacant.map_or(Rebind::Fresh, Rebind::Rearm)) + Ok(vacant.map(Rebind::Rearm)) } /// Installs fresh USC backing into the vacant queue `qid`; the caller writes @@ -352,9 +383,10 @@ impl Registry { .ok_or_else(|| self.compute_admission_error( crate::g17::capacity::refused(crate::g17::capacity::Site::ComputeSlots)))?; let identity = self.identities.reserve(1)?.start; + let queues = self.admission.queues() as usize; let id = self .qids - .reserve(identity, DataMaster::Compute, Some(FIRST_COMPUTE_QID)) + .reserve_for(identity, DataMaster::Compute, Some(FIRST_COMPUTE_QID), queues) .map_err(|error| self.compute_admission_error(error))?; let reservation = ComputeReservation { slot, id, owner }; self.compute[slot] = Some(ComputeEntry { @@ -491,6 +523,11 @@ impl Backend { return Err(EIO); } let slot = firmware.queues.admission.reserve_queue()?; + // Installed compute channels narrow render's QIDs: every open + // logical queue keeps a dedicated render pair within reach. + if !firmware.queues.qids.render_fits(firmware.queues.admission.queues() as usize) { + return Err(super::capacity::refused(super::capacity::Site::LogicalQueues)); + } match firmware.uat.new_execution_context(vm) { Ok(execution) => { break ( @@ -1861,8 +1898,9 @@ impl Registry { self.compute.iter().flatten().count(), self.compute.len(), self.compute.iter().flatten().filter_map(|entry| entry.queue.as_deref()).filter(|queue| queue.reusable()).count(), self.compute.iter().flatten().filter(|entry| entry.queue.is_none()).count())?; - writeln!(f, "compute_channels used={} total={}", - self.compute.iter().flatten().count(), super::kick::COMPUTE_CHANNELS)?; + writeln!(f, "compute_channels used={} fresh_fits={}", + self.compute.iter().flatten().count(), + u8::from(self.qids.compute_fits(self.admission.queues() as usize)))?; writeln!(f, "scheduler_teardown pending={}", self.teardown.len())?; for queue in self.compute.iter().flatten().filter_map(|entry| entry.queue.as_deref()) { queue.write_capacity(f)?; @@ -1893,10 +1931,6 @@ impl Backend { if firmware.recovery.pending() || super::recovery::Memory::recovery_state(&firmware.init)? != 0 { return Ok(()); } - for queue in firmware.queues.compute.iter_mut().flatten().filter_map(|entry| entry.queue.as_deref_mut()) { - admission.check()?; - queue.share_idle()?; - } // Consumption, not publication or an elapsed timeout, frees backing. firmware.queues.observe_pool_releases(&firmware.init, &mut deferred)?; // Pool eviction addresses backing allocation pressure, not channel @@ -1937,12 +1971,17 @@ mod shared_capacity_tests { ) .unwrap(); let mut leases = KVec::with_capacity(crate::mmu::MAX_EXECUTION_CONTEXTS as usize, GFP_KERNEL).unwrap(); - // Physical compute channels are device-owned and bounded independently - // of how many logical clients have queued unsignalled work. - for owner in 1..=super::super::kick::COMPUTE_CHANNELS as u64 { + let mut slots = KVec::with_capacity(crate::mmu::MAX_EXECUTION_CONTEXTS as usize, GFP_KERNEL).unwrap(); + for _ in 0..crate::mmu::MAX_EXECUTION_CONTEXTS { + slots.push(registry.admission.reserve_queue().unwrap(), GFP_KERNEL).unwrap(); + } + // With every execution context open, compute keeps only the channels + // that leave each logical queue a dedicated render pair. + for owner in 1..=super::super::kick::COMPUTE_FLOOR as u64 { registry.reserve_compute(owner).unwrap(); } assert!(matches!(registry.reserve_compute(1000), Err(EAGAIN))); + assert!(registry.qids.render_fits(slots.len())); for owner in 1..=u64::from(crate::mmu::MAX_EXECUTION_CONTEXTS) { let lease = registry.admission.acquire_render(owner, None, u64::MAX, || Ok(())).unwrap(); registry.ensure_render_ids(lease.slot(), owner).unwrap(); @@ -1994,11 +2033,31 @@ mod shared_capacity_tests { leases.push(lease, GFP_KERNEL).unwrap(); } } - for owner in 1..=super::super::kick::COMPUTE_CHANNELS as u64 { + for owner in 1..=super::super::kick::COMPUTE_FLOOR as u64 { registry.reserve_compute(owner).unwrap(); } assert_eq!(leases.len(), 36); } + + #[test] + fn compute_grows_with_free_qids_and_narrows_queue_admission() { + let mut registry = KBox::init(Registry::new(1, [0; 2]), GFP_KERNEL).unwrap(); + let mut slots = KVec::with_capacity(20, GFP_KERNEL).unwrap(); + for _ in 0..20 { + slots.push(registry.admission.reserve_queue().unwrap(), GFP_KERNEL).unwrap(); + } + // Sixteen open compute clients each keep their own channel. + for owner in 1..=16u64 { + registry.reserve_compute(owner).unwrap(); + } + // Growth stops where open queues plus headroom would lose their pairs. + let mut owner = 17; + while registry.reserve_compute(owner).is_ok() { + owner += 1; + } + assert!(registry.qids.render_fits(slots.len() + 24)); + assert!(!registry.qids.render_fits(crate::mmu::MAX_EXECUTION_CONTEXTS as usize)); + } } impl Backend { From b141ca9b16c33abb26238782cd7c0b751b5f52fe Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Fri, 23 Oct 2026 21:40:28 -0400 Subject: [PATCH 019/388] drm/asahi: order same-VM render attachment overlaps in firmware A render whose attachment spans overlapped a live render of another logical queue in the same VM waited in admission, on the host, until that render retired. gpubench render.light queues renders every queue into one image, so all queues ran one render at a time: run_job blocked for (N-1) renders and the 2/4/8-queue rows stayed at one serialized step (174/201/217 us/submit). Order such overlaps at publication instead. The device mutex already orders publications; the later render's fragment kick now waits in firmware for the newest published overlapping fragment kick of every other pair, so its tiling kick stays concurrent and no host wait or scheduler stall is involved. Published QIDs keep their kick rings, so these stamps only advance. An admission bound on other pairs of the VM keeps the barrier count within its fixed room. Cross-VM overlaps through shared objects keep the host wait, so one VM's fault can never hold another VM's kick in firmware. render.light queues d2/d4/d8: 173.8/201.3/216.9 -> 81.4/66.9/62.1 us/submit. Other rows unchanged; the mild stress gate (CTS compute/sync, cross-queue order/data, slow-read pressure, mixed churn) passes. (cherry picked from commit 809d95cfdd685b6f94570cd313b1723c7f01f466) --- drivers/gpu/drm/asahi/g17/completion.rs | 7 ++ drivers/gpu/drm/asahi/g17/hazard.rs | 46 ++++++++++- drivers/gpu/drm/asahi/g17/queue/admission.rs | 78 ++++++++++++++++++- drivers/gpu/drm/asahi/g17/queue/render.rs | 17 +++- .../drm/asahi/g17/runtime/render/submit.rs | 10 ++- 5 files changed, 150 insertions(+), 8 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/completion.rs b/drivers/gpu/drm/asahi/g17/completion.rs index 5936bcb1b933a..d692a98acea8c 100644 --- a/drivers/gpu/drm/asahi/g17/completion.rs +++ b/drivers/gpu/drm/asahi/g17/completion.rs @@ -547,6 +547,13 @@ impl Completion { .map(super::queue::admission::RenderLease::slot) } + /// Called under the device mutex once the render's kicks are committed. + pub(crate) fn note_render_publication(&self, fragment: super::fw::kick::KickDependency) { + if let Some(lease) = self.render.lock().as_ref() { + lease.note_publication(fragment); + } + } + /// Called under the device mutex before deferring the error signal. pub(crate) fn quarantine_render(&self) { if let Some(lease) = self.render.lock().as_ref() { diff --git a/drivers/gpu/drm/asahi/g17/hazard.rs b/drivers/gpu/drm/asahi/g17/hazard.rs index 20daaab85b832..4d9e99d9a9cc0 100644 --- a/drivers/gpu/drm/asahi/g17/hazard.rs +++ b/drivers/gpu/drm/asahi/g17/hazard.rs @@ -3,11 +3,21 @@ //! Attachment overlap ordering across physical render pairs and shared images. //! Tile stores can overlap even when the application's pixel regions do not. //! Keep independent attachments concurrent; order only conflicting live spans. +//! +//! Within one VM, a render of another logical owner is ordered at publication: +//! the device mutex totally orders publications, and the later render's +//! fragment kick waits in firmware for the newest overlapping fragment kick of +//! every other pair. Admission never waits for such an overlap. Only a +//! cross-VM overlap through a shared object waits on the host for retirement, +//! so that one VM's fault can never hold another VM's kick in firmware. use super::job::Order; const MAX_SPANS: usize = 24; pub(crate) const WAIT_MS: u32 = 1000; +/// Firmware barriers one render publication may add for same-VM overlaps, +/// one per other pair. +pub(crate) const PUBLICATION_BARRIERS: usize = 7; #[derive(Clone, Copy)] struct Span { @@ -56,7 +66,12 @@ impl Footprint { self.shared |= self.count != before; } - fn overlaps(&self, other: &Self) -> bool { + /// Whether a publication orders an overlap with `other` in firmware. + pub(crate) fn same_vm(&self, other: &Self) -> bool { + self.vm == other.vm + } + + pub(crate) fn overlaps(&self, other: &Self) -> bool { if self.vm != other.vm && !(self.shared && other.shared) { return false; } @@ -86,6 +101,13 @@ pub(crate) struct Live<'a> { /// A queue prefix already orders covered predecessors. Pending arrivals order /// different owners fairly without imposing order on unrelated attachments. +/// +/// A same-VM overlap with another owner's admitted render needs no wait; its +/// publication carries one barrier per other pair. That count stays within +/// [`PUBLICATION_BARRIERS`] because such a render is admitted only while fewer +/// than `PUBLICATION_BARRIERS + 1` other pairs of its VM hold admitted renders: +/// of any set of renders overlapping one publication, the last admitted saw all +/// the others live. pub(crate) fn plan<'a>( owner: u64, prefix: Option, @@ -94,10 +116,29 @@ pub(crate) fn plan<'a>( live: impl IntoIterator>, ) -> Plan { let mut selected = None; + let mut ordered = false; + let mut pairs = [0u8; PUBLICATION_BARRIERS]; + let mut count = 0; + let mut crowded = false; for entry in live { + let foreign = entry.owner != owner && footprint.same_vm(entry.footprint); + if let Some(slot) = entry.slot.filter(|_| foreign) { + if !pairs[..count].contains(&slot) { + if count == PUBLICATION_BARRIERS { + crowded = true; + } else { + pairs[count] = slot; + count += 1; + } + } + } if entry.quarantined || !footprint.overlaps(entry.footprint) { continue; } + if foreign { + ordered |= entry.slot.is_some(); + continue; + } let Some(slot) = entry.slot else { if entry.owner != owner && ticket.is_none_or(|mine| entry.ticket < mine) { return Plan::Wait; @@ -116,5 +157,8 @@ pub(crate) fn plan<'a>( Some(_) => return Plan::Wait, } } + if ordered && crowded { + return Plan::Wait; + } selected.map_or(Plan::Any, Plan::Pair) } diff --git a/drivers/gpu/drm/asahi/g17/queue/admission.rs b/drivers/gpu/drm/asahi/g17/queue/admission.rs index ebdc79d2b2dd3..7533d345cfa6d 100644 --- a/drivers/gpu/drm/asahi/g17/queue/admission.rs +++ b/drivers/gpu/drm/asahi/g17/queue/admission.rs @@ -9,6 +9,7 @@ //! a graph or waiting for GPU work. Capacity waiters sleep on the release event. use crate::g17::{ + fw::kick::KickDependency, hazard::{self, Footprint, Plan}, job::Packet, }; @@ -43,6 +44,9 @@ struct InFlight { slot: Option, ticket: u64, footprint: Arc, + /// Fragment kick of the published render; a later same-VM overlap of + /// another pair waits for it in firmware. + fragment: Option, } const QUARANTINE_WAIT_MS: u32 = 1000; @@ -332,6 +336,7 @@ impl State { slot, ticket, footprint: request.footprint.clone(), + fragment: None, }, GFP_KERNEL, ) @@ -339,6 +344,12 @@ impl State { Some((owner, request.sequence)) } + fn position(&self, slot: u8, (owner, sequence): (u64, u64)) -> Option { + self.footprints.iter().position(|entry| { + entry.slot == Some(slot) && entry.owner == owner && entry.sequence == sequence + }) + } + fn forget_waiter(&mut self, ticket: Option) { if let Some(ticket) = ticket { self.footprints @@ -705,13 +716,72 @@ impl RenderLease { self.rehabilitate_after_processor_stop(); } } +impl RenderLease { + /// Called under the device mutex once the render's kicks are committed, + /// including a replay, which supersedes the earlier kick. + pub(crate) fn note_publication(&self, fragment: KickDependency) { + let Some(record) = self.record else { + return; + }; + let mut state = self.pool.state.lock(); + if let Some(index) = state.position(self.slot, record) { + state.footprints[index].fragment = Some(fragment); + } + } +} + +impl Pool { + /// Under the device mutex, which orders every publication: the newest + /// published fragment kick of each other pair whose live same-VM render + /// overlaps `footprint`. Quarantined pairs are skipped as in admission. + pub(crate) fn publication_barriers( + &self, + owner: u64, + slot: u8, + footprint: &Footprint, + barriers: &mut [KickDependency; hazard::PUBLICATION_BARRIERS], + ) -> Result { + let state = self.state.lock(); + let mut slots = [0u8; hazard::PUBLICATION_BARRIERS]; + let mut count = 0; + for entry in state.footprints.iter() { + let (Some(other), Some(fragment)) = (entry.slot, entry.fragment) else { + continue; + }; + if other == slot + || entry.owner == owner + || state.quarantined & (1u64 << other) != 0 + || !footprint.same_vm(&entry.footprint) + || !footprint.overlaps(&entry.footprint) + { + continue; + } + match slots[..count].iter().position(|known| *known == other) { + // One fragment ring completes in kick order; same QID, so the + // word orders by timestamp. + Some(index) => { + if fragment.word() > barriers[index].word() { + barriers[index] = fragment; + } + } + // Admission bounds the pairs; see hazard::plan. + None if count == hazard::PUBLICATION_BARRIERS => return Err(EBUSY), + None => { + slots[count] = other; + barriers[count] = fragment; + count += 1; + } + } + } + Ok(count) + } +} + impl Drop for RenderLease { fn drop(&mut self) { let mut state = self.pool.state.lock(); - if let Some((owner, sequence)) = self.record { - if let Some(index) = state.footprints.iter().position(|entry| { - entry.slot == Some(self.slot) && entry.owner == owner && entry.sequence == sequence - }) { + if let Some(record) = self.record { + if let Some(index) = state.position(self.slot, record) { state.footprints.swap_remove(index); } } diff --git a/drivers/gpu/drm/asahi/g17/queue/render.rs b/drivers/gpu/drm/asahi/g17/queue/render.rs index fb6c77fbdf04f..dfe3426eb6c24 100644 --- a/drivers/gpu/drm/asahi/g17/queue/render.rs +++ b/drivers/gpu/drm/asahi/g17/queue/render.rs @@ -46,6 +46,9 @@ pub(crate) struct RenderDependencies { pub(crate) tiling: [KickDependency; 2], pub(crate) tiling_count: usize, pub(crate) compute: Option, + /// Fragment kicks of other pairs' overlapping same-VM renders. + pub(crate) hazards: [KickDependency; crate::g17::hazard::PUBLICATION_BARRIERS], + pub(crate) hazard_count: usize, } impl RenderDependencies { @@ -55,6 +58,8 @@ impl RenderDependencies { tiling: [KickDependency::ZERO; 2], tiling_count: 0, compute: None, + hazards: [KickDependency::ZERO; crate::g17::hazard::PUBLICATION_BARRIERS], + hazard_count: 0, }; pub(crate) fn render(&self) -> &[KickDependency] { &self.render[..self.render_count] @@ -484,7 +489,7 @@ impl Pair { struct RenderBarriers { tiling: [KickDependency; 3], tiling_count: usize, - fragment: [KickDependency; 3], + fragment: [KickDependency; 3 + crate::g17::hazard::PUBLICATION_BARRIERS], fragment_count: usize, } @@ -495,7 +500,7 @@ fn render_barriers( ) -> RenderBarriers { let mut tiling_barriers = [KickDependency::ZERO; 3]; let mut tiling_count = 0; - let mut fragment_barriers = [own_tiling; 3]; + let mut fragment_barriers = [own_tiling; 3 + crate::g17::hazard::PUBLICATION_BARRIERS]; let mut fragment_count = 1; for dependency in dependencies.render() { if fragment_stage { @@ -521,6 +526,11 @@ fn render_barriers( tiling_barriers[tiling_count] = compute; tiling_count += 1; } + // Only tile stores overlap: the tiling kick stays concurrent. + for hazard in &dependencies.hazards[..dependencies.hazard_count] { + fragment_barriers[fragment_count] = *hazard; + fragment_count += 1; + } RenderBarriers { tiling: tiling_barriers, tiling_count, @@ -840,6 +850,9 @@ impl Pair { } }; self.ordinal = next_ordinal; + if let Some(fragment) = KickDependency::new(qids[1], kicks[1]) { + packet.completion.note_render_publication(fragment); + } self.cursors = windows.map(|window| window.target()); for stage in 0..2 { if present[stage] { diff --git a/drivers/gpu/drm/asahi/g17/runtime/render/submit.rs b/drivers/gpu/drm/asahi/g17/runtime/render/submit.rs index e7d0cf1168c98..663e62e8f86fc 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render/submit.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render/submit.rs @@ -462,7 +462,15 @@ impl crate::g17::Firmware { { return Err(EBUSY); } - let dependencies = self.queues.render_dependencies(owner, packet, state)?; + let mut dependencies = self.queues.render_dependencies(owner, packet, state)?; + if let Some(footprint) = packet.footprint.as_ref() { + dependencies.hazard_count = self.queues.admission.publication_barriers( + owner, + slot, + footprint, + &mut dependencies.hazards, + )?; + } let pair = self.queues.render.entry(slot)?.pair.as_ref().ok_or(EIO)?; // An owner change taken from a live owner which closed meanwhile: its // first command waits until that scheduler's release has been consumed. From a475f4f9f1f721b6636a4147dacbcf9f6224763b Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Fri, 23 Oct 2026 22:01:13 -0400 Subject: [PATCH 020/388] drm/asahi: map G17 compute command records cached for the firmware Each compute command writes a queue configuration record, an optional predecessor and completion-wait records, and an announcement; the firmware read all of them through an uncached mapping. Render keeps its equivalent configuration records cached and only its announcements uncached. Move the configuration, predecessor and completion-wait records into a retained pool object with a firmware-cached mapping at the same per-slot offsets; announcements stay uncached. compute.light d1 161.0 -> 152.2 us, d64 24.3 -> 23.1 us/submit, queue-to-queue hop 54.8 -> 51.2 us; compute/sync CTS gate, cross-queue order/data 10k, dependent reads under pressure and mixed churn pass. (cherry picked from commit b3d5b9d6294bb46d63778264205da9d3b750651e) --- drivers/gpu/drm/asahi/g17/queue/compute.rs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/asahi/g17/queue/compute.rs b/drivers/gpu/drm/asahi/g17/queue/compute.rs index 69841e5cd2aeb..625f65e6d6772 100644 --- a/drivers/gpu/drm/asahi/g17/queue/compute.rs +++ b/drivers/gpu/drm/asahi/g17/queue/compute.rs @@ -123,7 +123,11 @@ struct Graph { graph: KernelObject, context_low: KernelObject, context_high: KernelObject, + /// Announcements, which stay uncached for the firmware. records: KernelObject, + /// Configuration, predecessor and completion-wait records at the same + /// offsets, which the firmware reads through a cached mapping. + cached_records: PooledObject, record_ends: [Option; RECORD_SLOTS], reclaimed: u32, /// Host words of the pointer block in this firmware build. @@ -171,6 +175,14 @@ impl Graph { let context_low = alloc.lower(CONTEXT_SIZE, page, prot, cpu)?; let context_high = alloc.kernel(CONTEXT_SIZE, page, prot, cpu)?; let records = alloc.kernel(RECORD_SLOTS * RECORD_STRIDE, page, prot, cpu)?; + let cached_records = objects.allocate( + alloc, + Family::QueueConfig, + RECORD_SLOTS * RECORD_STRIDE, + RECORD_SLOTS * RECORD_STRIDE, + prot, + cpu, + )?; Ok(Self { ring: Some(ring), preempt, @@ -181,6 +193,7 @@ impl Graph { context_low, context_high, records, + cached_records, record_ends: [None; RECORD_SLOTS], reclaimed: 0, pointers, @@ -1729,7 +1742,12 @@ impl Graph { if slot >= RECORD_SLOTS { return Err(EINVAL); } - (&mut self.records, slot * RECORD_STRIDE + offset) + let object: &mut KernelObject = if offset == ANNOUNCE { + &mut self.records + } else { + &mut self.cached_records + }; + (object, slot * RECORD_STRIDE + offset) } else { (&mut self.graph, 0x6800 + offset) }; From ca71e4449b4a23d5f4e0149db76cf5198bfa7e5f Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Fri, 23 Oct 2026 22:03:59 -0400 Subject: [PATCH 021/388] drm/asahi: stop the G17 compute completion scan once every record is found Every event pass with an unobserved compute command walked all 2048 records of the compute completion ring under the device mutex to find the few it needed; that walk was half of the event worker's CPU time. Resume each scan after the last record the previous scan needed and stop as soon as every requested identity has an observation. A scan that finds nothing still wraps the whole ring once, as before. scan_compute leaves the top of the 16-process profile; benchmark rows are unchanged within noise (they are not bound by the scan). Compute/sync CTS gate, cross-queue order/data 10k, dependent reads under pressure and mixed churn pass. (cherry picked from commit 67a49c9c40b31b96a7834c8c185dc20e0bac1fa2) --- drivers/gpu/drm/asahi/g17/initdata.rs | 21 +++++++++++++------ .../drm/asahi/g17/queue/compute/retirement.rs | 12 +++++++++++ drivers/gpu/drm/asahi/g17/runtime.rs | 15 +++++++++++-- 3 files changed, 40 insertions(+), 8 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/initdata.rs b/drivers/gpu/drm/asahi/g17/initdata.rs index 049c93b6a466a..b9a8f326d970d 100644 --- a/drivers/gpu/drm/asahi/g17/initdata.rs +++ b/drivers/gpu/drm/asahi/g17/initdata.rs @@ -679,19 +679,28 @@ impl InitData { /// Snapshot the shared compute completion ring once per service transaction. The queue /// scanner latches exact identities; firmware retains sole ownership of these slots. + /// Visits ring slots from `start`, wrapping once, until `record` reports + /// that nothing more is wanted. Returns the slot after the last one visited + /// when the scan stopped early, or `start` after a complete wrap. pub(super) fn scan_compute_completions( &self, - mut record: impl FnMut(ComputeCompletion<'_>) -> Result, - ) -> Result { + start: usize, + mut record: impl FnMut(ComputeCompletion<'_>) -> Result, + ) -> Result { let object = self.object(self.completion_rings[0])?; + let slots = (abi::COMPLETION_RING_SIZE / 0x40).min(0x800); + let start = start % slots; fence(Ordering::Acquire); - for slot in 0..(abi::COMPLETION_RING_SIZE / 0x40).min(0x800) { - record(ComputeCompletion { + for step in 0..slots { + let slot = (start + step) % slots; + if record(ComputeCompletion { object, offset: slot * 0x40, - })?; + })? { + return Ok((slot + 1) % slots); + } } - Ok(()) + Ok(start) } fn object(&self, index: usize) -> Result<&KernelObject> { diff --git a/drivers/gpu/drm/asahi/g17/queue/compute/retirement.rs b/drivers/gpu/drm/asahi/g17/queue/compute/retirement.rs index e938ec566dbac..655e2921f2582 100644 --- a/drivers/gpu/drm/asahi/g17/queue/compute/retirement.rs +++ b/drivers/gpu/drm/asahi/g17/queue/compute/retirement.rs @@ -174,6 +174,8 @@ pub(crate) struct Batch { requests: KVVec, slots: [(usize, usize); QIDS], open: Option, + /// Requests with no observation yet; the scan stops when this reaches zero. + unobserved: usize, } impl Batch { @@ -182,12 +184,18 @@ impl Batch { requests: KVVec::with_capacity(QIDS * IN_FLIGHT, GFP_KERNEL)?, slots: [(0, 0); QIDS], open: None, + unobserved: 0, }) } pub(crate) fn clear(&mut self) { self.requests.clear(); self.slots.fill((0, 0)); self.open = None; + self.unobserved = 0; + } + /// True once every requested identity has an observation. + pub(crate) fn complete(&self) -> bool { + self.unobserved == 0 } pub(crate) fn request(&mut self, ticket: &Ticket) -> Result { if ticket.descriptor == 0 || ticket.kick == 0 { @@ -214,6 +222,7 @@ impl Batch { .map_err(|_| ENOSPC)?; *slot = (start, slot.1 + 1); self.open = Some(ticket.qid); + self.unobserved += 1; Ok(()) } /// Capture the control word once, then read only a requested identity's payload. @@ -247,6 +256,9 @@ impl Batch { { return Ok(()); } + if request.observed.is_none() { + self.unobserved -= 1; + } request.observed = Some(Observation { timestamps }); Ok(()) } diff --git a/drivers/gpu/drm/asahi/g17/runtime.rs b/drivers/gpu/drm/asahi/g17/runtime.rs index 44770b0cb3041..745cd62cae595 100644 --- a/drivers/gpu/drm/asahi/g17/runtime.rs +++ b/drivers/gpu/drm/asahi/g17/runtime.rs @@ -86,6 +86,8 @@ pub(super) struct Registry { compute_scheduler_building: bool, replay_outer_wait: Option, pub(super) observations: compute::retirement::Batch, + /// Completion-ring slot where the next compute scan starts. + completion_cursor: usize, worker_deferred: Option, teardown: super::teardown::Pending, } @@ -121,6 +123,7 @@ impl Registry { compute_scheduler_building: false, replay_outer_wait: None, observations: compute::retirement::Batch::new()?, + completion_cursor: 0, worker_deferred: Some(DeferredBatch::worker()?), teardown: super::teardown::Pending::new()?, }) @@ -1403,9 +1406,17 @@ impl super::Firmware { { requested |= queue.request_observations(&mut registry.observations, masks)?; } + // Completion records are appended in order: resume after the last + // record a previous scan needed and stop once every request is found. if requested { - self.init - .scan_compute_completions(|record| registry.observations.fold(record))?; + let observations = &mut registry.observations; + registry.completion_cursor = self.init.scan_compute_completions( + registry.completion_cursor, + |record| { + observations.fold(record)?; + Ok(observations.complete()) + }, + )?; } let view = qos::View::new(self.init.qos()?)?; for queue in registry From 1cdac9ba0dddec96f59eb02a5067ee343aae43f4 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Fri, 23 Oct 2026 22:06:56 -0400 Subject: [PATCH 022/388] drm/asahi: publish G17 compute into a bound queue without the device mutex Every compute publication took the device mutex, which the event worker also holds for each service pass, and repeated device-wide work on every submit (replay scan, render-prefix scan, binding checks). Give each physical compute queue its own lock (a cell the registry owns; device-mutex holders lock cells one at a time) and move the device-wide state a publication needs into a per-session port that is reached under a preparation lease: compute work-ring slot reservations, QoS accounting under its own spinlock, the recovery generation, the full-effort flag and the doorbell, whose RTKit borrows are serialized by a mailbox lock. The steady-state path (bound, installed, no input sources, no render prefix) publishes from the submit ioctl or run_job with only the queue's lock and the port's small locks; everything else falls back to the device-mutex path, which now reserves work-ring slots through the same port. A command published while the event worker is scanning is not part of that scan's completion snapshot and is left unobserved until the next one. gpubench on Neo (us/submit, vs matched baseline): compute.light d1 166.3 -> 156.2, d64 27.4 -> 25.8, procs d16 33.4 -> 33.0 (firmware per command time now bounds these rows). Stress gate passes. (cherry picked from commit 8fc91b2117de925e362342a842b0d0befbba5312) --- drivers/gpu/drm/asahi/g17/context.rs | 4 + drivers/gpu/drm/asahi/g17/initdata.rs | 63 ++- drivers/gpu/drm/asahi/g17/kick.rs | 9 - drivers/gpu/drm/asahi/g17/mod.rs | 112 ++++- drivers/gpu/drm/asahi/g17/port.rs | 267 ++++++++++++ drivers/gpu/drm/asahi/g17/power.rs | 2 +- drivers/gpu/drm/asahi/g17/qos.rs | 19 +- drivers/gpu/drm/asahi/g17/queue/compute.rs | 5 + .../drm/asahi/g17/queue/compute/retirement.rs | 7 +- drivers/gpu/drm/asahi/g17/runtime.rs | 386 ++++++++++++++---- .../drm/asahi/g17/runtime/compute_control.rs | 13 +- .../gpu/drm/asahi/g17/runtime/compute_exit.rs | 28 +- .../drm/asahi/g17/runtime/compute_replay.rs | 22 +- drivers/gpu/drm/asahi/g17/runtime/events.rs | 12 +- drivers/gpu/drm/asahi/g17/runtime/render.rs | 21 +- drivers/gpu/drm/asahi/g17/runtime/teardown.rs | 2 +- 16 files changed, 784 insertions(+), 188 deletions(-) create mode 100644 drivers/gpu/drm/asahi/g17/port.rs diff --git a/drivers/gpu/drm/asahi/g17/context.rs b/drivers/gpu/drm/asahi/g17/context.rs index 1818fd8c6922d..564d761d37f97 100644 --- a/drivers/gpu/drm/asahi/g17/context.rs +++ b/drivers/gpu/drm/asahi/g17/context.rs @@ -214,6 +214,10 @@ impl Context { self.render_seen.load(Ordering::Acquire) || self.compute_binding_hint.load(Ordering::Relaxed) } + /// This logical queue has submitted render work, so its compute may have a render prefix. + pub(crate) fn render_seen(&self) -> bool { + self.render_seen.load(Ordering::Acquire) + } pub(crate) fn note_render(&self) { if !self.render_seen.load(Ordering::Relaxed) { self.render_seen.store(true, Ordering::Release); diff --git a/drivers/gpu/drm/asahi/g17/initdata.rs b/drivers/gpu/drm/asahi/g17/initdata.rs index b9a8f326d970d..6d0b2f9951956 100644 --- a/drivers/gpu/drm/asahi/g17/initdata.rs +++ b/drivers/gpu/drm/asahi/g17/initdata.rs @@ -129,14 +129,6 @@ impl WorkPublication<'_> { ); } } - - fn commit(self) -> u32 { - self.copy_record(); - fence(Ordering::SeqCst); - self.producer.store(self.next, Ordering::Relaxed); - fence(Ordering::SeqCst); - self.next - } } /// Session-long storage and mappings. Aliases are released before their backing objects. @@ -516,7 +508,7 @@ impl InitData { let engine = match record.data_master { 0 => DataMaster::Tiling, 1 => DataMaster::Fragment, - 2 => DataMaster::Compute, + // Compute publications reserve their slots through the port. _ => return Err(EINVAL), }; let channel = channels::work_ring_index(priority, engine).ok_or(EINVAL)?; @@ -569,16 +561,6 @@ impl InitData { Ok(values) } - /// Inner queue records must be visible before this outer publication. The returned - /// producer is the retained consumer witness for this slot. - pub(super) fn publish_work( - &self, - priority: u8, - record: &super::fw::channels::WorkSlot, - ) -> Result { - Ok(self.work_publication(priority, record)?.commit()) - } - /// Validate both destinations before exposing fragment, then tiling. The accounting token /// is committed after copying fragment; late tiling state is published before its slot. /// Deferred pairs retain both slot bodies without exposing either producer. @@ -677,6 +659,49 @@ impl InitData { Ok(()) } + /// Raw views of the memory publishers share without the device mutex: every compute work + /// ring, the QoS table and the recovery state word. They stay valid for this session. + pub(super) fn port_views( + &self, + mailbox: *const super::Mailbox, + ) -> Result { + use super::fw::{channels, queue::{DataMaster, Policy}}; + let ring = |priority: u8| -> Result { + let channel = channels::work_ring_index(priority, DataMaster::Compute).ok_or(EINVAL)?; + let base = self.state_offset(Role::Primary) + abi::work_state_offset(channel); + let state = self.object(self.cluster)?; + let word = |offset: usize| -> Result<*const AtomicU32> { + Ok(state.word(base + offset)?) + }; + Ok(super::port::RingView { + slots: self.object(self.bundle)?.pointer( + abi::work_ring_offset(channel), + channels::WORK_RING_SLOTS as usize + * core::mem::size_of::(), + )?, + consumers: [ + word(abi::ChannelState::CONSUMER)?, + word(abi::ChannelState::UNK_10)?, + ], + producer: word(abi::ChannelState::PRODUCER)?, + }) + }; + let mut compute = [ring(0)?; Policy::PRIORITIES as usize]; + for (priority, view) in compute.iter_mut().enumerate().skip(1) { + *view = ring(priority as u8)?; + } + Ok(super::port::Views { + compute, + qos: self + .qos()? + .pointer(0, core::mem::size_of::())?, + recovery: self + .object(self.cluster)? + .word(abi::private::STATUS_B + abi::PrimaryStatusB::RECOVERY_STATE)?, + mailbox, + }) + } + /// Snapshot the shared compute completion ring once per service transaction. The queue /// scanner latches exact identities; firmware retains sole ownership of these slots. /// Visits ring slots from `start`, wrapping once, until `record` reports diff --git a/drivers/gpu/drm/asahi/g17/kick.rs b/drivers/gpu/drm/asahi/g17/kick.rs index 87a90516d1b42..f3183bb005df6 100644 --- a/drivers/gpu/drm/asahi/g17/kick.rs +++ b/drivers/gpu/drm/asahi/g17/kick.rs @@ -87,7 +87,6 @@ struct Identity { pub(crate) struct QueueIds { entries: [Option; QID_COUNT], initial_render: [Option; 2], - publication: u64, leased: usize, /// First QID the host may use ([`crate::g17::fw::Variant::qid_floor`]). floor: usize, @@ -98,7 +97,6 @@ impl QueueIds { kernel::try_init!(Self { entries <- pin_init::init_array_from_fn(|_| None), initial_render: [None; 2], - publication: 0, leased: 0, // The reserve must keep render capacity above the floor. floor: if floor < FULL_RENDER_RESERVE { floor } else { return Err(EINVAL) }, @@ -239,13 +237,6 @@ impl QueueIds { Ok(id) } - /// Ordered under the device mutex; no atomic operation on the submission path. Compute and - /// render publications share it, so a watchdog can order work across both engines. - pub(crate) fn next_publication(&mut self) -> Result { - self.publication = self.publication.checked_add(1).ok_or(EOVERFLOW)?; - Ok(self.publication) - } - /// Call after preflighting the complete installation, before any address /// becomes firmware-visible. A failed or partial installation retains it. pub(crate) fn publish(&mut self, id: Id) -> Result { diff --git a/drivers/gpu/drm/asahi/g17/mod.rs b/drivers/gpu/drm/asahi/g17/mod.rs index a85f68829018e..edcb2ae737d63 100644 --- a/drivers/gpu/drm/asahi/g17/mod.rs +++ b/drivers/gpu/drm/asahi/g17/mod.rs @@ -38,6 +38,7 @@ mod job; mod capacity; mod kick; mod object; +mod port; pub(crate) mod power; mod preparation; mod qos; @@ -395,13 +396,48 @@ impl rtkit::Operations for RoleOps { } } +/// The RTKit of one coprocessor. The mailbox driver serializes its own register writes; this +/// lock only serializes the exclusive borrows the RTKit API takes, so that publishers holding +/// different locks can ring the same doorbell. +#[pin_data] +struct Mailbox { + #[pin] + lock: Mutex<()>, + rtkit: core::cell::UnsafeCell>, +} + +// SAFETY: Every access to the RTKit goes through `with`, which holds the lock. +unsafe impl Sync for Mailbox {} + +impl Mailbox { + fn new(rtkit: rtkit::RtKit) -> Result>> { + KBox::pin_init( + pin_init!(Self { + lock <- new_mutex!((), "g17::Mailbox::lock"), + rtkit: core::cell::UnsafeCell::new(rtkit), + }), + GFP_KERNEL, + ) + } + + fn with(&self, f: impl FnOnce(Pin<&mut rtkit::RtKit>) -> R) -> R { + let _guard = self.lock.lock(); + // SAFETY: The lock excludes every other borrow of the RTKit. + f(Pin::new(unsafe { &mut *self.rtkit.get() })) + } + + fn send(&self, endpoint: u8, message: u64) -> Result { + self.with(|rtkit| rtkit.send_message(endpoint, message)) + } +} + /// A running firmware coprocessor. /// /// Dropping it stops the CPU before RTKit and the crash log buffer go away. struct Coprocessor { cpu: regs::CpuControl, running: bool, - rtkit: rtkit::RtKit, + rtkit: Pin>, state: Arc, _crash_buffer: Option, } @@ -456,7 +492,7 @@ impl Coprocessor { Ok(Coprocessor { cpu: regs::CpuControl::new(pdev, index)?, running: false, - rtkit: Self::receiver(dev, index, &state)?, + rtkit: Mailbox::new(Self::receiver(dev, index, &state)?)?, state, _crash_buffer: crash_buffer, }) @@ -485,19 +521,20 @@ impl Coprocessor { fn boot(&mut self) -> Result { self.cpu.start()?; self.running = true; - Pin::new(&mut self.rtkit).boot() + self.rtkit.with(|rtkit| rtkit.boot()) } /// Starts the firmware endpoints and hands the firmware its initialization root. An /// acknowledgment message is awaited here; one in status A is the caller's to poll. fn init(&mut self, root_va: u64, ack: fw::variant::InitAck) -> Result { - let mut rtkit = Pin::new(&mut self.rtkit); - rtkit.as_mut().start_endpoint(EP_FIRMWARE)?; - rtkit.as_mut().start_endpoint(EP_DOORBELL)?; - rtkit.send_message( - EP_FIRMWARE, - MSG_INITDATA | (root_va & MSG_INITDATA_ADDR_MASK), - )?; + self.rtkit.with(|mut rtkit| { + rtkit.as_mut().start_endpoint(EP_FIRMWARE)?; + rtkit.as_mut().start_endpoint(EP_DOORBELL)?; + rtkit.send_message( + EP_FIRMWARE, + MSG_INITDATA | (root_va & MSG_INITDATA_ADDR_MASK), + ) + })?; match ack { fw::variant::InitAck::Message => self.state.wait_for_ack(), fw::variant::InitAck::Status => Ok(()), @@ -505,8 +542,8 @@ impl Coprocessor { } /// Sends a host notification to the firmware. - fn notify(&mut self, msg: u64) -> Result { - Pin::new(&mut self.rtkit).send_message(EP_DOORBELL, msg) + fn notify(&self, msg: u64) -> Result { + self.rtkit.send(EP_DOORBELL, msg) } /// Stops the CPU. The firmware keeps its state, but no longer runs. @@ -573,6 +610,10 @@ struct Firmware { secondary: Option, primary: Coprocessor, bridge: Bridge, + /// Publication state shared with publishers that do not hold the device mutex. + port: Arc, + /// Fast-path submissions already accounted by the idle check. + seen_submissions: u64, rings: channel::Rings, recovery: recovery::State, effort: power::Effort, @@ -647,12 +688,13 @@ impl Firmware { } fn apply_effort(&mut self, effort: u32) -> Result { - Self::apply_shared_effort(&self.init, &mut self.effort, effort) + Self::apply_shared_effort(&self.init, &mut self.effort, &self.port, effort) } fn apply_shared_effort( init: &initdata::InitData, state: &mut power::Effort, + port: &port::Port, effort: u32, ) -> Result { if state.applied() == effort && init.effort()? == effort { @@ -660,21 +702,35 @@ impl Firmware { } init.publish_effort(effort)?; state.did_apply(effort); + port.set_effort_full(effort == power::FULL_EFFORT); Ok(()) } /// Returns the delay in ns for a follow-up idle check. Queue owners report only submitted, - /// unretired work; quarantined work retaining mappings is excluded. + /// unretired work; quarantined work retaining mappings is excluded. Submissions published + /// without the device mutex count as activity, and a clear that races one is abandoned. fn idle_check(&mut self, now_ns: u64, in_flight: bool) -> Result> { - let effort = match self.effort.idle(now_ns, in_flight) { + let submissions = self.port.submissions(); + let submitted = submissions != self.seen_submissions; + self.seen_submissions = submissions; + if submitted { + self.effort.submit(); + } + let effort = match self.effort.idle(now_ns, in_flight || submitted) { power::IdleAction::None => None, power::IdleAction::Clear => { + self.port.set_effort_full(false); + if self.port.submissions() != submissions { + self.port.set_effort_full(true); + self.effort.submit(); + return Ok(Some(power::IDLE_HOLD_NS)); + } self.apply_effort(0)?; None } power::IdleAction::Recheck(delay) => Some(delay), }; - Ok(match (effort, self.idle_hold(now_ns, in_flight)) { + Ok(match (effort, self.idle_hold(now_ns, in_flight || submitted)) { (Some(a), Some(b)) => Some(a.min(b)), (a, b) => a.or(b), }) @@ -801,6 +857,8 @@ struct Shared { /// Counts `changed` notifications, so that a waiter which checked under /// the device mutex can tell whether it already missed one. changes: AtomicU64, + /// The port of the session in `state`; null before boot and once detachment begins. + port: core::sync::atomic::AtomicPtr, } impl_has_work! { @@ -900,6 +958,20 @@ impl Shared { self.changes.load(Ordering::Acquire) } + /// The session's port, valid while `lease` is held: shutdown and device loss join every + /// lease before they detach the session. + fn port<'a>(&self, lease: &'a preparation::Lease) -> Option<&'a port::Port> { + let _ = lease; + // SAFETY: The pointer is published before the session becomes reachable and cleared + // only after every lease has been joined, so it names a live port for 'a. + unsafe { self.port.load(Ordering::Acquire).as_ref() } + } + + /// Called after joining every preparation lease, before the session is detached. + fn forget_port(&self) { + self.port.store(core::ptr::null_mut(), Ordering::Release); + } + /// Queues the event worker. Callable from any context. fn queue_events(self: &Arc) { // Already queued if this fails, which is just as good. @@ -1081,6 +1153,7 @@ impl Gpu { state <- new_mutex!(None, "g17::Shared::state"), changed <- new_condvar!("g17::Shared::changed"), changes: AtomicU64::new(0), + port: core::sync::atomic::AtomicPtr::new(core::ptr::null_mut()), }), GFP_KERNEL, )?; @@ -1103,6 +1176,9 @@ impl Gpu { GFP_KERNEL, )?; let firmware = Self::boot(pdev, dev, cfg, regs, geometry, &gpu.shared)?; + gpu.shared + .port + .store(Arc::as_ptr(&firmware.port).cast_mut(), Ordering::Release); *gpu.shared.state.lock() = Some(firmware); gpu.shared.queue_events(); Ok(gpu) @@ -1214,6 +1290,8 @@ impl Gpu { secondary, primary, bridge, + port: port::Port::new()?, + seen_submissions: 0, rings: channel::Rings::new(), recovery: recovery::State::new(), effort: power::Effort::new(), @@ -1260,6 +1338,8 @@ impl Gpu { if let Some(root_entries) = root_entries { session.uat.confirm_firmware_root_entries(root_entries)?; } + let mailbox: *const Mailbox = &*session.primary.rtkit; + session.port.attach(session.init.port_views(mailbox)?)?; session.uat.mirror_secondary_root()?; if cfg.fabric_bridge { session.bridge.regs.enable_g17_gating()?; diff --git a/drivers/gpu/drm/asahi/g17/port.rs b/drivers/gpu/drm/asahi/g17/port.rs new file mode 100644 index 0000000000000..63eab753632be --- /dev/null +++ b/drivers/gpu/drm/asahi/g17/port.rs @@ -0,0 +1,267 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT + +//! Device-wide publication state that physical queues share without the device mutex. +//! +//! A firmware session owns its port, and the views it holds name that session's +//! firmware-shared memory. Code that does not hold the device mutex reaches the port only +//! through a preparation lease ([`super::Shared::port`]): shutdown and device loss join every +//! lease before they detach the session. Each piece below is atomic or has its own lock. +//! +//! Lock order: device mutex -> compute queue cell -> the locks of this port. + +use super::{ + fw::{ + channels::{WorkSlot, WORK_RING_SLOTS}, + queue::{DataMaster, Policy}, + }, + qos, +}; +use core::sync::atomic::{fence, AtomicBool, AtomicU32, AtomicU64, Ordering}; +use kernel::{ + new_spinlock, + prelude::*, + sync::{Arc, SetOnce, SpinLock}, +}; + +const PRIORITIES: usize = Policy::PRIORITIES as usize; + +/// One priority class's compute work ring: its slot array and the cursors of its two firmware +/// consumers and of the host producer. +#[derive(Clone, Copy)] +pub(super) struct RingView { + pub(super) slots: *mut u8, + pub(super) consumers: [*const AtomicU32; 2], + pub(super) producer: *const AtomicU32, +} + +impl RingView { + fn cursors(&self) -> Result<[u32; 3]> { + // SAFETY: The views name 32-bit words of the session's retained firmware objects, which + // the port's owner keeps alive. Other agents only use single 32-bit accesses. + let values = unsafe { + [ + (*self.consumers[0]).load(Ordering::Relaxed), + (*self.consumers[1]).load(Ordering::Relaxed), + (*self.producer).load(Ordering::Relaxed), + ] + }; + if values.iter().any(|&value| value >= WORK_RING_SLOTS) { + return Err(EINVAL); + } + Ok(values) + } + + /// Slots the host may still fill before reaching either consumer. + fn free(&self) -> Result { + let [first, second, producer] = self.cursors()?; + let room = |consumer: u32| (consumer + WORK_RING_SLOTS - producer - 1) % WORK_RING_SLOTS; + Ok(room(first).min(room(second))) + } +} + +/// Raw views of the session's firmware-shared memory, fixed once the firmware is built. +pub(super) struct Views { + pub(super) compute: [RingView; PRIORITIES], + pub(super) qos: *mut u8, + pub(super) recovery: *const AtomicU32, + pub(super) mailbox: *const super::Mailbox, +} + +// SAFETY: The views only name memory and a mailbox owned by the firmware session, which outlives +// every user of the port. Accesses are atomic words or serialized by the port's locks. +unsafe impl Send for Views {} +// SAFETY: As above. +unsafe impl Sync for Views {} + +#[pin_data] +pub(super) struct Port { + views: SetOnce, + /// Compute work-ring slots reserved by publishers that have not exposed them yet. + #[pin] + rings: SpinLock<[u32; PRIORITIES]>, + #[pin] + qos: SpinLock, + /// The accepted recovery generation, mirrored for publishers without the device mutex. + generation: AtomicU64, + /// Device publication order shared by compute and render commands. + publications: AtomicU64, + /// Full firmware effort is applied; cleared before an idle clear rechecks `submissions`. + effort_full: AtomicBool, + submissions: AtomicU64, +} + +/// A compute work-ring slot reserved before any command becomes visible. Dropping an unused +/// claim returns the slot. +pub(super) struct OuterClaim<'a> { + port: &'a Port, + priority: usize, + armed: bool, +} + +impl Drop for OuterClaim<'_> { + fn drop(&mut self) { + if self.armed { + let mut reserved = self.port.rings.lock(); + reserved[self.priority] = reserved[self.priority].saturating_sub(1); + } + } +} + +impl Port { + pub(super) fn new() -> Result> { + Arc::pin_init( + pin_init!(Self { + views: SetOnce::new(), + rings <- new_spinlock!([0; PRIORITIES], "g17::Port::rings"), + qos <- new_spinlock!(qos::Accounting::new(), "g17::Port::qos"), + generation: AtomicU64::new(0), + publications: AtomicU64::new(0), + effort_full: AtomicBool::new(false), + submissions: AtomicU64::new(0), + }), + GFP_KERNEL, + ) + } + + /// Called once while the session is built, before any publisher can see the port. + pub(super) fn attach(&self, views: Views) -> Result { + if self.views.populate(views) { + Ok(()) + } else { + Err(EBUSY) + } + } + + fn views(&self) -> Result<&Views> { + self.views.as_ref().ok_or(ENODEV) + } + + fn ring(&self, priority: u8) -> Result<(usize, RingView)> { + let index = usize::from(priority); + Ok((index, *self.views()?.compute.get(index).ok_or(EINVAL)?)) + } + + /// Reserves one slot of the compute work ring of `priority`. EAGAIN while firmware has not + /// consumed enough slots; only consumption, never a waiting publisher, frees one. + pub(super) fn reserve_compute(&self, priority: u8) -> Result> { + let (index, ring) = self.ring(priority)?; + let mut reserved = self.rings.lock(); + if ring.free()? <= reserved[index] { + return Err(EAGAIN); + } + reserved[index] += 1; + Ok(OuterClaim { + port: self, + priority: index, + armed: true, + }) + } + + /// Whether a compute publication at `priority` can reserve a slot now. + pub(super) fn compute_ready(&self, priority: u8) -> Result { + let (index, ring) = self.ring(priority)?; + let reserved = self.rings.lock(); + Ok(ring.free()? > reserved[index]) + } + + /// Exposes `record` in its reserved slot. Inner queue records must be visible first. Returns + /// the new producer, the retained consumer witness for this slot. + pub(super) fn publish_compute(&self, mut claim: OuterClaim<'_>, record: &WorkSlot) -> Result { + if record.data_master != DataMaster::Compute as u32 || !core::ptr::eq(claim.port, self) { + return Err(EINVAL); + } + let ring = self.views()?.compute[claim.priority]; + let mut reserved = self.rings.lock(); + let [first, second, producer] = ring.cursors()?; + let next = (producer + 1) % WORK_RING_SLOTS; + if next == first || next == second { + return Err(EIO); + } + // SAFETY: The ring view spans WORK_RING_SLOTS records, checked when the session built + // it; `producer` is in range. The reservation proves firmware does not own this slot and + // the ring lock excludes every other host writer. The record is a padding-free ABI value. + unsafe { + core::ptr::copy_nonoverlapping( + core::ptr::from_ref(record).cast::(), + ring.slots.add(producer as usize * core::mem::size_of::()), + core::mem::size_of::(), + ); + } + fence(Ordering::SeqCst); + // SAFETY: As for `RingView::cursors`. + unsafe { (*ring.producer).store(next, Ordering::Relaxed) }; + fence(Ordering::SeqCst); + reserved[claim.priority] = reserved[claim.priority].saturating_sub(1); + claim.armed = false; + Ok(next) + } + + fn qos_view(&self) -> Result> { + // SAFETY: The session retains the QoS table for at least as long as this port. + unsafe { qos::View::from_raw(self.views()?.qos) } + } + + pub(super) fn qos_publish( + &self, + owner: qos::Owner, + scheduler: u64, + policy: Policy, + ) -> Result { + let view = self.qos_view()?; + self.qos.lock().publish(&view, owner, scheduler, policy, qos::clock) + } + + pub(super) fn qos_cancel(&self, publication: qos::Publication) -> Result { + let view = self.qos_view()?; + self.qos.lock().cancel(&view, publication) + } + + pub(super) fn qos_complete(&self, owners: &[qos::Owner]) -> Result { + let view = self.qos_view()?; + self.qos.lock().complete(&view, owners) + } + + /// Rings the primary coprocessor's doorbell. + pub(super) fn notify(&self, message: u64) -> Result { + // SAFETY: The session's primary coprocessor owns the mailbox and outlives the port. + let mailbox = unsafe { &*self.views()?.mailbox }; + mailbox.send(super::EP_DOORBELL, message) + } + + pub(super) fn recovery_state(&self) -> Result { + let word = self.views()?.recovery; + fence(Ordering::Acquire); + // SAFETY: As for `RingView::cursors`. + Ok(unsafe { (*word).load(Ordering::Relaxed) }) + } + + pub(super) fn generation(&self) -> u64 { + self.generation.load(Ordering::Acquire) + } + + /// Called with the device mutex held after the accepted generation changed, before + /// publication reopens. + pub(super) fn set_generation(&self, generation: u64) { + self.generation.store(generation, Ordering::Release); + } + + pub(super) fn next_publication(&self) -> u64 { + self.publications.fetch_add(1, Ordering::Relaxed) + 1 + } + + /// A publisher without the device mutex counts itself before it checks that full effort is + /// applied. The idle clear withdraws the flag before it rechecks the count, so either the + /// clear sees this submission or this submission sees the flag withdrawn. + pub(super) fn note_submission(&self) -> bool { + self.submissions.fetch_add(1, Ordering::SeqCst); + self.effort_full.load(Ordering::SeqCst) + } + + pub(super) fn submissions(&self) -> u64 { + self.submissions.load(Ordering::SeqCst) + } + + pub(super) fn set_effort_full(&self, full: bool) { + self.effort_full.store(full, Ordering::SeqCst); + } +} diff --git a/drivers/gpu/drm/asahi/g17/power.rs b/drivers/gpu/drm/asahi/g17/power.rs index 7fef15084c50a..b0e21e70bbe18 100644 --- a/drivers/gpu/drm/asahi/g17/power.rs +++ b/drivers/gpu/drm/asahi/g17/power.rs @@ -6,7 +6,7 @@ use core::sync::atomic::AtomicU32; pub(super) const FULL_EFFORT: u32 = 0x10000; -const IDLE_HOLD_NS: u64 = 16_000_000; +pub(super) const IDLE_HOLD_NS: u64 = 16_000_000; /// The firmware's idle power-off delay as measured on T6040/25G83: the cores power off 2 ms after /// the last job. Not measured on other firmware; at or below it the hold stays off. diff --git a/drivers/gpu/drm/asahi/g17/qos.rs b/drivers/gpu/drm/asahi/g17/qos.rs index 9a100db4cffdf..4f6913ad5ee95 100644 --- a/drivers/gpu/drm/asahi/g17/qos.rs +++ b/drivers/gpu/drm/asahi/g17/qos.rs @@ -2,17 +2,14 @@ //! Shared QoS publication and exact-completion accounting for every engine. //! -//! The device publication lock serializes this host state. Publishing identity +//! The port's QoS lock serializes this host state. Publishing identity //! precedes submitted counters; retiring a final owner clears only its two row //! bytes. Cancellation balances host references without rewinding shared counts. -use super::{ - fw::{qos as fw, queue::Policy}, - object::KernelObject, -}; +use super::fw::{qos as fw, queue::Policy}; use crate::hw::t8140::qos as cfg; use core::{ - mem::{offset_of, size_of}, + mem::offset_of, sync::atomic::{fence, AtomicU32, AtomicU64, AtomicU8, Ordering}, }; use kernel::prelude::*; @@ -30,15 +27,17 @@ pub(crate) struct View<'a> { times_and_counts: &'a [AtomicU64; 7], } impl<'a> View<'a> { - pub(crate) fn new(object: &'a KernelObject) -> Result { - let ptr = object.pointer(0, size_of::())?; + /// # Safety + /// + /// `ptr` must span a complete QoS table that stays mapped for `'a`. + pub(crate) unsafe fn from_raw(ptr: *mut u8) -> Result { if ptr as usize & 7 != 0 { return Err(EINVAL); } // SAFETY: pointer spans the complete layout, whose asserted offsets // and size bound every array below. Integer atomics have their integer - // representation and alignment. The retained object borrow keeps every - // shared reference alive; accesses use each field's fixed width. + // representation and alignment. The caller keeps every shared + // reference alive; accesses use each field's fixed width. Ok(unsafe { Self { rows: &*ptr.add(offset_of!(fw::Qos, rows)).cast(), diff --git a/drivers/gpu/drm/asahi/g17/queue/compute.rs b/drivers/gpu/drm/asahi/g17/queue/compute.rs index 625f65e6d6772..a21d1c915d0c8 100644 --- a/drivers/gpu/drm/asahi/g17/queue/compute.rs +++ b/drivers/gpu/drm/asahi/g17/queue/compute.rs @@ -2351,6 +2351,11 @@ impl Queue { Ok(self.reusable()) } + /// The queue's first command has been published: its identity and configuration exist. + pub(crate) fn installed(&self) -> bool { + self.installed + } + /// An open client's idle channel that [`Self::share_idle`] can hand over. pub(crate) fn shareable(&self) -> bool { self.installed && self.owner.is_some() && self.retirement_ready diff --git a/drivers/gpu/drm/asahi/g17/queue/compute/retirement.rs b/drivers/gpu/drm/asahi/g17/queue/compute/retirement.rs index 655e2921f2582..b268c1c40de00 100644 --- a/drivers/gpu/drm/asahi/g17/queue/compute/retirement.rs +++ b/drivers/gpu/drm/asahi/g17/queue/compute/retirement.rs @@ -262,16 +262,17 @@ impl Batch { request.observed = Some(Observation { timestamps }); Ok(()) } + /// A command published after this batch staged its queue's requests was not part of the + /// ring snapshot: it stays unobserved until a later batch. pub(crate) fn take(&mut self, ticket: &Ticket) -> Result> { let &(start, count) = self.slots.get(ticket.qid as usize).ok_or(EINVAL)?; - let request = self + Ok(self .requests .get_mut(start..start + count) .ok_or(EIO)? .iter_mut() .find(|r| r.descriptor == ticket.descriptor && r.kick == ticket.kick) - .ok_or(EIO)?; - Ok(request.observed.take()) + .and_then(|request| request.observed.take())) } } diff --git a/drivers/gpu/drm/asahi/g17/runtime.rs b/drivers/gpu/drm/asahi/g17/runtime.rs index 745cd62cae595..237c92db48ce2 100644 --- a/drivers/gpu/drm/asahi/g17/runtime.rs +++ b/drivers/gpu/drm/asahi/g17/runtime.rs @@ -69,13 +69,103 @@ pub(super) struct ComputeReservation { struct ComputeEntry { reservation: ComputeReservation, - queue: Option>, + queue: ComputeSlot, +} + +/// A physical compute queue behind its own lock. Publication into a bound queue takes only +/// this lock; device-mutex holders take it to observe or change the queue (device mutex -> +/// cell). A logical queue may keep the cell after the registry gave the queue up: the cell is +/// then empty, so the cached reference never extends any firmware-visible lifetime. +#[pin_data] +pub(super) struct ComputeCell { + #[pin] + queue: kernel::sync::Mutex>>, +} + +impl ComputeCell { + fn new() -> Result> { + Arc::pin_init( + pin_init!(Self { + queue <- kernel::new_mutex!(None, "g17::ComputeCell::queue"), + }), + GFP_KERNEL, + ) + } + + /// The installed queue, locked; None once the registry gave it up. + pub(super) fn lock(&self) -> Option> { + QueueGuard::new(self.queue.lock()) + } +} + +/// A locked installed queue. +pub(super) struct QueueGuard<'a> { + queue: core::ptr::NonNull, + _guard: kernel::sync::MutexGuard<'a, Option>>, +} + +impl<'a> QueueGuard<'a> { + fn new(mut guard: kernel::sync::MutexGuard<'a, Option>>) -> Option { + let queue = core::ptr::NonNull::from((*guard).as_deref_mut()?); + Some(Self { queue, _guard: guard }) + } +} + +impl core::ops::Deref for QueueGuard<'_> { + type Target = compute::Queue; + fn deref(&self) -> &compute::Queue { + // SAFETY: The boxed queue cannot be taken or dropped while the guard holds its lock. + unsafe { self.queue.as_ref() } + } +} + +impl core::ops::DerefMut for QueueGuard<'_> { + fn deref_mut(&mut self) -> &mut compute::Queue { + // SAFETY: As for `deref`; the lock makes this the only reference. + unsafe { self.queue.as_mut() } + } +} + +/// The registry's handle on one compute cell. Dropping it drops the queue in the cell, so a +/// cached cell never keeps a removed queue (or its QID's aliases) alive. +struct ComputeSlot(Arc); + +impl ComputeSlot { + fn new() -> Result { + Ok(Self(ComputeCell::new()?)) + } + fn as_deref(&self) -> Option> { + self.0.lock() + } + fn as_deref_mut(&mut self) -> Option> { + self.0.lock() + } + fn is_some(&self) -> bool { + self.0.queue.lock().is_some() + } + fn is_none(&self) -> bool { + !self.is_some() + } + /// Gives the queue up to the caller, which drops it outside the cell lock. + fn take(&mut self) -> Option> { + self.0.queue.lock().take() + } + fn put(&mut self, queue: Option>) { + let old = core::mem::replace(&mut *self.0.queue.lock(), queue); + drop(old); + } +} + +impl Drop for ComputeSlot { + fn drop(&mut self) { + let queue = self.0.queue.lock().take(); + drop(queue); + } } pub(super) struct Registry { pub(super) qids: kick::QueueIds, pub(super) buffers: BufferIds, - pub(super) accounting: qos::Accounting, pub(super) metrics: Arc, pub(super) qos_ids: Arc, pub(super) admission: Arc, @@ -106,7 +196,6 @@ impl Registry { qids.reserve_initial_render() }), buffers <- BufferIds::new(), - accounting: qos::Accounting::new(), metrics: Arc::new(MetricsIds::new(), GFP_KERNEL)?, qos_ids: QosIds::new()?, admission: admission::Pool::new(kick::queue_limit(variant.qid_floor))?, @@ -129,7 +218,7 @@ impl Registry { }) } - pub(super) fn compute(&self, owner: u64) -> Option<&compute::Queue> { + pub(super) fn compute(&self, owner: u64) -> Option> { self.compute .iter() .flatten() @@ -137,12 +226,16 @@ impl Registry { .find(|queue| queue.owner() == Some(owner)) } - pub(super) fn compute_mut(&mut self, owner: u64) -> Option<&mut compute::Queue> { - self.compute - .iter_mut() - .flatten() - .filter_map(|entry| entry.queue.as_deref_mut()) - .find(|queue| queue.owner() == Some(owner)) + pub(super) fn compute_mut(&mut self, owner: u64) -> Option> { + self.compute(owner) + } + + /// The cell of `owner`'s bound queue, which its publisher may lock without the device mutex. + pub(super) fn compute_cell(&self, owner: u64) -> Option> { + self.compute.iter().flatten().find_map(|entry| { + entry.queue.as_deref()?.owner().filter(|bound| *bound == owner)?; + Some(entry.queue.0.clone()) + }) } /// An installed idle queue keeps its original QID and backing. Mapping a @@ -175,18 +268,20 @@ impl Registry { .filter_map(|entry| entry.queue.as_deref()) .filter(|queue| queue.shareable()) .filter(|queue| !queue.context().is_some_and(|bound| teardown.releasing(bound))) - .min_by_key(|queue| (!queue.pool_fits(required), queue.last_published())) - .map(|queue| queue.qid()); + .map(|queue| ((!queue.pool_fits(required), queue.last_published()), queue.qid())) + .min_by_key(|(key, _)| *key) + .map(|(_, qid)| qid); let Some(qid) = idle else { return Err(ENOSPC); }; admission.check()?; - let queue = self + let shared = self .compute .iter_mut() .flatten() .filter_map(|entry| entry.queue.as_deref_mut()) .find(|queue| queue.qid() == qid) - .ok_or(EIO)?; - if !queue.share_idle()? { + .ok_or(EIO)? + .share_idle()?; + if !shared { return Err(ENOSPC); } Ok(self.bind_reusable(owner, context, required, admission)?.unwrap_or(Rebind::Fresh)) @@ -197,7 +292,7 @@ impl Registry { fn bind_reusable(&mut self, owner: u64, context: &Arc, required: usize, admission: &Admission<'_>) -> Result> { let mut vacant = None; let teardown = &self.teardown; - for queue in self + for mut queue in self .compute .iter_mut() .flatten() @@ -230,7 +325,7 @@ impl Registry { pages: crate::g17::freelist::Pages, ring: compute::DescriptorRing, ) -> Result<(u16, u64)> { - let queue = self + let mut queue = self .compute .iter_mut() .flatten() @@ -242,7 +337,7 @@ impl Registry { pub(super) fn bind_rearmed(&mut self, qid: u8, owner: u64, context: &Arc) -> Result { let teardown = &self.teardown; - let queue = self + let mut queue = self .compute .iter_mut() .flatten() @@ -310,7 +405,7 @@ impl Registry { let excess = candidates.len().saturating_sub(cap).max(forced); let mut published = 0; for (_, qid) in candidates.iter().take(excess.min(4)) { - let queue = self + let mut queue = self .compute .iter_mut() .flatten() @@ -340,7 +435,7 @@ impl Registry { let consumer = init.control_consumer()?.load(Ordering::Relaxed); let producer = init.control_producer()?.load(Ordering::Relaxed); let mut freed = 0; - for queue in self + for mut queue in self .compute .iter_mut() .flatten() @@ -371,7 +466,7 @@ impl Registry { || self.compute.iter().flatten().any(|entry| { entry .queue - .as_ref() + .as_deref() .map_or(entry.reservation.owner == owner, |queue| { queue.owner() == Some(owner) }) @@ -394,7 +489,7 @@ impl Registry { let reservation = ComputeReservation { slot, id, owner }; self.compute[slot] = Some(ComputeEntry { reservation, - queue: None, + queue: ComputeSlot::new()?, }); Ok(reservation) } @@ -426,7 +521,7 @@ impl Registry { return Err(EIO); } let entry = self.reserved(reservation)?; - entry.queue = queue.take(); + entry.queue.put(queue.take()); Ok(()) } @@ -441,13 +536,14 @@ impl Registry { let Some(slot) = self.compute.iter().position(|entry| { entry .as_ref() - .and_then(|entry| entry.queue.as_ref()) + .and_then(|entry| entry.queue.as_deref()) .is_some_and(|queue| queue.owner() == Some(owner)) }) else { return Ok(()); }; let entry = self.compute[slot].as_mut().ok_or(EIO)?; - if entry.queue.as_mut().ok_or(EIO)?.release_owner()? { + let unpublished = entry.queue.as_deref_mut().ok_or(EIO)?.release_owner()?; + if unpublished { self.qids.cancel_unpublished(entry.reservation.id)?; self.compute[slot] = None; } @@ -495,6 +591,7 @@ mod uninstalled_cleanup_tests { /// An immutable logical owner shared by its two scheduler entities. Installed /// physical graphs retain the context independently of this frontend lifetime. +#[pin_data] pub(super) struct Backend { shared: Arc, pub(super) context: Arc, @@ -502,6 +599,11 @@ pub(super) struct Backend { backlog: core::sync::atomic::AtomicU32, compute_backlog: core::sync::atomic::AtomicU32, closed: core::sync::atomic::AtomicBool, + /// The cell of the compute queue this owner last published to under the device mutex, + /// with the shared compute objects mapped into its VM. Direct publication revalidates the + /// binding under the cell lock; a stale cell only sends the packet back to that path. + #[pin] + direct: kernel::sync::Mutex>>, } impl Backend { @@ -580,15 +682,16 @@ impl Backend { } drop(state); Ok(( - Arc::new( - Self { + Arc::pin_init( + pin_init!(Self { shared: shared.clone(), context, owner, backlog: core::sync::atomic::AtomicU32::new(0), compute_backlog: core::sync::atomic::AtomicU32::new(0), closed: core::sync::atomic::AtomicBool::new(false), - }, + direct <- kernel::new_mutex!(None, "g17::Backend::direct"), + }), GFP_KERNEL, )?, slot, @@ -762,7 +865,7 @@ impl Backend { if !lease.is_current() { return Err(EAGAIN); } if self.shared.crashed.load(Ordering::Acquire) { return Err(EIO); } firmware.scan_compute(None, &mut deferred)?; - let queue = firmware.queues.compute_mut(self.owner).ok_or(EAGAIN)?; + let mut queue = firmware.queues.compute_mut(self.owner).ok_or(EAGAIN)?; if queue.pool_fits(required) { return Ok(None); } if growth.is_none() { let notify = queue.begin_growth(&firmware.init, required)?; @@ -793,8 +896,9 @@ impl Backend { if let Some(firmware) = state.as_deref_mut() { for index in 0..firmware.queues.compute.len() { let Some(entry) = firmware.queues.compute[index].as_mut() else { continue; }; - let Some(queue) = entry.queue.as_deref_mut() else { continue; }; - if queue.abort_growth(token)? == Some(true) { + let Some(aborted) = entry.queue.as_deref_mut() + .map(|mut queue| queue.abort_growth(token)).transpose()? else { continue; }; + if aborted == Some(true) { // Keep the empty entry and QID reserved until all // graph aliases have been destroyed outside the lock. retired = entry.queue.take().map(|queue| (entry.reservation, queue)); @@ -1156,15 +1260,15 @@ impl DeferredBatch { struct ComputeHost<'a> { init: &'a super::initdata::InitData, - primary: &'a mut super::Coprocessor, + port: &'a super::port::Port, + outer: Option>, effort: &'a mut super::power::Effort, qids: &'a mut kick::QueueIds, - accounting: &'a mut qos::Accounting, generation: u64, } impl compute::Host for ComputeHost<'_> { fn next_compute_publication(&mut self) -> Result { - self.qids.next_publication() + Ok(self.port.next_publication()) } fn epoch(&self) -> Result<(u64, u32)> { Ok(( @@ -1181,30 +1285,66 @@ impl compute::Host for ComputeHost<'_> { scheduler: u64, policy: super::fw::queue::Policy, ) -> Result { - self.accounting.publish( - &qos::View::new(self.init.qos()?)?, - owner, - scheduler, - policy, - qos::clock, - ) + self.port.qos_publish(owner, scheduler, policy) } fn qos_cancel(&mut self, publication: qos::Publication) -> Result { - self.accounting - .cancel(&qos::View::new(self.init.qos()?)?, publication) + self.port.qos_cancel(publication) } fn publish_qid(&mut self, id: kick::Id) -> Result { self.qids.publish(id) } - fn publish_outer(&mut self, priority: u8, slot: &super::fw::channels::WorkSlot) -> Result { - self.init.publish_work(priority, slot).map(|_| ()) + fn publish_outer(&mut self, _priority: u8, slot: &super::fw::channels::WorkSlot) -> Result { + self.port.publish_compute(self.outer.take().ok_or(EIO)?, slot).map(|_| ()) } fn notify(&mut self, message: u64) -> Result { - self.primary.notify(message) + self.port.notify(message) } fn note_submission(&mut self) -> Result { self.effort.submit(); - super::Firmware::apply_shared_effort(self.init, self.effort, super::power::FULL_EFFORT) + super::Firmware::apply_shared_effort(self.init, self.effort, self.port, super::power::FULL_EFFORT) + } +} + +/// Publication into a bound, installed queue through the port alone. The caller checked the +/// recovery state, full effort and the queue's binding; first installation never comes here. +struct DirectHost<'a> { + port: &'a super::port::Port, + outer: Option>, + generation: u64, +} +impl compute::Host for DirectHost<'_> { + fn next_compute_publication(&mut self) -> Result { + Ok(self.port.next_publication()) + } + fn epoch(&self) -> Result<(u64, u32)> { + Ok((self.generation, self.port.recovery_state()?)) + } + fn prepare_compute_shared(&mut self) -> Result { + Err(EAGAIN) + } + fn qos_publish( + &mut self, + owner: qos::Owner, + scheduler: u64, + policy: super::fw::queue::Policy, + ) -> Result { + self.port.qos_publish(owner, scheduler, policy) + } + fn qos_cancel(&mut self, publication: qos::Publication) -> Result { + self.port.qos_cancel(publication) + } + fn publish_qid(&mut self, _id: kick::Id) -> Result { + // An installed queue published its identity with its first command. + Ok(()) + } + fn publish_outer(&mut self, _priority: u8, slot: &super::fw::channels::WorkSlot) -> Result { + self.port.publish_compute(self.outer.take().ok_or(EIO)?, slot).map(|_| ()) + } + fn notify(&mut self, message: u64) -> Result { + self.port.notify(message) + } + fn note_submission(&mut self) -> Result { + Ok(()) } } @@ -1238,7 +1378,7 @@ impl super::Firmware { if source.submission.source_status()? { continue; } let found = self.queues.compute.iter_mut().flatten() .filter_map(|entry| entry.queue.as_deref_mut()) - .find_map(|queue| match queue.source_dependency(source, packet.completion.status(), + .find_map(|mut queue| match queue.source_dependency(source, packet.completion.status(), self.recovery.generation()) { Ok(None) => None, result => Some(result), @@ -1284,15 +1424,10 @@ impl super::Firmware { return Err(EAGAIN); } // No descriptor, accounting or inner producer changes precede this - // capacity check. The same device lock covers queue.publish below. - if !self - .init - .work_ready(packet.context.policy().priority(), DataMaster::Compute)? - { - return Err(EAGAIN); - } + // reservation; dropping it unused returns the work-ring slot. + let outer = self.port.reserve_compute(packet.context.policy().priority())?; let queues = &mut self.queues; - let queue = queues + let mut queue = queues .compute .iter_mut() .flatten() @@ -1310,10 +1445,10 @@ impl super::Firmware { } let mut host = ComputeHost { init: &self.init, - primary: &mut self.primary, + port: &self.port, + outer: Some(outer), effort: &mut self.effort, qids: &mut queues.qids, - accounting: &mut queues.accounting, generation: self.recovery.generation(), }; if native.is_empty() { @@ -1341,10 +1476,11 @@ impl Registry { fn release_compute_witness( &mut self, init: &super::initdata::InitData, + port: &super::port::Port, owner: u64, deferred: &mut DeferredBatch, ) -> Result { - let Some(queue) = self + let Some(mut queue) = self .compute .iter_mut() .flatten() @@ -1356,10 +1492,9 @@ impl Registry { if !queue.spared_pending() { return Ok(false); } - let view = qos::View::new(init.qos()?)?; queue.release_witnessed( super::recovery::Memory::recovery_state(init)?, - |owner| self.accounting.complete(&view, &[owner]), + |owner| port.qos_complete(&[owner]), &mut |event| deferred.push(event), )?; Ok(queue.spared_pending()) @@ -1371,7 +1506,7 @@ impl Registry { error: Error, deferred: &mut DeferredBatch, ) -> Result { - for queue in self + for mut queue in self .compute .iter_mut() .flatten() @@ -1418,8 +1553,8 @@ impl super::Firmware { }, )?; } - let view = qos::View::new(self.init.qos()?)?; - for queue in registry + let port = &self.port; + for mut queue in registry .compute .iter_mut() .flatten() @@ -1430,7 +1565,7 @@ impl super::Firmware { masks, generation, state, - |owner| registry.accounting.complete(&view, &[owner]), + |owner| port.qos_complete(&[owner]), &mut |event| deferred.push(event), )?; } @@ -1440,16 +1575,16 @@ impl super::Firmware { let terminal = self.queues.compute[index] .as_mut() .and_then(|entry| entry.queue.as_deref_mut()) - .and_then(compute::Queue::take_terminal); + .and_then(|mut queue| queue.take_terminal()); if let Some((status, error)) = terminal { status.record(error); self.settle_render_vm(&status, error, deferred)?; self.queues.fail_compute_vm(&status, error, deferred)?; } } - let view = qos::View::new(self.init.qos()?)?; + let port = &self.port; let registry = &mut self.queues; - for queue in registry + for mut queue in registry .compute .iter_mut() .flatten() @@ -1457,7 +1592,7 @@ impl super::Firmware { { queue.release_witnessed( state, - |owner| registry.accounting.complete(&view, &[owner]), + |owner| port.qos_complete(&[owner]), &mut |event| deferred.push(event), )?; } @@ -1480,7 +1615,7 @@ impl Backend { firmware.replay_compute(&mut deferred)?; firmware .queues - .release_compute_witness(&firmware.init, self.owner, &mut deferred) + .release_compute_witness(&firmware.init, &firmware.port, self.owner, &mut deferred) })(); deferred.finish(); let pending = pending?; @@ -1500,6 +1635,78 @@ impl Backend { Err(EBUSY) } + /// Steady-state publication into this owner's bound channel without the device mutex: only + /// the channel's own lock and the port's small locks are taken, and nothing device-wide is + /// scanned. None leaves the packet untouched for the device-mutex path, which owns binding, + /// first installation, input sources, render prefixes, replay and recovery. + fn compute_direct( + &self, + packet: &Arc, + required: usize, + deferred: &mut DeferredBatch, + ) -> Option { + if packet.completion.sources().is_some() + || self.context.render_seen() + || packet.cancel_requested() + || packet.completion.status().get() != 0 + || self.admission_open().is_err() + { + return None; + } + let cell = self.direct.lock().clone()?; + let lease = self.shared.preparations.try_enter().ok()?; + let port = self.shared.port(&lease)?; + if self.shared.crashed.load(Ordering::Acquire) || port.recovery_state().ok()? != 0 { + return None; + } + // Full effort is applied by the device-mutex path when it is not already. + if !port.note_submission() { + return None; + } + match packet.completion.work_state().ok()?.acquire() { + Ok(_) => {} + Err(error) if error == EBUSY => return None, + Err(error) => return Some(Err(error)), + } + let mut queue = cell.lock()?; + if !queue.installed() + || queue.spared_pending() + || !queue.pool_fits(required) + || queue.pending_switch_from().is_some() + || !matches!(queue.matches(self.owner, queue.qid(), &self.context), Ok(true)) + || !lease.is_current() + { + return None; + } + let outer = port.reserve_compute(packet.context.policy().priority()).ok()?; + let mut host = DirectHost { + port, + outer: Some(outer), + generation: port.generation(), + }; + match queue.publish(packet.clone(), &[], &mut host, &mut |event| deferred.push(event)) { + Err(error) if (error == EAGAIN || error == EBUSY) && !packet.is_published() => None, + result => Some(result), + } + } + + /// Called with the device mutex held after a successful publication: remembers the bound + /// channel for direct publication. + fn remember_direct(&self, registry: &Registry) { + let cell = registry.compute_cell(self.owner); + let mut direct = self.direct.lock(); + let current = match (&*direct, &cell) { + (Some(old), Some(new)) => Arc::ptr_eq(old, new), + (None, None) => true, + _ => false, + }; + if !current { + let old = core::mem::replace(&mut *direct, cell); + drop(direct); + drop(old); + } + } + /// Publishes a dependency-free compute command from its submit ioctl when /// its queue is already bound and nothing has to wait, as renders do. Any /// wait leaves the command, unpublished, to its scheduler. @@ -1523,7 +1730,10 @@ impl Backend { return true; } }; - let result = self.compute_inline(packet, &mut deferred); + let result = match self.compute_direct_packet(packet, &mut deferred) { + Some(result) => result, + None => self.compute_inline(packet, &mut deferred), + }; if matches!(result, Err(error) if error == render::FALLBACK) && !packet.is_published() { deferred.finish(); packet.abandon_publication(); @@ -1536,6 +1746,21 @@ impl Backend { true } + fn compute_direct_packet( + &self, + packet: &Arc, + deferred: &mut DeferredBatch, + ) -> Option { + let super::command::Validated::Compute { scratch, .. } = &packet.command else { + return Some(Err(EINVAL)); + }; + let required = match super::fw::compute_pool::required_blocks(self.shared.active_cores, scratch.pages()) { + Ok(required) => required, + Err(error) => return Some(Err(error)), + }; + self.compute_direct(packet, required, deferred) + } + /// compute_packet's publication section without its waits: every condition /// it would sleep or retry for returns FALLBACK instead. fn compute_inline( @@ -1582,7 +1807,12 @@ impl Backend { Err(error) if (error == EBUSY || error == EAGAIN) && !packet.is_published() => { Err(render::FALLBACK) } - result => result, + result => { + if result.is_ok() { + self.remember_direct(&firmware.queues); + } + result + } } } @@ -1617,6 +1847,9 @@ impl Backend { packet.check_dependencies()?; let super::command::Validated::Compute { scratch, .. } = &packet.command else { return Err(EINVAL); }; let required = super::fw::compute_pool::required_blocks(self.shared.active_cores, scratch.pages())?; + if let Some(result) = self.compute_direct(packet, required, deferred) { + return result; + } // A bound queue needs none of ensure_compute's admission passes. Try // without them; the publication section below checks the same state. let mut bound = true; @@ -1730,6 +1963,9 @@ impl Backend { kernel::time::delay::fsleep(kernel::time::Delta::from_millis(1)); continue; } + if result.is_ok() { + self.remember_direct(&firmware.queues); + } return result; } } @@ -1741,7 +1977,7 @@ impl super::Firmware { /// or an allocation-dependent collector. fn finish_stopped_queues(&mut self) { self.stop_render(); - for queue in self + for mut queue in self .queues .compute .iter_mut() @@ -1797,6 +2033,7 @@ impl super::Shared { dev_warn!(self.dev.as_ref(), "Stopped GPU retains outstanding preparation owners\n"); return; } + self.forget_port(); let firmware = self.state.lock().take(); if let Some(mut firmware) = firmware { firmware.finish_stopped_queues(); } } @@ -1810,6 +2047,7 @@ impl super::Shared { pub(super) fn stop_queues(&self) { self.preparations.wait_drained(); let _stopping = self.stop.lock(); + self.forget_port(); let firmware = { let mut state = self.state.lock(); if let Some(firmware) = (*state).as_deref_mut() { @@ -1849,7 +2087,7 @@ impl super::Firmware { deferred: &mut DeferredBatch, now: u64, ) -> Result> { - for queue in self + for mut queue in self .queues .compute .iter_mut() diff --git a/drivers/gpu/drm/asahi/g17/runtime/compute_control.rs b/drivers/gpu/drm/asahi/g17/runtime/compute_control.rs index c407d9576e9c4..4f519743aed53 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/compute_control.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/compute_control.rs @@ -20,11 +20,10 @@ impl Backend { return Ok(Decision::Own); }; firmware.scan_compute(None, &mut deferred)?; - let Some(queue) = firmware.queues.compute(self.owner) - .filter(|queue| queue.can_classify_timeout(packet)) else { - return Ok(Decision::Own); - }; - let Some(own) = queue.timeout_head() else { + // Each queue is locked on its own: release this one before visiting the others. + let Some(own) = firmware.queues.compute(self.owner) + .filter(|queue| queue.can_classify_timeout(packet)) + .and_then(|queue| queue.timeout_head()) else { return Ok(Decision::Own); }; let computes = firmware.queues.compute.iter().flatten() @@ -64,7 +63,7 @@ impl Backend { return Ok(None); }; firmware.scan_compute(None, &mut deferred)?; - let Some(queue) = firmware.queues.compute_mut(self.owner) else { + let Some(mut queue) = firmware.queues.compute_mut(self.owner) else { return Ok(None); }; if !queue.owns_packet(packet) { @@ -91,7 +90,7 @@ impl Backend { return Ok(()); }; firmware.scan_compute(None, &mut deferred)?; - if let Some(queue) = firmware.queues.compute_mut(self.owner) { + if let Some(mut queue) = firmware.queues.compute_mut(self.owner) { queue.cancel(packet, &mut |event| deferred.push(event))?; } Ok(()) diff --git a/drivers/gpu/drm/asahi/g17/runtime/compute_exit.rs b/drivers/gpu/drm/asahi/g17/runtime/compute_exit.rs index 984b6c1f643eb..69bc7550fc4e9 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/compute_exit.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/compute_exit.rs @@ -19,7 +19,7 @@ impl super::Registry { /// batch cannot overflow a separate cache. No allocation or wait is needed. pub(super) fn acknowledge_compute_exit(&mut self, cookie: u64) { for entry in self.compute.iter_mut().flatten() { - if let Some(queue) = entry.queue.as_deref_mut() { + if let Some(mut queue) = entry.queue.as_deref_mut() { if queue.acknowledge_exit(cookie) { return; } @@ -31,12 +31,12 @@ impl super::Registry { impl Firmware { pub(in crate::g17) fn service_compute_exit(&mut self, deferred: &mut DeferredBatch) -> Result { let recovery_closed = self.init.recovery_state()? == 0; - let view = crate::g17::qos::View::new(self.init.qos()?)?; + let port = &self.port; for entry in self.queues.compute.iter_mut().flatten() { - if let Some(queue) = entry.queue.as_deref_mut() { + if let Some(mut queue) = entry.queue.as_deref_mut() { queue.settle_exit( recovery_closed, - &mut |owner| self.queues.accounting.complete(&view, &[owner]), + &mut |owner| port.qos_complete(&[owner]), &mut |event| deferred.push(event), )?; } @@ -48,7 +48,7 @@ impl Firmware { // Publication has already retained each kill. Retry only the doorbell; // failed attempts do not extend its bounded polling window. for entry in self.queues.compute.iter_mut().flatten() { - if let Some(queue) = entry.queue.as_deref_mut() { + if let Some(mut queue) = entry.queue.as_deref_mut() { let _ = queue.announce_exit(now, || self.primary.notify(MSG_CONTROL_NOTIFY)); } } @@ -85,14 +85,16 @@ impl Firmware { if self.render_control_backpressured() { return Ok(()); } - let queue = self.queues.compute[index] - .as_ref() - .and_then(|entry| entry.queue.as_deref()) - .ok_or(EIO)?; - let Some(owner) = queue.exit_owner(context) else { - return Ok(()); + let (owner, target) = { + let queue = self.queues.compute[index] + .as_ref() + .and_then(|entry| entry.queue.as_deref()) + .ok_or(EIO)?; + let Some(owner) = queue.exit_owner(context) else { + return Ok(()); + }; + (owner, queue.qid()) }; - let target = queue.qid(); let (mut own, mut foreign, slots) = self.queues.render.exit_scope(context); for entry in self.queues.compute.iter().flatten() { if let Some(queue) = entry.queue.as_deref() { @@ -130,7 +132,7 @@ impl Firmware { } // Publication is irrevocable even if its doorbell fails. Keep both // the killed graph and the held render identities until processor stop. - let queue = self.queues.compute[index] + let mut queue = self.queues.compute[index] .as_mut() .and_then(|entry| entry.queue.as_deref_mut()) .ok_or(EIO)?; diff --git a/drivers/gpu/drm/asahi/g17/runtime/compute_replay.rs b/drivers/gpu/drm/asahi/g17/runtime/compute_replay.rs index d71f410c76fb4..0a8ebc28c02e2 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/compute_replay.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/compute_replay.rs @@ -3,7 +3,7 @@ //! Ordered republication of witnessed, unexecuted compute commands. use super::DeferredBatch; -use crate::g17::{completion::Deferred, qos, recovery, Firmware}; +use crate::g17::{completion::Deferred, recovery, Firmware}; use kernel::prelude::*; impl Firmware { @@ -31,12 +31,12 @@ impl Firmware { return Ok(()); } deferred.reserve_replays(commands)?; - let view = qos::View::new(self.init.qos()?)?; + let port = &self.port; for entry in self.queues.compute.iter_mut().flatten() { - if let Some(queue) = entry.queue.as_deref_mut() { + if let Some(mut queue) = entry.queue.as_deref_mut() { queue.prepare_replays( state, - |owner| self.queues.accounting.complete(&view, &[owner]), + |owner| port.qos_complete(&[owner]), &mut |event| deferred.push(event), )?; } @@ -46,20 +46,14 @@ impl Firmware { if self.recovery.pending() || recovery::Memory::recovery_state(&self.init)? != 0 { return Ok(()); } - let Some(queue) = self.queues.compute[index] + let Some((packet, owner)) = self.queues.compute[index] .as_ref() .and_then(|entry| entry.queue.as_deref()) + .and_then(|queue| Some((queue.replay_front()?, queue.owner()))) else { break; }; - let Some(packet) = queue.replay_front() else { - break; - }; - let owner = queue.owner(); - let outer_ready = self.init.work_ready( - packet.context.policy().priority(), - crate::g17::fw::queue::DataMaster::Compute, - )?; + let outer_ready = self.port.compute_ready(packet.context.policy().priority())?; if !outer_ready { let now = super::now_ns(); let since = *self.queues.replay_outer_wait.get_or_insert(now); @@ -90,7 +84,7 @@ impl Firmware { let recovery_wait = matches!(result, Err(EAGAIN) | Err(EBUSY)) && (self.recovery.pending() || recovery::Memory::recovery_state(&self.init)? != 0); - let queue = self.queues.compute[index] + let mut queue = self.queues.compute[index] .as_mut() .and_then(|entry| entry.queue.as_deref_mut()) .ok_or(EIO)?; diff --git a/drivers/gpu/drm/asahi/g17/runtime/events.rs b/drivers/gpu/drm/asahi/g17/runtime/events.rs index 4fd6e56741986..e46c820efa155 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/events.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/events.rs @@ -225,9 +225,9 @@ impl recovery::Queues for Service<'_> { impl Firmware { fn release_compute_witnesses(&mut self, deferred: &mut DeferredBatch) -> Result { let state = recovery::Memory::recovery_state(&self.init)?; - let view = crate::g17::qos::View::new(self.init.qos()?)?; + let port = &self.port; let registry = &mut self.queues; - for queue in registry + for mut queue in registry .compute .iter_mut() .flatten() @@ -235,7 +235,7 @@ impl Firmware { { queue.release_witnessed( state, - |owner| registry.accounting.complete(&view, &[owner]), + |owner| port.qos_complete(&[owner]), &mut |event| deferred.push(event), )?; } @@ -276,7 +276,11 @@ impl Firmware { }; let result = recovery::service(&mut service, || { shared.crashed.load(core::sync::atomic::Ordering::Acquire) - })?; + }); + // Publishers without the device mutex stamp their tickets with this + // mirror; they rejoin only after preparation admission reopens. + service.firmware.port.set_generation(service.firmware.recovery.generation()); + let result = result?; if let Some(error) = result.target_error { dev_warn!( shared.dev.as_ref(), diff --git a/drivers/gpu/drm/asahi/g17/runtime/render.rs b/drivers/gpu/drm/asahi/g17/runtime/render.rs index c657a45bdfa48..2877dea91d0ea 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render.rs @@ -514,21 +514,10 @@ impl crate::g17::queue::render::Host for Host<'_> { scheduler: u64, policy: crate::g17::fw::queue::Policy, ) -> Result { - use crate::g17::qos; - self.firmware.queues.accounting.publish( - &qos::View::new(self.firmware.init.qos()?)?, - owner, - scheduler, - policy, - qos::clock, - ) + self.firmware.port.qos_publish(owner, scheduler, policy) } fn qos_cancel(&mut self, publication: crate::g17::qos::Publication) -> Result { - use crate::g17::qos; - self.firmware - .queues - .accounting - .cancel(&qos::View::new(self.firmware.init.qos()?)?, publication) + self.firmware.port.qos_cancel(publication) } /// Idle descent is held for the whole first install. A point-in-time power /// check let the firmware gate the cores between the host's configuration @@ -559,7 +548,7 @@ impl crate::g17::queue::render::Host for Host<'_> { self.firmware.queues.qids.publish(id) } fn next_publication(&mut self) -> Result { - self.firmware.queues.qids.next_publication() + Ok(self.firmware.port.next_publication()) } fn publish_outer_pair( &mut self, @@ -619,13 +608,11 @@ impl crate::g17::queue::render::Host for Host<'_> { generation: u64, buffer_id: u8, ) -> Result { - use crate::g17::qos; let queues = &mut self.firmware.queues; let next = queues.render.pools.completed(pool_id, generation)?; queues.buffers.can_release(buffer_id)?; - let view = qos::View::new(self.firmware.init.qos()?)?; if let Some(owners) = owners { - queues.accounting.complete(&view, &owners)?; + self.firmware.port.qos_complete(&owners)?; } queues.render.pools.commit_completed(pool_id, next); queues.buffers.release(buffer_id) diff --git a/drivers/gpu/drm/asahi/g17/runtime/teardown.rs b/drivers/gpu/drm/asahi/g17/runtime/teardown.rs index f8020e8962f84..aaa2e281c69ee 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/teardown.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/teardown.rs @@ -115,7 +115,7 @@ impl Host for Service<'_> { self.compute .iter() .flatten() - .filter_map(|entry| entry.queue.as_ref()) + .filter_map(|entry| entry.queue.as_deref()) .all(|queue| queue.idle_for_context(context) && !queue.switching_from(context)) } fn flists_released(&self, context: &Arc) -> bool { From 7242bf87efd2b3f4f8bf2a31fadb79b3e687f195 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Fri, 23 Oct 2026 22:21:42 -0400 Subject: [PATCH 023/388] drm/asahi: send a G17 compute queue configuration only when it changes Render emits its queue configuration record only for an install, a context or QoS update, or when the configuration differs from the last one the firmware consumed in the current recovery epoch. Compute appended the record to every command, so the firmware reapplied unchanged queue state each time. Use the same rule for compute, caching the last consumed configuration with its epoch and forgetting it when the item ring is reset. compute.light d2 61.9 -> 51.5, d8 25.9 -> 24.3, d64 23.9 -> 22.8 us/submit; queue-to-queue hop 51.1 -> 49.6 us. Compute/sync CTS gate, cross-queue order/data 10k, dependent reads under pressure and mixed churn pass. (cherry picked from commit 0e59b1f92effb58c87f705907959de45abe77623) --- drivers/gpu/drm/asahi/g17/queue/compute.rs | 36 +++++++++++++------ drivers/gpu/drm/asahi/g17/queue/render.rs | 2 +- .../drm/asahi/g17/queue/render/publication.rs | 2 +- 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/queue/compute.rs b/drivers/gpu/drm/asahi/g17/queue/compute.rs index a21d1c915d0c8..5e6f20a3da43a 100644 --- a/drivers/gpu/drm/asahi/g17/queue/compute.rs +++ b/drivers/gpu/drm/asahi/g17/queue/compute.rs @@ -545,6 +545,9 @@ pub(crate) struct Queue { /// Bit `p` set once a work slot of priority class `p` named this queue. outer_started: u8, published_config: Option, + /// Last configuration record the firmware consumed, and its recovery + /// epoch. A command repeats the record only when it differs. + config_cache: Option<(QueueConfig, u64)>, ordinal: u32, submitted: u32, last_published: u64, @@ -609,6 +612,7 @@ impl Queue { installed: false, outer_started: 0, published_config: None, + config_cache: None, ordinal: 0, submitted: 0, last_published: 0, @@ -1436,6 +1440,7 @@ impl Queue { } fn recycle_settled(&mut self) -> Result { self.graph.reset_items()?; + self.config_cache = None; self.exit = None; self.quarantined = false; self.quarantine_error = None; @@ -1925,6 +1930,8 @@ impl Queue { let policy = packet.context.policy(); let ring = 1u8 << policy.priority(); let config = self.queue_config(&identity, first, args.context_id, policy, &packet)?; + let cached = self.config_cache.and_then(|(config, at)| (at == epoch).then_some(config)); + let send_config = first || super::render::publication::required(cached, config); let announce = KickAnnounce::new(qid, DataMaster::Compute, kick_timestamp, policy.priority())?; let predecessor = KickPredecessor::new(qid, kick_parent)?; @@ -1989,7 +1996,11 @@ impl Queue { let cursors = self.graph.cursors()?; let _ = self.graph.recycle(cursors); } - let config_va = self.graph.record(record_slot, CONFIG, 0xc0, config)?; + let config_va = if send_config { + Some(self.graph.record(record_slot, CONFIG, 0xc0, config)?) + } else { + None + }; let cursors = self.graph.cursors()?; let mut wait_vas = KVec::with_capacity(native.len(), GFP_KERNEL)?; for (index, wait) in native.iter().enumerate() { @@ -2023,7 +2034,7 @@ impl Queue { })?; producer = self .graph - .append(producer, config_va, || {}) + .append(producer, config_va.ok_or(EINVAL)?, || {}) .or_else(|error| { self.pool.cancel_unpublished(target)?; host.qos_cancel(qos)?; @@ -2060,14 +2071,16 @@ impl Queue { policy, )?; self.active.back_mut().ok_or(EIO)?.qos_pending = true; - producer = self - .graph - .append(producer, config_va, || {}) - .or_else(|error| { - host.qos_cancel(qos)?; - self.active.back_mut().ok_or(EIO)?.qos_pending = false; - Err(error) - })?; + if let Some(config_va) = config_va { + producer = self + .graph + .append(producer, config_va, || {}) + .or_else(|error| { + host.qos_cancel(qos)?; + self.active.back_mut().ok_or(EIO)?.qos_pending = false; + Err(error) + })?; + } } if first { prepared_kick.prepare_compute_install()?; @@ -2101,6 +2114,9 @@ impl Queue { self.outer_started |= ring; self.installed = true; self.published_config = Some(identity); + if send_config { + self.config_cache = Some((config, epoch)); + } self.ordinal = ordinal; let (accepted, state) = host.epoch()?; if state != 0 || accepted != epoch { diff --git a/drivers/gpu/drm/asahi/g17/queue/render.rs b/drivers/gpu/drm/asahi/g17/queue/render.rs index dfe3426eb6c24..e063ef3a047fc 100644 --- a/drivers/gpu/drm/asahi/g17/queue/render.rs +++ b/drivers/gpu/drm/asahi/g17/queue/render.rs @@ -7,7 +7,7 @@ pub(crate) use observation::Observed; mod dependencies; mod memory; pub(crate) mod parked; -mod publication; +pub(super) mod publication; pub(crate) mod retirement; mod warm; diff --git a/drivers/gpu/drm/asahi/g17/queue/render/publication.rs b/drivers/gpu/drm/asahi/g17/queue/render/publication.rs index 9f4cb05b6bede..c1985c72f6ec1 100644 --- a/drivers/gpu/drm/asahi/g17/queue/render/publication.rs +++ b/drivers/gpu/drm/asahi/g17/queue/render/publication.rs @@ -19,7 +19,7 @@ fn identity(mut config: QueueConfig) -> QueueConfig { config.qos_update = 0; config } -pub(super) fn required(previous: Option, config: QueueConfig) -> bool { +pub(in crate::g17::queue) fn required(previous: Option, config: QueueConfig) -> bool { config.install != 0 || config.context_update != 0 || config.completion_seed_valid != 0 From 014605a3679581351913c3ab137653297f7a0cf7 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Fri, 23 Oct 2026 22:28:39 -0400 Subject: [PATCH 024/388] drm/asahi: add G17 resource wait domains and use them for queue creation Waits for shared capacity sampled a condition and slept 1 ms before checking again. Add availability domains: a waiter samples the domain's generation before its failed check and registers a fence against that sample (or retries when a release intervened); the release that provides the resource bumps the generation and signals only that domain's waiters. The fences can serve as scheduler prepare_job dependencies or be waited on directly. The preparation gate can carry a domain that its reopening, failure and removal signal. Logical-queue creation is the first user: a creator refused for want of a queue slot or an execution context held by a closing queue now waits for the returned slot or the reclaimed context instead of sleeping in 1 ms steps, under the same overall deadline. Tested on Neo as part of compute-wait2 (stress gate passes). (cherry picked from commit 79cf4be7732d549b0f5d1f903bed7effdb80a4fb) --- drivers/gpu/drm/asahi/g17/availability.rs | 107 +++++++++++++++++++ drivers/gpu/drm/asahi/g17/mod.rs | 20 +++- drivers/gpu/drm/asahi/g17/preparation.rs | 28 ++++- drivers/gpu/drm/asahi/g17/queue/admission.rs | 13 +++ drivers/gpu/drm/asahi/g17/runtime.rs | 20 +++- 5 files changed, 179 insertions(+), 9 deletions(-) create mode 100644 drivers/gpu/drm/asahi/g17/availability.rs diff --git a/drivers/gpu/drm/asahi/g17/availability.rs b/drivers/gpu/drm/asahi/g17/availability.rs new file mode 100644 index 0000000000000..5b5d1d639a18e --- /dev/null +++ b/drivers/gpu/drm/asahi/g17/availability.rs @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT + +//! Resource-specific scheduler waits. A caller samples a domain before a failed try, then +//! registers against that sample after rolling back unpublished claims; the release that frees +//! the resource bumps the generation and signals exactly the waiters of its own domain. +//! Successful acquisition never allocates a fence, and a release without waiters costs one +//! lock round trip. + +use super::fence::{self, CompletionFence}; +use kernel::{ + c_str, + dma_fence::{Fence, FenceContexts, RawDmaFence, UserFence}, + new_mutex, + prelude::*, + sync::{Arc, Mutex}, +}; + +struct Entry { + fence: UserFence, + next: Option>, +} + +struct State { + generation: u64, + waiters: Option>, +} + +#[pin_data] +pub(crate) struct Availability { + #[pin] + state: Mutex, + fences: FenceContexts, +} + +impl Availability { + pub(crate) fn new() -> Result> { + Arc::pin_init( + try_pin_init!(Self { + state <- new_mutex!( + State { generation: 0, waiters: None }, + "g17::Availability" + ), + fences: FenceContexts::new(1, c_str!("asahi_resource"), kernel::static_lock_class!())?, + }), + GFP_KERNEL, + ) + } + + /// Sample before checking capacity under the resource's own synchronization. + pub(crate) fn observe(&self) -> u64 { + self.state.lock().generation + } + + /// A fence the next notification signals. None means capacity changed after the sample: + /// retry instead of waiting. Called with no resource lock held. The recheck after allocation + /// closes both the failed-check/registration and the fence-allocation/release races. A + /// waiter that goes away leaves its entry for the next notification to signal. + pub(crate) fn wait(&self, observed: u64) -> Result> { + if self.observe() != observed { + return Ok(None); + } + let fence = fence::independent(&self.fences)?; + let mut entry = KBox::new( + Entry { fence: fence.clone(), next: None }, + GFP_KERNEL, + )?; + let mut state = self.state.lock(); + if state.generation != observed { + return Ok(None); + } + entry.next = state.waiters.take(); + state.waiters = Some(entry); + Ok(Some(Fence::from_fence(&fence))) + } + + /// Blocks until a notification after `observed`, for at most `timeout_ms`. For callers + /// outside the scheduler that own no other wait. + pub(crate) fn block(&self, observed: u64, timeout_ms: u32, interruptible: bool) -> Result { + let Some(fence) = self.wait(observed)? else { return Ok(()); }; + // SAFETY: The owned fence keeps the raw DMA fence alive throughout the wait. + let waited = unsafe { + kernel::bindings::dma_fence_wait_timeout( + fence.raw(), + interruptible, + kernel::time::msecs_to_jiffies(timeout_ms) as isize, + ) + }; + if waited < 0 { + return Err(Error::from_errno(waited as i32)); + } + Ok(()) + } + + /// Wakes every waiter of this domain. Call after dropping the resource's own lock; fence + /// callbacks run outside the domain lock. + pub(crate) fn notify(&self) { + let mut waiters = { + let mut state = self.state.lock(); + state.generation = state.generation.wrapping_add(1); + state.waiters.take() + }; + while let Some(mut entry) = waiters { + waiters = entry.next.take(); + entry.fence.signal(); + } + } +} diff --git a/drivers/gpu/drm/asahi/g17/mod.rs b/drivers/gpu/drm/asahi/g17/mod.rs index edcb2ae737d63..3b8a615d1c533 100644 --- a/drivers/gpu/drm/asahi/g17/mod.rs +++ b/drivers/gpu/drm/asahi/g17/mod.rs @@ -17,6 +17,7 @@ //! Completions are signalled after dropping it. Lock order: `Shared::state` -> UAT shared state //! -> VM exec lock -> GEM reservation. `RoleState::ack` nests inside nothing. +mod availability; mod buffer; mod channel; mod command; @@ -830,6 +831,8 @@ struct Shared { #[pin] feed_work: Work, preparations: Arc, + /// Logical-queue creation waiting for a returned queue slot or execution context. + queues: Arc, /// Unbound compute publishers which can reuse a retired channel. compute_demand: AtomicU32, #[pin] @@ -1000,6 +1003,7 @@ impl Shared { // Context release returns its execution root off-lock. The next // device pass can now detach idle compute aliases and their QoS // leases, without depending on a new client's submission/event. + self.queues.notify(); self.queue_events(); } } @@ -1112,6 +1116,7 @@ impl Gpu { }; let max_frequency_khz = Self::max_frequency_khz(dev)?; let preparations = preparation::Gate::new()?; + let queues = availability::Availability::new()?; let reclaim = runtime::teardown::ReclaimBatch::new()?; let feed = feed::Feed::new()?; let active_cores = id.core_masks.iter().map(|mask| mask.count_ones()).sum(); @@ -1142,6 +1147,7 @@ impl Gpu { feed, feed_work <- new_work!("g17::Shared::feed_work"), preparations, + queues, compute_demand: AtomicU32::new(0), work <- new_work!("g17::Shared::work"), poll <- new_delayed_work!("g17::Shared::poll"), @@ -1298,11 +1304,12 @@ impl Gpu { hold: power::Hold::new(), registration_power_release_pending: false, variant: shared.variant, - queues <- runtime::Registry::new( + queues <- runtime::Registry::with_waiters( shared.clusters, shared.deflake, shared.descriptor_flags, shared.variant, + Some(shared.queues.clone()), ), pool: object::Pool::new()?, render_rings: kick::RenderRings::new()?, @@ -1573,16 +1580,21 @@ impl gpu::Gpu for Gpu { let policy = scheduling_policy(priority)?; let admission_started = now_ns(); let (backend, slot) = loop { + let observed = self.shared.queues.observe(); match runtime::Backend::new(&self.shared, &vm, owner, policy) { Err(error) if error == EAGAIN => continue, Err(error) if error == ENOSPC => { // Closed logical queues can still own execution/QoS IDs // until the scheduler release is consumed and reclaimed. // Backend construction has rolled back all temporary leases. - if now_ns().saturating_sub(admission_started) - >= job::TIMEOUT_MS as u64 * 1_000_000 { return Err(EBUSY); } + let waited = now_ns().saturating_sub(admission_started); + let limit = job::TIMEOUT_MS as u64 * 1_000_000; + if waited >= limit { return Err(EBUSY); } self.shared.queue_events(); - kernel::time::delay::fsleep(kernel::time::Delta::from_millis(1)); + // A returned queue slot or reclaimed context wakes this. + self.shared + .queues + .block(observed, (limit - waited).div_ceil(1_000_000) as u32, true)?; } result => break result?, } diff --git a/drivers/gpu/drm/asahi/g17/preparation.rs b/drivers/gpu/drm/asahi/g17/preparation.rs index 0297c619b4f93..e014d4bbb6542 100644 --- a/drivers/gpu/drm/asahi/g17/preparation.rs +++ b/drivers/gpu/drm/asahi/g17/preparation.rs @@ -55,20 +55,36 @@ pub(super) struct Gate { wait: Mutex<()>, #[pin] changed: CondVar, + /// Scheduler admission waiting for publication to reopen. + available: Option>, } impl Gate { pub(super) fn new() -> Result> { + Self::with_waiters(None) + } + + /// A gate whose reopening, failure and removal also wake `available`. + pub(super) fn with_waiters( + available: Option>, + ) -> Result> { Arc::pin_init( pin_init!(Self { state: AtomicU64::new(EPOCH), wait <- new_mutex!((), "g17::Gate::wait"), changed <- new_condvar!("g17::Gate::changed"), + available, }), GFP_KERNEL, ) } + fn wake_waiters(&self) { + if let Some(available) = &self.available { + available.notify(); + } + } + pub(super) fn try_enter(self: &Arc) -> Result { let mut previous = self.state.load(Ordering::Acquire); loop { @@ -168,14 +184,18 @@ impl Gate { pub(super) fn remove(&self) { self.invalidate(REMOVED); - let _wait = self.wait.lock(); + let wait = self.wait.lock(); self.changed.notify_all(); + drop(wait); + self.wake_waiters(); } pub(super) fn fail(&self) { self.invalidate(FAILED); - let _wait = self.wait.lock(); + let wait = self.wait.lock(); self.changed.notify_all(); + drop(wait); + self.wake_waiters(); } /// Runtime failure/recovery must not join an indefinitely stalled builder. @@ -199,7 +219,7 @@ impl Gate { /// Reopen only an empty, temporary closure. Failed or removed devices remain closed. pub(super) fn reopen(&self) -> bool { - let _wait = self.wait.lock(); + let wait = self.wait.lock(); let mut previous = self.state.load(Ordering::Acquire); loop { if previous & (REMOVED | FAILED | COUNT_MASK) != 0 { @@ -213,6 +233,8 @@ impl Gate { ) { Ok(_) => { self.changed.notify_all(); + drop(wait); + self.wake_waiters(); return true; } Err(current) => previous = current, diff --git a/drivers/gpu/drm/asahi/g17/queue/admission.rs b/drivers/gpu/drm/asahi/g17/queue/admission.rs index 7533d345cfa6d..ff2b294b6f35c 100644 --- a/drivers/gpu/drm/asahi/g17/queue/admission.rs +++ b/drivers/gpu/drm/asahi/g17/queue/admission.rs @@ -377,9 +377,18 @@ pub(crate) struct Pool { queues: AtomicU32, /// Logical queues admitted at most: each must be able to hold a render pair. max_queues: u32, + /// Logical-queue creation waiting for a returned slot. + waiters: Option>, } impl Pool { + #[cfg(CONFIG_KUNIT)] pub(crate) fn new(max_queues: u32) -> Result> { + Self::with_waiters(max_queues, None) + } + pub(crate) fn with_waiters( + max_queues: u32, + waiters: Option>, + ) -> Result> { if max_queues > MAX_EXECUTION_CONTEXTS { return Err(EINVAL); } @@ -389,6 +398,7 @@ impl Pool { released <- new_condvar!("G17 render pair release"), queues: AtomicU32::new(0), max_queues, + waiters, }), GFP_KERNEL, ) @@ -667,6 +677,9 @@ pub(crate) struct QueueSlot { impl Drop for QueueSlot { fn drop(&mut self) { self.pool.queues.fetch_sub(1, Ordering::AcqRel); + if let Some(waiters) = &self.pool.waiters { + waiters.notify(); + } } } diff --git a/drivers/gpu/drm/asahi/g17/runtime.rs b/drivers/gpu/drm/asahi/g17/runtime.rs index 237c92db48ce2..88a3330155cac 100644 --- a/drivers/gpu/drm/asahi/g17/runtime.rs +++ b/drivers/gpu/drm/asahi/g17/runtime.rs @@ -185,11 +185,23 @@ pub(super) struct Registry { impl Registry { /// Initialize inside the firmware allocation instead of staging the whole /// registry on the probe stack. No additional allocation is required. + #[cfg(CONFIG_KUNIT)] pub(super) fn new( clusters: u32, deflake: crate::g17::fw::render::Deflake, descriptor_flags: [u32; 2], variant: &'static crate::g17::fw::Variant, + ) -> impl Init { + Self::with_waiters(clusters, deflake, descriptor_flags, variant, None) + } + + /// `queues` wakes logical-queue creation when a logical queue slot is returned. + pub(super) fn with_waiters( + clusters: u32, + deflake: crate::g17::fw::render::Deflake, + descriptor_flags: [u32; 2], + variant: &'static crate::g17::fw::Variant, + queues: Option>, ) -> impl Init { kernel::try_init!(Self { qids <- kick::QueueIds::new(variant.qid_floor).chain(|qids| { @@ -198,7 +210,7 @@ impl Registry { buffers <- BufferIds::new(), metrics: Arc::new(MetricsIds::new(), GFP_KERNEL)?, qos_ids: QosIds::new()?, - admission: admission::Pool::new(kick::queue_limit(variant.qid_floor))?, + admission: admission::Pool::with_waiters(kick::queue_limit(variant.qid_floor), queues)?, render: render::RenderState::new(clusters, deflake, descriptor_flags)?, identities: queue::Identities::new(), compute: { @@ -621,6 +633,7 @@ impl Backend { const RETAINED_CONTEXT_WAIT_NS: u64 = 4 * WORK_RING_WAIT_NS; let started = now_ns(); let (lease, uat, pool, qos, execution, slot) = loop { + let observed = shared.queues.observe(); let lease = shared.preparations.enter(true)?; let mut state = shared.state.lock(); let firmware = (*state).as_deref_mut().ok_or(ENODEV)?; @@ -660,7 +673,10 @@ impl Backend { return Err(ERESTARTSYS); } shared.queue_events(); - kernel::time::delay::fsleep(kernel::time::Delta::from_millis(1)); + // A closing queue returns its context when its release + // is reclaimed; that reclaim wakes this creator. + let left = RETAINED_CONTEXT_WAIT_NS.saturating_sub(now_ns().saturating_sub(started)); + shared.queues.block(observed, left.div_ceil(1_000_000) as u32, true)?; } Err(error) => return Err(error), } From 64f717dcfd1e4aa5856882181f5c2ac040c5ce4e Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Fri, 23 Oct 2026 22:30:03 -0400 Subject: [PATCH 025/388] drm/asahi: wait for G17 compute capacity on the release that frees it Compute admission and publication retried with sleeps: 1 ms for an unpublished input source, a 20 us..1 ms backoff for a work state, up to 200 x 1 ms for recovery, 1 ms per ring backpressure retry, 200 x 1 ms for a spared drain witness, and 1 ms per growth and channel capacity retry. Use two availability domains for compute admission; the release that provides the resource signals only the waiters of its own domain: - channels: a queue becomes reusable, shareable or re-armable (checked on each transition in the retirement scan and pool service), a scheduler release is consumed, an owner closes, or an unpublished slot and QID are returned; - progress: any compute retirement, finished replays, pool release observation, and the preparation gate reopening after recovery. prepare_job now returns the channel, work-state or recovery fence a compute packet still lacks, so run_job finds its capacity ready, and a prepared packet claims its bound channel so the channel is not shared away before run_job. Retries that remain inside publication block on the same fences; the admission slice only bounds cancellation checks. Each event pass also rechecks the level while waiters exist, which covers capacity that appears through render (QIDs, work states, closed queues). gpubench on Neo (us/submit, matched baseline): unchanged steady state (procs d16 33.1, d64 25.9, d1 162); procs d2 93.2 -> 69.2. With the channel budget forced back to ten channels (diagnostic build), sixteen processes now share them at 51.9 us/submit instead of the old 92.0, with no stall. Stress gate passes, also under the forced shortage. (cherry picked from commit 25fb5800d97db7457f093ff41206047fbd9bddcc) --- drivers/gpu/drm/asahi/g17/availability.rs | 4 + drivers/gpu/drm/asahi/g17/job.rs | 4 +- drivers/gpu/drm/asahi/g17/mod.rs | 24 +- drivers/gpu/drm/asahi/g17/preparation.rs | 1 + drivers/gpu/drm/asahi/g17/queue/compute.rs | 24 +- drivers/gpu/drm/asahi/g17/runtime.rs | 314 +++++++++++++++--- drivers/gpu/drm/asahi/g17/runtime/backend.rs | 18 +- .../drm/asahi/g17/runtime/compute_replay.rs | 2 + drivers/gpu/drm/asahi/g17/runtime/teardown.rs | 9 +- 9 files changed, 337 insertions(+), 63 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/availability.rs b/drivers/gpu/drm/asahi/g17/availability.rs index 5b5d1d639a18e..8014266f0b0ab 100644 --- a/drivers/gpu/drm/asahi/g17/availability.rs +++ b/drivers/gpu/drm/asahi/g17/availability.rs @@ -51,6 +51,10 @@ impl Availability { self.state.lock().generation } + pub(crate) fn has_waiters(&self) -> bool { + self.state.lock().waiters.is_some() + } + /// A fence the next notification signals. None means capacity changed after the sample: /// retry instead of waiting. Called with no resource lock held. The recheck after allocation /// closes both the failed-check/registration and the fence-allocation/release races. A diff --git a/drivers/gpu/drm/asahi/g17/job.rs b/drivers/gpu/drm/asahi/g17/job.rs index 4ad4f8e372764..ae401b06f2eb3 100644 --- a/drivers/gpu/drm/asahi/g17/job.rs +++ b/drivers/gpu/drm/asahi/g17/job.rs @@ -282,7 +282,9 @@ pub(crate) trait Backend: Send + Sync { fn fail_vm(&self, error: Error); /// Called after both scheduler entities have cancelled their queued jobs. fn release_owner(&self); - fn prepare(&self, packet: &Packet) -> Option; + /// A fence to wait for before `publish`, signalled by the release of the capacity the + /// packet lacks. Called again once it signals. + fn prepare(&self, packet: &Arc) -> Option; fn publish(&self, packet: Arc) -> Result; fn timed_out(&self, packet: Arc) -> sched::Status; fn cancel(&self, packet: Arc); diff --git a/drivers/gpu/drm/asahi/g17/mod.rs b/drivers/gpu/drm/asahi/g17/mod.rs index 3b8a615d1c533..330f57ab59779 100644 --- a/drivers/gpu/drm/asahi/g17/mod.rs +++ b/drivers/gpu/drm/asahi/g17/mod.rs @@ -12,10 +12,14 @@ //! //! # Locking //! -//! `Shared::state` is the device lock. It serializes all firmware-visible publication and all -//! firmware-shared state, including the bounded waits for the firmware that publication needs. -//! Completions are signalled after dropping it. Lock order: `Shared::state` -> UAT shared state -//! -> VM exec lock -> GEM reservation. `RoleState::ack` nests inside nothing. +//! `Shared::state` is the device lock. It serializes binding, recovery, the event worker and all +//! firmware-shared state that has no narrower owner. Steady-state compute publication into a +//! bound queue takes only that queue's cell lock and the port's locks ([`port`]), reached under +//! a preparation lease. Completions are signalled after dropping the device lock. Lock order: +//! `Shared::state` -> compute queue cell (one at a time) -> port locks -> availability domains; +//! `Shared::state` -> UAT shared state -> VM exec lock -> GEM reservation. `RoleState::ack` +//! nests inside nothing. Capacity waits block on an availability domain signalled by the +//! release that provides the capacity, never on a sleep. mod availability; mod buffer; @@ -831,6 +835,12 @@ struct Shared { #[pin] feed_work: Work, preparations: Arc, + /// Compute admission waiting for a physical channel: signalled when a channel becomes + /// reusable or shareable, a QID or slot is returned, or a pool vacancy is witnessed. + channels: Arc, + /// Compute admission waiting for retirement-bound capacity (work states, ring and + /// in-flight room, drain witnesses, pool releases) or for publication to reopen. + progress: Arc, /// Logical-queue creation waiting for a returned queue slot or execution context. queues: Arc, /// Unbound compute publishers which can reuse a retired channel. @@ -1115,8 +1125,10 @@ impl Gpu { oas: cfg.uat_oas, }; let max_frequency_khz = Self::max_frequency_khz(dev)?; - let preparations = preparation::Gate::new()?; + let channels = availability::Availability::new()?; + let progress = availability::Availability::new()?; let queues = availability::Availability::new()?; + let preparations = preparation::Gate::with_waiters(Some(progress.clone()))?; let reclaim = runtime::teardown::ReclaimBatch::new()?; let feed = feed::Feed::new()?; let active_cores = id.core_masks.iter().map(|mask| mask.count_ones()).sum(); @@ -1147,6 +1159,8 @@ impl Gpu { feed, feed_work <- new_work!("g17::Shared::feed_work"), preparations, + channels, + progress, queues, compute_demand: AtomicU32::new(0), work <- new_work!("g17::Shared::work"), diff --git a/drivers/gpu/drm/asahi/g17/preparation.rs b/drivers/gpu/drm/asahi/g17/preparation.rs index e014d4bbb6542..0df2c5ae2ebb2 100644 --- a/drivers/gpu/drm/asahi/g17/preparation.rs +++ b/drivers/gpu/drm/asahi/g17/preparation.rs @@ -60,6 +60,7 @@ pub(super) struct Gate { } impl Gate { + #[cfg(CONFIG_KUNIT)] pub(super) fn new() -> Result> { Self::with_waiters(None) } diff --git a/drivers/gpu/drm/asahi/g17/queue/compute.rs b/drivers/gpu/drm/asahi/g17/queue/compute.rs index 5e6f20a3da43a..da0123d8405df 100644 --- a/drivers/gpu/drm/asahi/g17/queue/compute.rs +++ b/drivers/gpu/drm/asahi/g17/queue/compute.rs @@ -550,6 +550,8 @@ pub(crate) struct Queue { config_cache: Option<(QueueConfig, u64)>, ordinal: u32, submitted: u32, + /// Prepared packets of the bound owner about to publish (see [`Self::claim`]). + claims: u32, last_published: u64, last_end: u64, released: bool, @@ -615,6 +617,7 @@ impl Queue { config_cache: None, ordinal: 0, submitted: 0, + claims: 0, last_published: 0, last_end: 0, released: false, @@ -2367,14 +2370,33 @@ impl Queue { Ok(self.reusable()) } + /// Another owner could take this channel now: idle and released, shareable, or vacant. + pub(crate) fn channel_free(&self) -> bool { + self.reusable() || self.shareable() || self.can_rearm() + } + + /// A pool replacement is in flight for this queue. + pub(crate) fn growing(&self) -> bool { + self.growing + } + /// The queue's first command has been published: its identity and configuration exist. pub(crate) fn installed(&self) -> bool { self.installed } + /// A packet of the bound owner passed prepare_job and will publish here. + pub(crate) fn claim(&mut self) { + self.claims = self.claims.saturating_add(1); + } + + pub(crate) fn release_claim(&mut self) { + self.claims = self.claims.saturating_sub(1); + } + /// An open client's idle channel that [`Self::share_idle`] can hand over. pub(crate) fn shareable(&self) -> bool { - self.installed && self.owner.is_some() && self.retirement_ready + self.claims == 0 && self.installed && self.owner.is_some() && self.retirement_ready && !self.growing && !self.quarantined && !self.awaiting_witness && self.exit.is_none() && self.active.len == 0 && self.replays.len == 0 && self.previous.is_none() && self.pool_phase == PoolPhase::Populated diff --git a/drivers/gpu/drm/asahi/g17/runtime.rs b/drivers/gpu/drm/asahi/g17/runtime.rs index 88a3330155cac..f1cefb99fe005 100644 --- a/drivers/gpu/drm/asahi/g17/runtime.rs +++ b/drivers/gpu/drm/asahi/g17/runtime.rs @@ -299,6 +299,38 @@ impl Registry { Ok(self.bind_reusable(owner, context, required, admission)?.unwrap_or(Rebind::Fresh)) } + /// Whether `owner` can bind a channel now, without changing any binding. Owner 0 asks + /// only whether some channel is free for a new owner. + fn compute_readiness(&self, owner: u64, context: Option<&Arc>, required: usize) -> Readiness { + if let (Some(context), Some(queue)) = (context, self.compute(owner).filter(|_| owner != 0)) { + return if queue.spared_pending() { + Readiness::Progress + } else if !matches!(queue.matches(owner, queue.qid(), context), Ok(true)) { + // Publication reports the mismatch. + Readiness::Ready + } else if queue.pool_fits(required) || !queue.growing() { + Readiness::Ready + } else { + Readiness::Progress + }; + } + let teardown = &self.teardown; + let available = self + .compute + .iter() + .flatten() + .filter_map(|entry| entry.queue.as_deref()) + .any(|queue| { + (queue.reusable() || queue.shareable() || queue.can_rearm()) + && !queue.context().is_some_and(|bound| teardown.releasing(bound)) + }); + if available || self.qids.compute_fits(self.admission.queues() as usize) { + Readiness::Ready + } else { + Readiness::Channel + } + } + /// Binds `owner` to a reusable channel whose last owner released it, or /// names a vacant one to re-arm. None if no channel is reusable. fn bind_reusable(&mut self, owner: u64, context: &Arc, required: usize, admission: &Admission<'_>) -> Result> { @@ -563,6 +595,16 @@ impl Registry { } } +/// What a compute packet still lacks before publication. +#[derive(Copy, Clone, PartialEq, Eq)] +pub(super) enum Readiness { + Ready, + /// A physical channel: every one is busy and no QID fits. + Channel, + /// Retirement-bound capacity or reopened publication. + Progress, +} + /// Outcome of offering an owner a retained compute queue. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub(super) enum Rebind { @@ -616,6 +658,10 @@ pub(super) struct Backend { /// binding under the cell lock; a stale cell only sends the packet back to that path. #[pin] direct: kernel::sync::Mutex>>, + /// The channel a prepared packet is about to publish into. Its claim keeps the idle + /// channel from being shared away between prepare_job and run_job. + #[pin] + claimed: kernel::sync::Mutex>>, } impl Backend { @@ -707,6 +753,7 @@ impl Backend { compute_backlog: core::sync::atomic::AtomicU32::new(0), closed: core::sync::atomic::AtomicBool::new(false), direct <- kernel::new_mutex!(None, "g17::Backend::direct"), + claimed <- kernel::new_mutex!(None, "g17::Backend::claimed"), }), GFP_KERNEL, )?, @@ -852,11 +899,14 @@ impl Backend { drop(queue); let mut state = self.shared.state.lock(); let firmware = state.as_deref_mut().ok_or(ENODEV)?; - firmware.queues.cancel_compute(reservation) + firmware.queues.cancel_compute(reservation)?; + drop(state); + // The slot and its never-published QID are free again. + self.shared.channels.notify(); + Ok(()) } fn grow_compute(&self, required: usize, admission: &Admission<'_>) -> Result { - use kernel::time::{delay::fsleep, Delta}; admission.check()?; { let state = self.shared.state.lock(); @@ -874,6 +924,9 @@ impl Backend { let mut deferred = DeferredBatch::publication()?; let mut growth = None; let result = (|| loop { + // Growth waits for this queue's retirement and for firmware to consume the pool + // release; both are progress the event worker reports. + let observed = self.shared.progress.observe(); let result = (|| { let mut state = self.shared.state.lock(); admission.check()?; @@ -899,9 +952,9 @@ impl Backend { match result { Ok(old) => { drop(old); return Ok(()); } Err(error) if error == EAGAIN && lease.is_current() => { - self.shared.queue_events(); - fsleep(Delta::from_millis(1)); - } + self.shared.queue_events(); + self.wait_release(&self.shared.progress, observed, admission)?; + } Err(error) => return Err(error), } })(); @@ -948,6 +1001,8 @@ impl Backend { } loop { admission.check()?; + // Sampled before any capacity check of this attempt. + let observed = self.shared.channels.observe(); let lease = self.shared.preparations.enter_until(admission)?; let dev = self.shared.drm()?; let (uat, objects, reservation) = { @@ -966,7 +1021,7 @@ impl Backend { Err(error) if error == ENOSPC => { drop(state); drop(lease); - self.wait_compute_capacity(admission, true)?; + self.wait_channels(observed, admission, true)?; continue; } result => result?, @@ -984,7 +1039,7 @@ impl Backend { // it cannot create a QID and forces fresh allocation. drop(state); drop(lease); - self.wait_compute_capacity(admission, false)?; + self.wait_channels(observed, admission, false)?; continue; } Err(error) if error == ENOSPC => { @@ -1018,7 +1073,7 @@ impl Backend { let (pages, ring) = match built { Err(error) if error == ENOSPC => { drop(lease); - self.wait_compute_capacity(admission, true)?; + self.wait_channels(observed, admission, true)?; continue; } result => result?, @@ -1108,7 +1163,7 @@ impl Backend { } drop(lease); if result == Err(ENOSPC) { - self.wait_compute_capacity(admission, true)?; + self.wait_channels(observed, admission, true)?; continue; } if result != Err(EAGAIN) { return result; } @@ -1237,6 +1292,10 @@ impl DeferredBatch { } Ok(()) } + /// Completions collected so far. + pub(super) fn pending(&self) -> usize { + self.values.len() + } /// Called only after releasing the device mutex. pub(super) fn finish(&mut self) { use super::completion::Deferred; @@ -1570,12 +1629,15 @@ impl super::Firmware { )?; } let port = &self.port; + let retired = deferred.pending(); + let mut freed = false; for mut queue in registry .compute .iter_mut() .flatten() .filter_map(|entry| entry.queue.as_deref_mut()) { + let busy = !queue.channel_free(); queue.latch_observations(&mut registry.observations, masks)?; queue.retire( masks, @@ -1584,6 +1646,7 @@ impl super::Firmware { |owner| port.qos_complete(&[owner]), &mut |event| deferred.push(event), )?; + freed |= busy && queue.channel_free(); } // All compute observations in this batch are latched before a VM // failure can settle its render peers. Exact successes win first. @@ -1606,11 +1669,22 @@ impl super::Firmware { .flatten() .filter_map(|entry| entry.queue.as_deref_mut()) { + let busy = !queue.channel_free(); queue.release_witnessed( state, |owner| port.qos_complete(&[owner]), &mut |event| deferred.push(event), )?; + freed |= busy && queue.channel_free(); + } + // Wake only the admission waits this batch can end. The fences gate + // scheduler entities alone; their callbacks never take the device mutex. + let shared = &self.primary.state.shared; + if freed { + shared.channels.notify(); + } + if deferred.pending() != retired { + shared.progress.notify(); } Ok(()) } @@ -1620,11 +1694,10 @@ impl Backend { fn ensure_compute(&self, scs_pages: u16, admission: &Admission<'_>) -> Result { admission.check()?; let required = super::fw::compute_pool::required_blocks(self.shared.active_cores, scs_pages)?; - use kernel::time::{delay::fsleep, Delta}; - const SPARED_WAIT_MS: usize = 200; let mut deferred = DeferredBatch::publication()?; - for _ in 0..SPARED_WAIT_MS { + loop { admission.check()?; + let observed = self.shared.progress.observe(); let pending = (|| { let mut state = self.shared.state.lock(); let firmware = (*state).as_deref_mut().ok_or(ENODEV)?; @@ -1641,14 +1714,12 @@ impl Backend { result => return result, } } - // Other owners continue submitting while this owner gives its - // queue-local drain witness time to arrive. - fsleep(Delta::from_millis(1)); + // Other owners continue submitting while this owner waits for its + // queue-local drain witness. No new command has been accepted: the + // spared queue and its retained work stay intact past the deadline. + self.shared.queue_events(); + self.wait_release(&self.shared.progress, observed, admission)?; } - // No new command has been accepted. Keep the spared queue and its - // retained work intact until a later admission observes its witness. - admission.check()?; - Err(EBUSY) } /// Steady-state publication into this owner's bound channel without the device mutex: only @@ -1706,6 +1777,98 @@ impl Backend { } } + /// Scheduler admission of a compute packet. Shared capacity the packet lacks (a channel, a + /// work state, retirement-bound room, reopened publication) becomes a fence that the release + /// providing it signals, so publication finds it ready instead of sleeping for it. Errors + /// and anything prepare cannot judge are left to publication. + pub(super) fn prepare_compute(&self, packet: &Arc) -> Option { + if packet.early_result().is_some() + || packet.cancel_requested() + || packet.completion.status().get() != 0 + || self.admission_open().is_err() + { + return None; + } + let super::command::Validated::Compute { scratch, .. } = &packet.command else { return None; }; + let required = super::fw::compute_pool::required_blocks(self.shared.active_cores, scratch.pages()).ok()?; + loop { + let channels = self.shared.channels.observe(); + let progress = self.shared.progress.observe(); + let (domain, observed) = match self.compute_readiness(packet, required) { + Readiness::Ready => return None, + Readiness::Channel => (&self.shared.channels, channels), + Readiness::Progress => (&self.shared.progress, progress), + }; + self.shared.queue_events(); + match domain.wait(observed) { + Ok(Some(fence)) => return Some(fence), + Ok(None) => continue, + Err(_) => return None, + } + } + } + + fn compute_readiness(&self, packet: &Arc, required: usize) -> Readiness { + match packet.completion.work_state().and_then(|work| work.acquire()) { + Err(error) if error == EBUSY => return Readiness::Progress, + Err(_) => return Readiness::Ready, + Ok(_) => {} + } + // The steady state: this owner's channel is bound and installed. + if packet.completion.sources().is_none() { + if let Some(cell) = self.direct.lock().clone() { + let bound = cell.lock().is_some_and(|mut queue| { + let bound = queue.installed() + && !queue.spared_pending() + && queue.pool_fits(required) + && matches!(queue.matches(self.owner, queue.qid(), &self.context), Ok(true)); + if bound { + queue.claim(); + } + bound + }); + if bound { + self.hold_claim(Some(cell)); + return Readiness::Ready; + } + } + } + match self.sources_published(packet) { + Ok(false) => return Readiness::Progress, + Err(_) => return Readiness::Ready, + Ok(true) => {} + } + let lease = match self.shared.preparations.try_enter() { + Ok(lease) => lease, + Err(error) if error == EAGAIN => return Readiness::Progress, + Err(_) => return Readiness::Ready, + }; + let state = self.shared.state.lock(); + let Some(firmware) = state.as_deref() else { return Readiness::Ready; }; + if !lease.is_current() + || firmware.recovery.pending() + || super::recovery::Memory::recovery_state(&firmware.init).unwrap_or(0) != 0 + { + return Readiness::Progress; + } + firmware.queues.compute_readiness(self.owner, Some(&self.context), required) + } + + /// Replaces the prepared packet's channel claim; None releases it after publication, + /// cancellation or close. + pub(super) fn hold_claim(&self, cell: Option>) { + let old = core::mem::replace(&mut *self.claimed.lock(), cell); + let freed = old.as_ref().and_then(|old| old.lock()).is_some_and(|mut queue| { + let busy = !queue.channel_free(); + queue.release_claim(); + busy && queue.channel_free() + }); + if freed { + // A channel nobody prepares into any more is shareable now. + self.shared.channels.notify(); + } + } + /// Called with the device mutex held after a successful publication: remembers the bound /// channel for direct publication. fn remember_direct(&self, registry: &Registry) { @@ -1868,10 +2031,11 @@ impl Backend { } // A bound queue needs none of ensure_compute's admission passes. Try // without them; the publication section below checks the same state. + // Every retry blocks on the release that ends its wait: retirement + // progress (work states, ring room, replays) and the reopening of + // publication after recovery both signal `progress`. let mut bound = true; let mut demand = None; - let mut recovery_polls = 0; - let mut state_delay = 20i64; let deadline = now_ns().saturating_add(WORK_RING_WAIT_NS); let check = || { if packet.cancel_requested() { return Err(ECANCELED); } @@ -1880,11 +2044,13 @@ impl Backend { Ok(()) }; let admission = Admission::new(deadline, &check); + let progress = &self.shared.progress; loop { admission.check()?; + let observed = progress.observe(); if !self.sources_published(packet)? { self.shared.queue_events(); - kernel::time::delay::fsleep(kernel::time::Delta::from_millis(1)); + self.wait_release(progress, observed, &admission)?; continue; } // All DRM dependencies were satisfied before this scheduler callback. @@ -1897,15 +2063,11 @@ impl Backend { } let lease = self.shared.preparations.enter_until(&admission)?; // Work states free as this context's earlier commands retire. - // Wait for one under the packet's admission deadline, as render - // publication does, instead of failing the packet. match packet.completion.work_state()?.acquire() { Err(error) if error == EBUSY => { drop(lease); - admission.check()?; self.shared.queue_events(); - kernel::time::delay::fsleep(kernel::time::Delta::from_micros(state_delay)); - state_delay = (state_delay * 2).min(1000); + self.wait_release(progress, observed, &admission)?; continue; } result => { @@ -1928,14 +2090,10 @@ impl Backend { } let recovery_state = super::recovery::Memory::recovery_state(&firmware.init)?; if firmware.recovery.pending() || recovery_state != 0 { - if recovery_polls == RECOVERY_SETTLE_POLLS { - return Err(EBUSY); - } - recovery_polls += 1; drop(state); drop(lease); self.shared.queue_events(); - kernel::time::delay::fsleep(kernel::time::Delta::from_millis(1)); + self.wait_release(progress, observed, &admission)?; continue; } firmware.replay_compute(deferred)?; @@ -1955,28 +2113,18 @@ impl Backend { dependencies.as_slice(), deferred, ); - if matches!(result, Err(EBUSY) | Err(EAGAIN)) - && !packet.is_published() - && (firmware.recovery.pending() - || super::recovery::Memory::recovery_state(&firmware.init)? != 0) + if !packet.is_published() + && (matches!(result, Err(EAGAIN)) + || (matches!(result, Err(EBUSY)) + && (firmware.recovery.pending() + || super::recovery::Memory::recovery_state(&firmware.init)? != 0))) { - if recovery_polls == RECOVERY_SETTLE_POLLS { - return Err(EBUSY); - } - recovery_polls += 1; - drop(state); - drop(lease); - self.shared.queue_events(); - kernel::time::delay::fsleep(kernel::time::Delta::from_millis(1)); - continue; - } - if matches!(result, Err(EAGAIN)) && !packet.is_published() { - // Backpressure owns no firmware-visible attempt. Other queues - // can submit and the event worker can retire while we sleep. + // Recovery or backpressure: nothing is firmware-visible. Other + // queues keep submitting and the event worker retires meanwhile. drop(state); drop(lease); self.shared.queue_events(); - kernel::time::delay::fsleep(kernel::time::Delta::from_millis(1)); + self.wait_release(progress, observed, &admission)?; continue; } if result.is_ok() { @@ -2103,6 +2251,8 @@ impl super::Firmware { deferred: &mut DeferredBatch, now: u64, ) -> Result> { + let mut freed = false; + let mut releasing = false; for mut queue in self .queues .compute @@ -2110,13 +2260,32 @@ impl super::Firmware { .flatten() .filter_map(|entry| entry.queue.as_deref_mut()) { + let busy = !queue.channel_free(); queue.recycle_killed()?; queue.recycle_quarantined_idle()?; if let Some(binding) = queue.detach_exited_owner() { deferred.defer_binding(binding); } + freed |= busy && queue.channel_free(); + releasing |= queue.pool_release_pending(); + } + freed |= self.queues.observe_pool_releases(&self.init, deferred)? != 0; + let shared = &self.primary.state.shared; + // Edge notifications cover the transitions this driver makes; every + // event pass also rechecks the level, so capacity that appeared through + // another engine (render QIDs, closed logical queues, render-held work + // states) still ends a scheduler wait that has no timeout of its own. + if freed + || (shared.channels.has_waiters() + && self.queues.compute_readiness(0, None, 0) == Readiness::Ready) + { + shared.channels.notify(); + } + // A pool release has no completion event: each observation pass is + // the progress a growing owner waits for. + if releasing || shared.progress.has_waiters() { + shared.progress.notify(); } - self.queues.observe_pool_releases(&self.init, deferred)?; if self.queues.compute.iter().flatten().filter_map(|entry| entry.queue.as_deref()) .any(|queue| queue.pool_release_stalled(now)) { return Err(ETIMEDOUT); @@ -2182,6 +2351,47 @@ impl Backend { } fn wait_compute_capacity(&self, admission: &Admission<'_>, reclaim_backing: bool) -> Result { + self.reclaim_capacity(admission, reclaim_backing)?; + kernel::time::delay::fsleep(kernel::time::Delta::from_millis(1)); + admission.check() + } + + /// Compute admission without a channel: give back what pressure allows, then block until a + /// channel is released after `observed` (sampled before the failed check). + fn wait_channels(&self, observed: u64, admission: &Admission<'_>, reclaim_backing: bool) -> Result { + self.reclaim_capacity(admission, reclaim_backing)?; + self.wait_release(&self.shared.channels, observed, admission) + } + + /// Blocks until `domain` is signalled after `observed`, this packet's admission ends, or one + /// admission slice passes: cancellation and logical close have no release to wait for. + fn wait_release( + &self, + domain: &super::availability::Availability, + observed: u64, + admission: &Admission<'_>, + ) -> Result { + use kernel::dma_fence::RawDmaFence; + let slice = admission.slice_ms()?; + if let Some(fence) = domain.wait(observed)? { + // SAFETY: The owned fence keeps the raw DMA fence alive throughout the wait. + let waited = unsafe { + kernel::bindings::dma_fence_wait_timeout( + fence.raw(), + false, + kernel::time::msecs_to_jiffies(slice) as isize, + ) + }; + if waited < 0 { + return Err(Error::from_errno(waited as i32)); + } + } + admission.check() + } + + /// Reclaim steps of a capacity wait that never sleep: observe consumed pool releases and, + /// under backing pressure, give idle pools back. + fn reclaim_capacity(&self, admission: &Admission<'_>, reclaim_backing: bool) -> Result { admission.check()?; let mut deferred = DeferredBatch::publication()?; let result: Result = (|| { @@ -2216,8 +2426,6 @@ impl Backend { Err(error) if error == EAGAIN => {} result => result?, } - admission.check()?; - kernel::time::delay::fsleep(kernel::time::Delta::from_millis(1)); admission.check() } } diff --git a/drivers/gpu/drm/asahi/g17/runtime/backend.rs b/drivers/gpu/drm/asahi/g17/runtime/backend.rs index b393afd9c8df2..39f628d007e18 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/backend.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/backend.rs @@ -55,6 +55,7 @@ impl Backend { drop(state); drop(binding); self.finish_uninstalled_compute(retired)?; + self.shared.channels.notify(); self.shared.queue_events(); return Ok(()); } @@ -92,6 +93,7 @@ impl Backend { // A builder may own only an off-lock reservation when close arrives. // Record intent before scanning queues; every later install/rearm checks it. self.closed.store(true, Ordering::Release); + self.hold_claim(None); let mut state = self.shared.state.lock(); let Some(firmware) = (*state).as_deref_mut() else { return Ok(()); @@ -106,6 +108,8 @@ impl Backend { firmware.queues.release_render(self.owner)?; firmware.queues.admission.release_owner(self.owner); drop(state); + // Its idle channel, or its slot and never-published QID, is free. + self.shared.channels.notify(); self.shared.queue_events(); Ok(()) } @@ -212,8 +216,12 @@ impl job::Backend for Backend { /// Firmware kick parents order the same engine. Explicit cross-engine and /// input dependencies were already attached to the DRM scheduled job. - fn prepare(&self, _packet: &Packet) -> Option { - None + /// Compute admission waits for its shared capacity here, as a fence. + fn prepare(&self, packet: &Arc) -> Option { + match packet.engine() { + Engine::Compute => self.prepare_compute(packet), + Engine::Render => None, + } } fn publish(&self, packet: Arc) -> Result { @@ -241,6 +249,9 @@ impl job::Backend for Backend { Engine::Render => self.render_packet(&packet, &mut deferred), } }; + if packet.engine() == Engine::Compute { + self.hold_claim(None); + } self.release_backlog(&packet); self.finish_publication(&packet, result, &mut deferred) } @@ -269,6 +280,9 @@ impl job::Backend for Backend { fn cancel(&self, packet: Arc) { self.release_backlog(&packet); + if packet.engine() == Engine::Compute { + self.hold_claim(None); + } if packet.request_cancel() { Deferred::Unpublished(packet.completion.clone(), ECANCELED).finish(); return; diff --git a/drivers/gpu/drm/asahi/g17/runtime/compute_replay.rs b/drivers/gpu/drm/asahi/g17/runtime/compute_replay.rs index 0a8ebc28c02e2..32b9acef94c0f 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/compute_replay.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/compute_replay.rs @@ -104,6 +104,8 @@ impl Firmware { } } self.queues.replay_outer_wait = None; + // Publication behind these replays may proceed. + self.primary.state.shared.progress.notify(); Ok(()) } } diff --git a/drivers/gpu/drm/asahi/g17/runtime/teardown.rs b/drivers/gpu/drm/asahi/g17/runtime/teardown.rs index aaa2e281c69ee..16bd5828265a4 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/teardown.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/teardown.rs @@ -190,10 +190,17 @@ impl Firmware { teardown.publish_ready(&mut service, recovery, super::now_ns())?; let consumer = self.init.control_consumer()?.load(Ordering::Relaxed); let producer = self.init.control_producer()?.load(Ordering::Relaxed); + let mut consumed = false; while teardown .take_consumed(consumer, producer, &mut service)? .is_some() - {} + { + consumed = true; + } + if consumed { + // A channel whose last scheduler release is consumed can change owner. + self.primary.state.shared.channels.notify(); + } Ok(()) } } From 327e50fc45b57acf1954e8c2d78977198286feb0 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Fri, 23 Oct 2026 22:52:55 -0400 Subject: [PATCH 026/388] drm/asahi: wait for a G17 compute source only through its completion record A consumer of a firmware-visible compute source carried a completion-wait record before its descriptor and also named the source in its kick entry. The second dependency flagged the command as entry-signalled, so it also appended a predecessor record, and the firmware resolved the same wait twice. Keep only the completion-wait record, which orders the consumer behind its source by itself. Queue-to-queue hop 49.6 -> 46.0 us. Cross-queue order and data checks (10k iterations each), dependent reads under pressure, the compute/sync CTS gate and mixed churn pass. (cherry picked from commit 58ef65a530b707fb77878e9fdc68d577a27759fc) --- drivers/gpu/drm/asahi/g17/runtime.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/asahi/g17/runtime.rs b/drivers/gpu/drm/asahi/g17/runtime.rs index f1cefb99fe005..2c0d91820c979 100644 --- a/drivers/gpu/drm/asahi/g17/runtime.rs +++ b/drivers/gpu/drm/asahi/g17/runtime.rs @@ -1461,7 +1461,9 @@ impl super::Firmware { let Some((sequence, dependency, stamp_va, event_slot, hold)) = found else { return Err(EAGAIN); }; - merged.insert(sequence, dependency)?; + // The completion-wait record orders the consumer behind its + // source; naming the source in the kick entry as well only adds + // a second, slower wait and a predecessor record. let wait = super::sync_input::NativeWait { sequence, dependency, stamp_va, event_slot }; if let Some(old) = native.iter_mut().find(|old| old.dependency.qid() == dependency.qid()) { From 6f4d3bbb47c1a7b19abef118b41b131440a8f414 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Fri, 23 Oct 2026 23:11:57 -0400 Subject: [PATCH 027/388] drm/asahi: add G17 command lifecycle tracepoints Add tracepoints for each command's publication (doorbell rung, with its out fence and the CPU counter), its retirement (with the firmware's GPU start and end timestamps), physical queue binds and event worker pass boundaries. With the counter and the GPU's system counter timestamps on the same events, a trace places firmware dispatch, GPU execution, firmware completion and every host stage of a command on one timeline. The events are disabled by default and cost a static-key test each. (cherry picked from commit 565dae906223948df97a252cc3274e8db6183363) --- drivers/gpu/drm/asahi/Makefile | 3 +- drivers/gpu/drm/asahi/g17/mod.rs | 1 + drivers/gpu/drm/asahi/g17/queue/compute.rs | 8 ++ drivers/gpu/drm/asahi/g17/queue/render.rs | 4 + drivers/gpu/drm/asahi/g17/runtime/events.rs | 3 + drivers/gpu/drm/asahi/g17/trace.c | 38 +++++++ drivers/gpu/drm/asahi/g17/trace.h | 114 ++++++++++++++++++++ drivers/gpu/drm/asahi/g17/trace.rs | 56 ++++++++++ 8 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 drivers/gpu/drm/asahi/g17/trace.c create mode 100644 drivers/gpu/drm/asahi/g17/trace.h create mode 100644 drivers/gpu/drm/asahi/g17/trace.rs diff --git a/drivers/gpu/drm/asahi/Makefile b/drivers/gpu/drm/asahi/Makefile index 8f8f679532d2b..9edbc6ba3b8d4 100644 --- a/drivers/gpu/drm/asahi/Makefile +++ b/drivers/gpu/drm/asahi/Makefile @@ -1,5 +1,6 @@ # SPDX-License-Identifier: GPL-2.0 obj-$(CONFIG_DRM_ASAHI) += asahi.o -asahi-y := asahi_core.o sysfs.o g17/sync_input.o +asahi-y := asahi_core.o sysfs.o g17/sync_input.o g17/trace.o +CFLAGS_g17/trace.o := -I$(src)/g17 asahi-$(CONFIG_DRM_ASAHI_G16) += g16/glue.o diff --git a/drivers/gpu/drm/asahi/g17/mod.rs b/drivers/gpu/drm/asahi/g17/mod.rs index 330f57ab59779..3d7f58ea08ecf 100644 --- a/drivers/gpu/drm/asahi/g17/mod.rs +++ b/drivers/gpu/drm/asahi/g17/mod.rs @@ -31,6 +31,7 @@ pub(crate) mod context; mod data_segment; mod dependency; mod sync_input; +mod trace; mod event; mod feed; mod fence; diff --git a/drivers/gpu/drm/asahi/g17/queue/compute.rs b/drivers/gpu/drm/asahi/g17/queue/compute.rs index da0123d8405df..801e69c97ef79 100644 --- a/drivers/gpu/drm/asahi/g17/queue/compute.rs +++ b/drivers/gpu/drm/asahi/g17/queue/compute.rs @@ -589,6 +589,7 @@ impl Queue { let binding = graph.bind(context.clone(), kick_alias, pool_alias)?; context.mark_published(); context.note_compute_binding(true); + crate::g17::trace::bind(crate::g17::trace::COMPUTE, kick.id().qid(), context.id(), 0); Ok(Self { owner: Some(owner), retained_owner: None, @@ -924,6 +925,10 @@ impl Queue { self.active.iter_mut().next().ok_or(EIO)?.qos_pending = false; } let front = self.active.front().ok_or(EIO)?; + if let Some(ticket) = front.ticket { + crate::g17::trace::complete(crate::g17::trace::COMPUTE, ticket.qid, + ticket.ordinal, &front.packet, [timestamps[0], timestamps[1]]); + } self.retire_pending = true; defer(Deferred::Retired( front.packet.completion.clone(), @@ -1316,6 +1321,8 @@ impl Queue { } context.mark_published(); context.note_compute_binding(true); + crate::g17::trace::bind(crate::g17::trace::COMPUTE, self.qid(), context.id(), + if same_vm { 2 } else { 1 }); self.owner = Some(owner); self.unpublished_binding = true; self.released = false; @@ -2151,6 +2158,7 @@ impl Queue { } fence(Ordering::SeqCst); host.notify((0x83 << 48) | 0x10)?; + crate::g17::trace::publish(crate::g17::trace::COMPUTE, qid, ordinal, &packet); host.note_submission() })(); if let Err(error) = result { diff --git a/drivers/gpu/drm/asahi/g17/queue/render.rs b/drivers/gpu/drm/asahi/g17/queue/render.rs index e063ef3a047fc..faf31514a2f2f 100644 --- a/drivers/gpu/drm/asahi/g17/queue/render.rs +++ b/drivers/gpu/drm/asahi/g17/queue/render.rs @@ -954,6 +954,8 @@ impl Pair { host.publish_retained_pair(priority, fragment_slot, tiling_slot, outer_next)?; self.active.back_mut().ok_or(EIO)?.outer_published = true; host.notify_render(priority)?; + crate::g17::trace::publish(crate::g17::trace::RENDER, self.qids()[1], self.ordinal as u32, + packet); host.note_submission() })(); if let Err(error) = publish { @@ -1236,6 +1238,8 @@ impl Pair { front.qos_completed = true; front.flist_completed = true; } + crate::g17::trace::complete(crate::g17::trace::RENDER, qids[1], ordinal as u32, + &front.packet, [timestamps[0], timestamps[3]]); defer(Deferred::Retired( front.packet.completion.clone(), Ok(timestamps), diff --git a/drivers/gpu/drm/asahi/g17/runtime/events.rs b/drivers/gpu/drm/asahi/g17/runtime/events.rs index e46c820efa155..b1e8f82d858f9 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/events.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/events.rs @@ -323,8 +323,10 @@ impl crate::g17::Shared { }; deferred }; + crate::g17::trace::event(0); let result = self.runtime_event_turn(&mut deferred); deferred.finish(); + crate::g17::trace::event(2); self.queue_feed(); if let Some(firmware) = (*self.state.lock()).as_deref_mut() { firmware.queues.worker_deferred = Some(deferred); @@ -352,6 +354,7 @@ impl crate::g17::Shared { (recover, masks) }; deferred.finish(); + crate::g17::trace::event(1); if recover { self.preparations.wait_drained_until(super::now_ns().saturating_add(super::WORK_RING_WAIT_NS))?; } diff --git a/drivers/gpu/drm/asahi/g17/trace.c b/drivers/gpu/drm/asahi/g17/trace.c new file mode 100644 index 0000000000000..feffb96315845 --- /dev/null +++ b/drivers/gpu/drm/asahi/g17/trace.c @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +#include +#include + +#define CREATE_TRACE_POINTS +#include "trace.h" + +bool asahi_g17_trace_publish_enabled(void) +{ + return trace_asahi_g17_publish_enabled(); +} + +void asahi_g17_trace_publish(u8 engine, u8 qid, u32 ordinal, u32 ctx, u64 seq, + struct dma_fence *fence) +{ + trace_asahi_g17_publish(engine, qid, ordinal, ctx, seq, + fence ? fence->context : 0, fence ? fence->seqno : 0, + __arch_counter_get_cntvct()); +} + +void asahi_g17_trace_complete(u8 engine, u8 qid, u32 ordinal, u32 ctx, u64 seq, + u64 gpu_start, u64 gpu_end) +{ + if (trace_asahi_g17_complete_enabled()) + trace_asahi_g17_complete(engine, qid, ordinal, ctx, seq, gpu_start, + gpu_end, __arch_counter_get_cntvct()); +} + +void asahi_g17_trace_bind(u8 engine, u8 qid, u32 ctx, u8 kind) +{ + trace_asahi_g17_bind(engine, qid, ctx, kind); +} + +void asahi_g17_trace_event(u8 phase) +{ + if (trace_asahi_g17_event_enabled()) + trace_asahi_g17_event(phase, __arch_counter_get_cntvct()); +} diff --git a/drivers/gpu/drm/asahi/g17/trace.h b/drivers/gpu/drm/asahi/g17/trace.h new file mode 100644 index 0000000000000..e1ef55715e312 --- /dev/null +++ b/drivers/gpu/drm/asahi/g17/trace.h @@ -0,0 +1,114 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ +/* + * Command lifecycle tracepoints of the G17 runtime. Times are joined by + * (ctx, seq); `counter` is the CPU's virtual counter and `gpu_start`/`gpu_end` + * come from the firmware's completion record, so a reader can place firmware + * dispatch and completion on the host timeline. + */ +#undef TRACE_SYSTEM +#define TRACE_SYSTEM asahi_g17 + +#if !defined(ASAHI_G17_TRACE_H) || defined(TRACE_HEADER_MULTI_READ) +#define ASAHI_G17_TRACE_H + +#include + +TRACE_EVENT(asahi_g17_publish, + TP_PROTO(u8 engine, u8 qid, u32 ordinal, u32 ctx, u64 seq, + u64 fence_context, u64 fence_seqno, u64 counter), + TP_ARGS(engine, qid, ordinal, ctx, seq, fence_context, fence_seqno, counter), + TP_STRUCT__entry( + __field(u8, engine) + __field(u8, qid) + __field(u32, ordinal) + __field(u32, ctx) + __field(u64, seq) + __field(u64, fence_context) + __field(u64, fence_seqno) + __field(u64, counter) + ), + TP_fast_assign( + __entry->engine = engine; + __entry->qid = qid; + __entry->ordinal = ordinal; + __entry->ctx = ctx; + __entry->seq = seq; + __entry->fence_context = fence_context; + __entry->fence_seqno = fence_seqno; + __entry->counter = counter; + ), + TP_printk("engine=%u qid=%u ordinal=%u ctx=%u seq=%llu fence=%llu:%llu counter=%llu", + __entry->engine, __entry->qid, __entry->ordinal, __entry->ctx, + __entry->seq, __entry->fence_context, __entry->fence_seqno, + __entry->counter) +); + +TRACE_EVENT(asahi_g17_complete, + TP_PROTO(u8 engine, u8 qid, u32 ordinal, u32 ctx, u64 seq, + u64 gpu_start, u64 gpu_end, u64 counter), + TP_ARGS(engine, qid, ordinal, ctx, seq, gpu_start, gpu_end, counter), + TP_STRUCT__entry( + __field(u8, engine) + __field(u8, qid) + __field(u32, ordinal) + __field(u32, ctx) + __field(u64, seq) + __field(u64, gpu_start) + __field(u64, gpu_end) + __field(u64, counter) + ), + TP_fast_assign( + __entry->engine = engine; + __entry->qid = qid; + __entry->ordinal = ordinal; + __entry->ctx = ctx; + __entry->seq = seq; + __entry->gpu_start = gpu_start; + __entry->gpu_end = gpu_end; + __entry->counter = counter; + ), + TP_printk("engine=%u qid=%u ordinal=%u ctx=%u seq=%llu gpu=%llu..%llu counter=%llu", + __entry->engine, __entry->qid, __entry->ordinal, __entry->ctx, + __entry->seq, __entry->gpu_start, __entry->gpu_end, __entry->counter) +); + +TRACE_EVENT(asahi_g17_bind, + TP_PROTO(u8 engine, u8 qid, u32 ctx, u8 kind), + TP_ARGS(engine, qid, ctx, kind), + TP_STRUCT__entry( + __field(u8, engine) + __field(u8, qid) + __field(u32, ctx) + __field(u8, kind) + ), + TP_fast_assign( + __entry->engine = engine; + __entry->qid = qid; + __entry->ctx = ctx; + __entry->kind = kind; + ), + TP_printk("engine=%u qid=%u ctx=%u kind=%u", + __entry->engine, __entry->qid, __entry->ctx, __entry->kind) +); + +TRACE_EVENT(asahi_g17_event, + TP_PROTO(u8 phase, u64 counter), + TP_ARGS(phase, counter), + TP_STRUCT__entry( + __field(u8, phase) + __field(u64, counter) + ), + TP_fast_assign( + __entry->phase = phase; + __entry->counter = counter; + ), + TP_printk("phase=%u counter=%llu", __entry->phase, __entry->counter) +); + +#endif + +#undef TRACE_INCLUDE_PATH +#define TRACE_INCLUDE_PATH . +#undef TRACE_INCLUDE_FILE +#define TRACE_INCLUDE_FILE trace +#include diff --git a/drivers/gpu/drm/asahi/g17/trace.rs b/drivers/gpu/drm/asahi/g17/trace.rs new file mode 100644 index 0000000000000..a616b68735225 --- /dev/null +++ b/drivers/gpu/drm/asahi/g17/trace.rs @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT + +//! Command lifecycle tracepoints (tracefs `events/asahi_g17`). Each call is a +//! static-key test in C while its event is disabled. + +use kernel::{bindings, dma_fence::RawDmaFence, prelude::*, sync::Arc}; + +unsafe extern "C" { + fn asahi_g17_trace_publish_enabled() -> bool; + fn asahi_g17_trace_publish(engine: u8, qid: u8, ordinal: u32, ctx: u32, seq: u64, + fence: *mut bindings::dma_fence); + fn asahi_g17_trace_complete(engine: u8, qid: u8, ordinal: u32, ctx: u32, seq: u64, + gpu_start: u64, gpu_end: u64); + fn asahi_g17_trace_bind(engine: u8, qid: u8, ctx: u32, kind: u8); + fn asahi_g17_trace_event(phase: u8); +} + +pub(super) const RENDER: u8 = 0; +pub(super) const COMPUTE: u8 = 1; + +/// Called once the command's doorbell has been rung. +pub(super) fn publish(engine: u8, qid: u8, ordinal: u32, packet: &Arc) { + // SAFETY: Pure static-key query. + if !unsafe { asahi_g17_trace_publish_enabled() } { + return; + } + let fence = packet.completion.fence(); + // SAFETY: `fence` holds a reference for the duration of the call. + unsafe { + asahi_g17_trace_publish(engine, qid, ordinal, packet.context.id(), packet.order.sequence, + fence.raw()) + }; +} + +/// Called when exact completion with GPU timestamps is retired. +pub(super) fn complete(engine: u8, qid: u8, ordinal: u32, packet: &super::job::Packet, + gpu: [u64; 2]) +{ + // SAFETY: Plain values only. + unsafe { + asahi_g17_trace_complete(engine, qid, ordinal, packet.context.id(), packet.order.sequence, + gpu[0], gpu[1]) + }; +} + +/// A physical queue was bound to a logical owner: 0 fresh, 1 rebound, 2 same VM. +pub(super) fn bind(engine: u8, qid: u8, ctx: u32, kind: u8) { + // SAFETY: Plain values only. + unsafe { asahi_g17_trace_bind(engine, qid, ctx, kind) }; +} + +/// Event worker pass boundaries: 0 begin, 1 fences signalled, 2 end. +pub(super) fn event(phase: u8) { + // SAFETY: Plain values only. + unsafe { asahi_g17_trace_event(phase) }; +} From e24e3bad9e7d3c6cf7079d683c9364d619cd3a55 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Fri, 23 Oct 2026 23:07:01 -0400 Subject: [PATCH 028/388] drm/asahi: ring the G17 compute pipe doorbell for every publication Compute rang its pipe and priority doorbell (pipe 2 in bits 1:0, class in bits 3:2) only for the first publication of a priority class, then the generic work doorbell for every command. Render rings its own pipe doorbell for every publication, and so does the M5 compute path. Ring the compute pipe doorbell for every publication instead of the generic one. gpubench on Neo (us/submit, on the integration branch): compute.light d2 60.3 -> 53.2, d64 22.8 -> 21.5, d1 163 -> 154..160, procs d16 unchanged (27.2), hop unchanged (48.7). Stress gate passes. (cherry picked from commit 398141cf95c44ac4d0073f3620a99d1235f31309) --- drivers/gpu/drm/asahi/g17/queue/compute.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/queue/compute.rs b/drivers/gpu/drm/asahi/g17/queue/compute.rs index 801e69c97ef79..4189f687b0694 100644 --- a/drivers/gpu/drm/asahi/g17/queue/compute.rs +++ b/drivers/gpu/drm/asahi/g17/queue/compute.rs @@ -2120,7 +2120,6 @@ impl Queue { self.outer_started & ring == 0, )?, )?; - let notify_activation = self.outer_started & ring == 0; self.outer_started |= ring; self.installed = true; self.published_config = Some(identity); @@ -2151,13 +2150,12 @@ impl Queue { previous.publication = Some((packet.order.sequence, epoch)); } } - if notify_activation { - fence(Ordering::SeqCst); - // Pipe 2 (compute) in bits 1:0, priority class in bits 3:2. - host.notify((0x83 << 48) | 0x02 | u64::from(policy.priority()) << 2)?; - } + // Every publication rings its own pipe and priority class, as render + // publication does: pipe 2 (compute) in bits 1:0, priority class in + // bits 3:2. The generic work doorbell left the firmware to find the + // ring itself and delayed dispatch. fence(Ordering::SeqCst); - host.notify((0x83 << 48) | 0x10)?; + host.notify((0x83 << 48) | 0x02 | u64::from(policy.priority()) << 2)?; crate::g17::trace::publish(crate::g17::trace::COMPUTE, qid, ordinal, &packet); host.note_submission() })(); From 94c66ecf24e6d931a434914d6a513ee36ff6ffd7 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Fri, 23 Oct 2026 23:38:41 -0400 Subject: [PATCH 029/388] drm/asahi: map the G17 compute queue graph cached for the firmware Render keeps its queue records in a firmware-cached prefix; the compute queue record, job-list head and item ring were mapped uncached. Allocate the compute graph from the retained object pool with its 32 KiB queue record, job-list head and item ring in a firmware-cached prefix, and move the producer/consumer words to their own uncached page after it, as native does. compute.light d64 22.2 -> 19.7 us/submit, 16-process compute 27.2 -> 25.7, queue-to-queue hop 47.5 -> 42.4 us. Compute/sync CTS gate, cross-queue order/data 10k, dependent reads under pressure and mixed churn pass. (cherry picked from commit 488708add18c39f709d6a9b53736d339428cf5d0) --- drivers/gpu/drm/asahi/g17/queue/compute.rs | 39 +++++++++++++++------- 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/queue/compute.rs b/drivers/gpu/drm/asahi/g17/queue/compute.rs index 4189f687b0694..6ca63595154de 100644 --- a/drivers/gpu/drm/asahi/g17/queue/compute.rs +++ b/drivers/gpu/drm/asahi/g17/queue/compute.rs @@ -34,6 +34,10 @@ const CONFIG: usize = 0; const ANNOUNCE: usize = 0x800; const PREDECESSOR: usize = 0x400; const POINTERS: usize = 0x100; +/// Producer/consumer words live on their own uncached page after the graph, +/// which the firmware reads through a cached mapping (queue record, job-list +/// head and item ring), as native does. +const CURSORS: usize = GRAPH_SIZE; const ITEMS: usize = 0x4000; const STAMP: usize = 0x300; @@ -120,7 +124,7 @@ struct Graph { usage: KernelObject, operand: KernelObject, support: KernelObject, - graph: KernelObject, + graph: PooledObject, context_low: KernelObject, context_high: KernelObject, /// Announcements, which stay uncached for the firmware. @@ -157,11 +161,18 @@ impl Graph { support.write(0x100, support_va + page + 4)?; support.write(0x110, 0x50u32)?; support.write(mmu::UAT_PGSZ + 4, 1u32)?; - let mut graph = alloc.kernel(GRAPH_SIZE, page, prot, cpu)?; + let mut graph = objects.allocate( + alloc, + Family::QueueState, + GRAPH_SIZE + mmu::UAT_PGSZ, + GRAPH_SIZE, + prot, + cpu, + )?; let graph_va = graph.gpu_va(); graph.initialize::(0, |view| { view.queue = QueueRecord::new(&QueueRecordArgs { - pointers_va: graph_va + POINTERS as u64, + pointers_va: graph_va + CURSORS as u64, items_va: graph_va + ITEMS as u64, job_list_va: context.work_head_va(), scheduler_va: context.scheduler_va(), @@ -172,6 +183,10 @@ impl Graph { view.jobs = JobListHead::new(graph_va + 0x200); Ok(()) })?; + graph.initialize::(CURSORS, |view| { + *view = PointerBlock::new(&pointers)?; + Ok(()) + })?; let context_low = alloc.lower(CONTEXT_SIZE, page, prot, cpu)?; let context_high = alloc.kernel(CONTEXT_SIZE, page, prot, cpu)?; let records = alloc.kernel(RECORD_SLOTS * RECORD_STRIDE, page, prot, cpu)?; @@ -259,9 +274,9 @@ impl Graph { fn cursors(&self) -> Result { Ok(Cursors { - producer: self.graph.word(POINTERS + self.pointers.producer)?.load(Ordering::Relaxed), - consumer: self.graph.word(POINTERS)?.load(Ordering::Relaxed), - count: self.graph.word(POINTERS + 0x60)?.load(Ordering::Relaxed), + producer: self.graph.word(CURSORS + self.pointers.producer)?.load(Ordering::Relaxed), + consumer: self.graph.word(CURSORS)?.load(Ordering::Relaxed), + count: self.graph.word(CURSORS + 0x60)?.load(Ordering::Relaxed), }) } fn stamp(&self) -> Result { @@ -315,7 +330,7 @@ impl Graph { } fence(Ordering::SeqCst); self.graph - .word(POINTERS + self.pointers.producer)? + .word(CURSORS + self.pointers.producer)? .store(cursors.consumer, Ordering::Relaxed); self.reclaimed = cursors.consumer; fence(Ordering::SeqCst); @@ -1689,11 +1704,11 @@ impl Queue { let producer = self .graph .graph - .word(POINTERS + self.graph.pointers.producer)? + .word(CURSORS + self.graph.pointers.producer)? .load(Ordering::Relaxed); self.graph .graph - .word(POINTERS + target)? + .word(CURSORS + target)? .store(producer, Ordering::Relaxed); Ok(()) } @@ -1764,7 +1779,7 @@ impl Graph { }; (object, slot * RECORD_STRIDE + offset) } else { - (&mut self.graph, 0x6800 + offset) + (&mut *self.graph, 0x6800 + offset) }; if core::mem::size_of::() > window { return Err(EINVAL); @@ -1790,7 +1805,7 @@ impl Graph { return Err(EBUSY); } let slot = self.graph.dword(ITEMS + expected as usize * 8)?; - let producer = self.graph.word(POINTERS + self.pointers.producer)?; + let producer = self.graph.word(CURSORS + self.pointers.producer)?; if slot.load(Ordering::Relaxed) != 0 { return Err(EBUSY); } @@ -2262,7 +2277,7 @@ impl Graph { let first = self.graph.dword(ITEMS)?; let second = self.graph.dword(ITEMS + 8)?; let third = self.graph.dword(ITEMS + 16)?; - let producer = self.graph.word(POINTERS + self.pointers.producer)?; + let producer = self.graph.word(CURSORS + self.pointers.producer)?; first.store(address, Ordering::Relaxed); second.store(0, Ordering::Relaxed); third.store(0, Ordering::Relaxed); From db365ecc299f0663db8a629e70dfdb4d117eb95e Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Fri, 23 Oct 2026 23:28:59 -0400 Subject: [PATCH 030/388] drm/asahi: wait for G17 render capacity on the release that frees it Render admission and publication still slept or polled: a 20 us..1 ms backoff when every work state of the context was taken, up to 200 x 1 ms while recovery was observed, an unconditional 1 ms sleep for render capacity (through the compute capacity wait), and a 1 ms condvar timeout as the wakeup for a pair on loan, firmware room or a pending teardown. Block on the event that ends each wait instead: - a render availability domain, signalled by every admission-pool return (lease drop, owner close, quarantine change, exit-kill hold release, processor stop), every pair return from a loan, and on each event pass while a render publisher waits, since firmware ring room, consumed releases and teardown steps have no event of their own; - a per-context work-state domain, signalled whenever a work state returns; - the preparation gate: a publisher that observed recovery waits for its closure (close() now wakes the gate's waiters) and then blocks in enter() until recovery reopens it. Render capacity first returns idle backing, then waits on the domain. Waiters sample a domain only after a failed check and retry once before blocking, so steady-state publication takes no extra lock. Deadlines only bound the waits; 10 ms slices only bound cancellation checks. Steady state is unchanged (render d1 135.7, d32 41.4, queues d8 62.6 us/submit); the mild stress gate passes. (cherry picked from commit a3fc054bc52c2bc82350e6073366045a1c3c9489) --- drivers/gpu/drm/asahi/g17/context.rs | 27 ++- drivers/gpu/drm/asahi/g17/mod.rs | 18 +- drivers/gpu/drm/asahi/g17/preparation.rs | 31 +++ drivers/gpu/drm/asahi/g17/queue/admission.rs | 24 ++- drivers/gpu/drm/asahi/g17/runtime.rs | 16 +- drivers/gpu/drm/asahi/g17/runtime/events.rs | 5 + .../drm/asahi/g17/runtime/render/submit.rs | 192 +++++++++++++----- 7 files changed, 236 insertions(+), 77 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/context.rs b/drivers/gpu/drm/asahi/g17/context.rs index 564d761d37f97..0d3948a2b69ff 100644 --- a/drivers/gpu/drm/asahi/g17/context.rs +++ b/drivers/gpu/drm/asahi/g17/context.rs @@ -122,6 +122,8 @@ pub(crate) struct Context { qos: QosLease, owner_pid: u32, states: WorkStates, + /// Signalled when a work state returns; publishers that found none wait on it. + state_returns: Arc, work: Arc>, work_base: u64, render: Arc>>>, @@ -140,6 +142,7 @@ impl Context { execution: Arc, owner_pid: u32, policy: Policy, + state_returns: Arc, ) -> Result { let status = execution.vm().status().ok_or(EINVAL)?.clone(); let qos = qos.acquire()?; @@ -199,6 +202,7 @@ impl Context { qos, owner_pid, states: WorkStates::new(), + state_returns, work, work_base, render, @@ -378,8 +382,9 @@ impl WorkStateLease { Err(EBUSY) }; }; - if let Err(error) = self.context.work.lock().initialize(candidate) { - self.context.states.release(candidate); + let initialized = self.context.work.lock().initialize(candidate); + if let Err(error) = initialized { + self.context.release_state(candidate); return Err(error); } match self @@ -388,11 +393,15 @@ impl WorkStateLease { { Ok(_) => Ok(candidate), Err(installed) => { - self.context.states.release(candidate); + self.context.release_state(candidate); Ok(installed) } } } + /// The domain a publisher samples before acquire() and waits on after EBUSY. + pub(crate) fn returns(&self) -> &Arc { + &self.context.state_returns + } pub(crate) fn word(&self) -> u32 { self.word.load(Ordering::Acquire) } @@ -433,7 +442,17 @@ impl Drop for WorkStateLease { fn drop(&mut self) { let word = self.word(); if word != FREE { - self.context.states.release(word); + self.context.release_state(word); + } + } +} + +impl Context { + /// Every return bumps the domain: a publisher that sampled it before its + /// failed acquire() then never sleeps through this node. + fn release_state(&self, word: u32) { + if self.states.release(word) { + self.state_returns.notify(); } } } diff --git a/drivers/gpu/drm/asahi/g17/mod.rs b/drivers/gpu/drm/asahi/g17/mod.rs index 3d7f58ea08ecf..5e4c0dcc756cb 100644 --- a/drivers/gpu/drm/asahi/g17/mod.rs +++ b/drivers/gpu/drm/asahi/g17/mod.rs @@ -62,7 +62,6 @@ use core::ops::Range; use core::sync::atomic::{ AtomicBool, AtomicU32, - AtomicU64, Ordering, // }; @@ -844,6 +843,11 @@ struct Shared { progress: Arc, /// Logical-queue creation waiting for a returned queue slot or execution context. queues: Arc, + /// Render publication waiting for render capacity, a pair on loan, a work + /// state or firmware room: signalled when a pair, lease or work state + /// returns, an owner closes, a teardown parks, and on every event pass + /// while a render publisher waits (firmware consumption has no event). + renders: Arc, /// Unbound compute publishers which can reuse a retired channel. compute_demand: AtomicU32, #[pin] @@ -868,9 +872,6 @@ struct Shared { state: Mutex>>, #[pin] changed: CondVar, - /// Counts `changed` notifications, so that a waiter which checked under - /// the device mutex can tell whether it already missed one. - changes: AtomicU64, /// The port of the session in `state`; null before boot and once detachment begins. port: core::sync::atomic::AtomicPtr, } @@ -964,13 +965,10 @@ impl Shared { /// Wakes the waiters for a returned pair, binding or capacity change. fn note_changed(&self) { - self.changes.fetch_add(1, Ordering::AcqRel); self.changed.notify_all(); + self.renders.notify(); } - fn changes(&self) -> u64 { - self.changes.load(Ordering::Acquire) - } /// The session's port, valid while `lease` is held: shutdown and device loss join every /// lease before they detach the session. @@ -1130,6 +1128,7 @@ impl Gpu { let progress = availability::Availability::new()?; let queues = availability::Availability::new()?; let preparations = preparation::Gate::with_waiters(Some(progress.clone()))?; + let renders = availability::Availability::new()?; let reclaim = runtime::teardown::ReclaimBatch::new()?; let feed = feed::Feed::new()?; let active_cores = id.core_masks.iter().map(|mask| mask.count_ones()).sum(); @@ -1163,6 +1162,7 @@ impl Gpu { channels, progress, queues, + renders, compute_demand: AtomicU32::new(0), work <- new_work!("g17::Shared::work"), poll <- new_delayed_work!("g17::Shared::poll"), @@ -1173,7 +1173,6 @@ impl Gpu { stop <- new_mutex!((), "g17::Shared::stop"), state <- new_mutex!(None, "g17::Shared::state"), changed <- new_condvar!("g17::Shared::changed"), - changes: AtomicU64::new(0), port: core::sync::atomic::AtomicPtr::new(core::ptr::null_mut()), }), GFP_KERNEL, @@ -1325,6 +1324,7 @@ impl Gpu { shared.descriptor_flags, shared.variant, Some(shared.queues.clone()), + Some(shared.renders.clone()), ), pool: object::Pool::new()?, render_rings: kick::RenderRings::new()?, diff --git a/drivers/gpu/drm/asahi/g17/preparation.rs b/drivers/gpu/drm/asahi/g17/preparation.rs index 0df2c5ae2ebb2..3cafecf5d8206 100644 --- a/drivers/gpu/drm/asahi/g17/preparation.rs +++ b/drivers/gpu/drm/asahi/g17/preparation.rs @@ -179,8 +179,39 @@ impl Gate { } } + /// Also wakes publishers in wait_closure(); they then block in enter(). pub(super) fn close(&self) { self.invalidate(0); + let _wait = self.wait.lock(); + self.changed.notify_all(); + } + + /// Opaque snapshot for wait_closure(), taken where recovery was observed. + pub(super) fn snapshot(&self) -> u64 { + self.state.load(Ordering::Acquire) + } + + /// A publisher that observed recovery (host pending, or the firmware's state + /// ahead of the event worker, which always follows with a restart and the + /// closure) sleeps until this gate has closed since `seen`. enter() then + /// blocks until recovery reopens it. The deadline is only a failure bound. + pub(super) fn wait_closure(&self, seen: u64, deadline: u64) -> Result { + let mut wait = self.wait.lock(); + loop { + let state = self.state.load(Ordering::Acquire); + if state & (REMOVED | FAILED) != 0 { + return Err(ENODEV); + } + if state & CLOSED != 0 || state & !COUNT_MASK != seen & !COUNT_MASK { + return Ok(()); + } + let remaining = deadline.saturating_sub(super::now_ns()); + if remaining == 0 { + return Err(ETIMEDOUT); + } + let ms = u32::try_from(remaining.div_ceil(1_000_000)).unwrap_or(u32::MAX); + let _ = self.changed.wait_timeout(&mut wait, kernel::time::msecs_to_jiffies(ms)); + } } pub(super) fn remove(&self) { diff --git a/drivers/gpu/drm/asahi/g17/queue/admission.rs b/drivers/gpu/drm/asahi/g17/queue/admission.rs index ff2b294b6f35c..424bb1b7601f5 100644 --- a/drivers/gpu/drm/asahi/g17/queue/admission.rs +++ b/drivers/gpu/drm/asahi/g17/queue/admission.rs @@ -379,15 +379,19 @@ pub(crate) struct Pool { max_queues: u32, /// Logical-queue creation waiting for a returned slot. waiters: Option>, + /// Render publication waiting for render capacity: woken with every + /// release event above. + renders: Option>, } impl Pool { #[cfg(CONFIG_KUNIT)] pub(crate) fn new(max_queues: u32) -> Result> { - Self::with_waiters(max_queues, None) + Self::with_waiters(max_queues, None, None) } pub(crate) fn with_waiters( max_queues: u32, waiters: Option>, + renders: Option>, ) -> Result> { if max_queues > MAX_EXECUTION_CONTEXTS { return Err(EINVAL); @@ -399,10 +403,18 @@ impl Pool { queues: AtomicU32::new(0), max_queues, waiters, + renders, }), GFP_KERNEL, ) } + /// Every pair or lease return, owner close and quarantine change. + fn returned(&self) { + self.released.notify_all(); + if let Some(renders) = &self.renders { + renders.notify(); + } + } /// Logical queues currently counted, including one being created. pub(crate) fn queues(&self) -> u32 { self.queues.load(Ordering::Acquire) @@ -579,7 +591,7 @@ impl Pool { } } drop(state); - self.released.notify_all(); + self.returned(); } /// Active leases survive this transition. Their later drop is still the /// event which releases capacity, so a stopped owner is never double-freed. @@ -597,7 +609,7 @@ impl Pool { state.spared &= !released; state.kill_hold = 0; drop(state); - self.released.notify_all(); + self.returned(); } } @@ -715,7 +727,7 @@ impl RenderLease { } state.quarantined |= bit; drop(state); - self.pool.released.notify_all(); + self.pool.returned(); } /// The VM pin must be retired first. Keep this lease until after clearing /// quarantine; its drop is what permits waiting publications to proceed. @@ -800,7 +812,7 @@ impl Drop for RenderLease { } state.release(self.slot); drop(state); - self.pool.released.notify_all(); + self.pool.returned(); } } @@ -839,7 +851,7 @@ impl Pool { return; } self.state.lock().kill_hold &= !slots; - self.released.notify_all(); + self.returned(); } } diff --git a/drivers/gpu/drm/asahi/g17/runtime.rs b/drivers/gpu/drm/asahi/g17/runtime.rs index 2c0d91820c979..1bd500bae6cb0 100644 --- a/drivers/gpu/drm/asahi/g17/runtime.rs +++ b/drivers/gpu/drm/asahi/g17/runtime.rs @@ -192,16 +192,18 @@ impl Registry { descriptor_flags: [u32; 2], variant: &'static crate::g17::fw::Variant, ) -> impl Init { - Self::with_waiters(clusters, deflake, descriptor_flags, variant, None) + Self::with_waiters(clusters, deflake, descriptor_flags, variant, None, None) } - /// `queues` wakes logical-queue creation when a logical queue slot is returned. + /// `queues` wakes logical-queue creation when a logical queue slot is returned; + /// `renders` wakes render publication when a render pair or lease returns. pub(super) fn with_waiters( clusters: u32, deflake: crate::g17::fw::render::Deflake, descriptor_flags: [u32; 2], variant: &'static crate::g17::fw::Variant, queues: Option>, + renders: Option>, ) -> impl Init { kernel::try_init!(Self { qids <- kick::QueueIds::new(variant.qid_floor).chain(|qids| { @@ -210,7 +212,7 @@ impl Registry { buffers <- BufferIds::new(), metrics: Arc::new(MetricsIds::new(), GFP_KERNEL)?, qos_ids: QosIds::new()?, - admission: admission::Pool::with_waiters(kick::queue_limit(variant.qid_floor), queues)?, + admission: admission::Pool::with_waiters(kick::queue_limit(variant.qid_floor), queues, renders)?, render: render::RenderState::new(clusters, deflake, descriptor_flags)?, identities: queue::Identities::new(), compute: { @@ -735,7 +737,7 @@ impl Backend { let owner_pid = u32::try_from(kernel::current!().group_leader().pid()).map_err(|_| EOVERFLOW)?; let context = Arc::new( - Context::new(&alloc, &pool, &qos, execution, owner_pid, policy)?, + Context::new(&alloc, &pool, &qos, execution, owner_pid, policy, shared.renders.clone())?, GFP_KERNEL, )?; let state = shared.state.lock(); @@ -2346,12 +2348,6 @@ impl Registry { } impl Backend { - pub(super) fn wait_capacity(&self, started: u64) -> Result { - let check = || self.admission_open(); - let admission = Admission::new(started.saturating_add(WORK_RING_WAIT_NS), &check); - self.wait_compute_capacity(&admission, true) - } - fn wait_compute_capacity(&self, admission: &Admission<'_>, reclaim_backing: bool) -> Result { self.reclaim_capacity(admission, reclaim_backing)?; kernel::time::delay::fsleep(kernel::time::Delta::from_millis(1)); diff --git a/drivers/gpu/drm/asahi/g17/runtime/events.rs b/drivers/gpu/drm/asahi/g17/runtime/events.rs index b1e8f82d858f9..1797ca5c0ce94 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/events.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/events.rs @@ -467,6 +467,11 @@ impl crate::g17::Shared { if grow_ready { self.queue_grow(); } + // Firmware ring room, consumed releases and teardown steps have no + // event of their own: recheck render publishers' conditions every pass. + if self.renders.has_waiters() { + self.renders.notify(); + } Ok(()) } } diff --git a/drivers/gpu/drm/asahi/g17/runtime/render/submit.rs b/drivers/gpu/drm/asahi/g17/runtime/render/submit.rs index 663e62e8f86fc..bd83c81b1d593 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render/submit.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render/submit.rs @@ -8,11 +8,7 @@ use crate::g17::{ queue::admission::{RenderLease, Request}, }; use core::sync::atomic::Ordering; -use kernel::{ - prelude::*, - sync::Arc, - time::{delay::fsleep, Delta}, -}; +use kernel::{prelude::*, sync::Arc}; /// Internal result of a publication attempted from the submitting ioctl that /// met contention before anything became visible: the scheduler publishes the @@ -60,6 +56,9 @@ impl Backend { }; let started = super::super::now_ns(); let deadline_ns = started.saturating_add(u64::from(crate::g17::job::TIMEOUT_MS) * 1_000_000); + // A capacity refusal samples the render domain and retries once; only + // a refusal after that sample waits, so no release can be missed. + let mut observed = None; loop { self.admission_open()?; if super::super::now_ns() >= deadline_ns { return Err(EBUSY); } @@ -97,7 +96,7 @@ impl Backend { .reclaim_unused_render_spare(Some(self.owner), false)? }; if reclaimed.is_none() { - self.wait_render_capacity(started, sleep)?; + self.wait_render_capacity(&mut observed, started, sleep)?; } drop(reclaimed); continue; @@ -131,7 +130,7 @@ impl Backend { let ready = firmware.queues.has_render_ids(self.owner); drop(state); if reclaimed.is_none() && !ready { - self.wait_render_capacity(started, sleep)?; + self.wait_render_capacity(&mut observed, started, sleep)?; } drop(reclaimed); } @@ -140,36 +139,129 @@ impl Backend { } } - /// A pair is borrowed, still settling, or waiting for the release of the - /// scheduler it last ran for to be consumed and the pair parked. The event - /// worker observes both, so kick it on every poll; time out only after the - /// same bound as a capacity wait. - fn wait_render_busy(&self, since: &mut Option, seen: u64, sleep: bool) -> Result { + /// Conditions which end a publication that has not become visible yet. + fn publication_open(&self, packet: &Packet) -> Result { + self.admission_open()?; + if packet.cancel_requested() { + return Err(ECANCELED); + } + if packet.completion.status().get() != 0 { + return Err(EIO); + } + Ok(()) + } + + /// Blocks on `domain` until a notification after `observed`, or the + /// deadline, which only bounds the wait. Slices of at most 10 ms let the + /// publisher observe cancellation, which has no notification here. + fn wait_domain( + &self, + domain: &crate::g17::availability::Availability, + observed: u64, + deadline_ns: u64, + packet: &Packet, + ) -> Result { + self.publication_open(packet)?; + let remaining = deadline_ns.saturating_sub(super::super::now_ns()); + if remaining == 0 { + return Err(ETIMEDOUT); + } + domain.block(observed, remaining.div_ceil(1_000_000).min(10) as u32, false) + } + + /// A pair is borrowed, still settling, waiting for firmware room or for the + /// release of the scheduler it last ran for. Every pair return, retirement + /// and teardown step signals the render domain, and each event pass does + /// while a publisher waits; the deadline is only the failure bound. + fn wait_render_busy( + &self, + since: &mut Option, + observed: &mut Option, + sleep: bool, + packet: &Packet, + ) -> Result { if !sleep { return Err(FALLBACK); } let now = super::super::now_ns(); - if now.saturating_sub(*since.get_or_insert(now)) >= super::super::WORK_RING_WAIT_NS { - return Err(ETIMEDOUT); - } + let deadline = since.get_or_insert(now).saturating_add(super::super::WORK_RING_WAIT_NS); + // Sample, then retry once: only a refusal after the sample waits. + let Some(sample) = observed.take() else { + *observed = Some(self.shared.renders.observe()); + return Ok(()); + }; self.shared.queue_events(); - // Every pair return notifies under the device mutex: wake on it rather - // than sleeping out a fixed interval behind a short observation loan. - let mut state = self.shared.state.lock(); - if self.shared.changes() == seen { - let _ = self.shared.changed.wait_timeout(&mut state, kernel::time::msecs_to_jiffies(1)); - } - Ok(()) + self.wait_domain(&self.shared.renders, sample, deadline, packet) } - /// A closing owner may still hold its pair until exact retirement. Wait - /// outside both the device mutex and preparation gate so the event worker - /// and recovery can return capacity. No packet is firmware-visible here. - fn wait_render_capacity(&self, started: u64, sleep: bool) -> Result { + /// Render capacity is short: return what idle owners hold, then block until + /// a pair, lease or owner returns. No packet is firmware-visible here, and + /// neither the device mutex nor a preparation lease is held while waiting. + fn wait_render_capacity(&self, observed: &mut Option, started: u64, sleep: bool) -> Result { if !sleep { return Err(FALLBACK); } - self.wait_capacity(started) + if self.reclaim_render_capacity()? { + *observed = None; + return Ok(()); + } + let Some(sample) = observed.take() else { + *observed = Some(self.shared.renders.observe()); + return Ok(()); + }; + self.shared.queue_events(); + self.admission_open()?; + let remaining = started + .saturating_add(super::super::WORK_RING_WAIT_NS) + .saturating_sub(super::super::now_ns()); + if remaining == 0 { + return Err(EBUSY); + } + self.shared + .renders + .block(sample, remaining.div_ceil(1_000_000).min(10) as u32, false) + } + + /// Gives idle render backing back under capacity pressure. Returns whether + /// anything was returned, in which case the caller retries at once. + fn reclaim_render_capacity(&self) -> Result { + let mut deferred = DeferredBatch::publication()?; + let result: Result = (|| { + let lease = self.shared.preparations.try_enter()?; + let mut state = self.shared.state.lock(); + let firmware = (*state).as_deref_mut().ok_or(ENODEV)?; + if !lease.is_current() || self.shared.crashed.load(Ordering::Acquire) { + return Err(EAGAIN); + } + if firmware.recovery.pending() + || crate::g17::recovery::Memory::recovery_state(&firmware.init)? != 0 + { + return Ok(false); + } + firmware.queues.observe_pool_releases(&firmware.init, &mut deferred)?; + firmware.queues.reclaim_idle_render_pools(&firmware.init, &mut deferred)?; + let mut published = 0; + if !firmware.render_control_backpressured() { + (published, _) = firmware.queues.vacate_idle_pools( + &firmware.init, + 0, + super::super::now_ns(), + 0, + )?; + if published != 0 { + firmware.primary.notify(crate::g17::MSG_CONTROL_NOTIFY)?; + } + } + Ok(published != 0 + || !deferred.render.is_empty() + || !deferred.vacated.is_empty() + || !deferred.bindings.is_empty()) + })(); + deferred.finish(); + match result { + Err(error) if error == EAGAIN => Ok(false), + result => result, + } } fn render_with_lease( @@ -193,13 +285,12 @@ impl Backend { let admission = self.acquire_render_with_ids(packet, early, sleep)?; let slot = admission.slot(); packet.completion.attach_render_lease(admission)?; - let mut state_polls = 0; - let mut state_delay = 20i64; let mut busy_since = None; - let mut recovery_polls = 0; let capacity_started = super::super::now_ns(); + let deadline_ns = capacity_started.saturating_add(super::super::WORK_RING_WAIT_NS); + let mut seen = None; + let mut states = None; loop { - let seen = self.shared.changes(); if packet.cancel_requested() { return Err(ECANCELED); } @@ -229,12 +320,15 @@ impl Backend { if !sleep { return Err(FALLBACK); } - if state_polls == 1000 { - return Err(ETIMEDOUT); + // Every node of this context belongs to unretired work: + // sample, retry once, then wait for the next returned one. + let work = packet.completion.work_state()?; + match states.take() { + None => states = Some(work.returns().observe()), + Some(sample) => { + self.wait_domain(work.returns(), sample, deadline_ns, packet)? + } } - state_polls += 1; - fsleep(Delta::from_micros(state_delay)); - state_delay = (state_delay * 2).min(1000); continue; } result => { @@ -243,40 +337,42 @@ impl Backend { } match self.ensure_render(packet, slot, sleep) { Err(error) if error == ENOSPC => { - self.wait_render_capacity(capacity_started, sleep)?; + self.wait_render_capacity(&mut seen, capacity_started, sleep)?; continue; } Err(error) if error == EAGAIN => continue, Err(error) if error == EBUSY => { - self.wait_render_busy(&mut busy_since, seen, sleep)?; + self.wait_render_busy(&mut busy_since, &mut seen, sleep, packet)?; continue; } result => result?, } match self.prepare_render(slot, packet, deferred, sleep) { Err(error) if !packet.is_published() && error == ENOSPC => { - self.wait_render_capacity(capacity_started, sleep)?; + self.wait_render_capacity(&mut seen, capacity_started, sleep)?; continue; } Err(error) if !packet.is_published() && [EAGAIN, EBUSY].contains(&error) => { - let recovering = { + let (recovering, gate) = { let state = self.shared.state.lock(); let firmware = (*state).as_deref().ok_or(ENODEV)?; - firmware.recovery.pending() - || crate::g17::recovery::Memory::recovery_state(&firmware.init)? != 0 + ( + firmware.recovery.pending() + || crate::g17::recovery::Memory::recovery_state(&firmware.init)? != 0, + self.shared.preparations.snapshot(), + ) }; if recovering { if !sleep { return Err(FALLBACK); } - if recovery_polls == super::super::RECOVERY_SETTLE_POLLS { - return Err(ETIMEDOUT); - } - recovery_polls += 1; + // Recovery closes the preparation gate before touching + // any queue; the next pass blocks in enter() until it + // reopens. The worker raises the closure. self.shared.queue_events(); - fsleep(Delta::from_millis(1)); + self.shared.preparations.wait_closure(gate, deadline_ns)?; } else if error == EBUSY { - self.wait_render_busy(&mut busy_since, seen, sleep)?; + self.wait_render_busy(&mut busy_since, &mut seen, sleep, packet)?; } else if !sleep { return Err(FALLBACK); } From 06bc290c266abcb8b0d45f75c3369e231ae8c4a7 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Fri, 23 Oct 2026 22:34:06 -0400 Subject: [PATCH 031/388] drm/asahi: give the G17 render scheduler the pair's depth as credits A logical render queue owns exactly one physical pair, whose active ring holds RENDER_DEPTH (16) commands, but its scheduler had 32 credits. Jobs 17 to 32 entered run_job and parked there in the admission pool (or in the 1 ms busy wait) until a retirement freed a ring entry, holding the scheduler worker. With the credit limit equal to the pair depth the scheduler itself waits for the credit that the retirement returns, and run_job never blocks on a full pair. render.light d32 41.15 -> 41.26 us/submit (unchanged; the row is firmware bound), all other rows unchanged, mild stress gate passes. (cherry picked from commit 15de925f91cdb2a875e09fc1c6b64af79fa37ae2) --- drivers/gpu/drm/asahi/g17/job.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/asahi/g17/job.rs b/drivers/gpu/drm/asahi/g17/job.rs index ae401b06f2eb3..572cfd5d0f382 100644 --- a/drivers/gpu/drm/asahi/g17/job.rs +++ b/drivers/gpu/drm/asahi/g17/job.rs @@ -14,7 +14,10 @@ use kernel::{ }; pub(super) const TIMEOUT_MS: kernel::time::Msecs = super::timeout::POLL_MS; -const RENDER_CREDITS: u32 = 32; +/// One logical render queue owns one physical pair, whose active ring holds +/// `RENDER_DEPTH` commands. A deeper scheduler would only park run_job in +/// admission until a retirement; the scheduler waits for the credit instead. +const RENDER_CREDITS: u32 = crate::hw::t8140::queues::RENDER_DEPTH as u32; const COMPUTE_CREDITS: u32 = 16; #[derive(Clone, Copy, PartialEq, Eq)] From 3d1f2047766c2d6e8d165a4af2cfe4adc0edd366 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sat, 24 Oct 2026 00:03:31 -0400 Subject: [PATCH 032/388] drm/asahi: keep a G17 render free list registered while the firmware is busy A render pool whose last render retired sent its final FreeListRelease right away: a control-ring record and a control doorbell for every render of a one-deep queue. The firmware processes each such message serially before it returns to the work rings, so with several queues active the releases stretched every completion burst (one release per render in render.light queues: about 44.6k per run). When another pair still has published work, leave a registration whose references drop to zero lingering instead; the owner's next render reuses it like an overlapping render, with no release and no new generation. A lingering registration ends, and its release is published, when its owner closes or moves to another pool (each event pass checks) or after the 16 ms firmware idle hold, for which the event worker schedules a timed pass. On an otherwise idle device the release is still sent at once: reusing a registration that went idle costs the firmware more than the release does (render d1 147 -> 184 us when every idle registration lingered). render.light queues d2/d4/d8: 80.8/67.3/62.6 -> 76.9/62.4/58.2 us/submit; render d1 148, d32 41.3 (unchanged). Releases per queues-d8 run: 44574 -> 18 (measured with every idle registration lingering). Mild stress gate passes. --- drivers/gpu/drm/asahi/g17/freelist.rs | 37 +++++++++++++++++-- drivers/gpu/drm/asahi/g17/runtime/events.rs | 18 ++++++--- drivers/gpu/drm/asahi/g17/runtime/render.rs | 35 +++++++++++++++++- .../gpu/drm/asahi/g17/runtime/render/pools.rs | 34 +++++++++++++++-- 4 files changed, 111 insertions(+), 13 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/freelist.rs b/drivers/gpu/drm/asahi/g17/freelist.rs index 3b31c3cead064..9015dc82e4076 100644 --- a/drivers/gpu/drm/asahi/g17/freelist.rs +++ b/drivers/gpu/drm/asahi/g17/freelist.rs @@ -670,10 +670,18 @@ pub(crate) struct Owner { enum LeasePhase { Preparing, Ready, + /// No reference since the given time. The registration stays live so that + /// the owner's next render reuses it without a control-ring release; the + /// release becomes pending when the linger ends. + Lingering(u64), ReleaseSent(u32), Released, } +/// How long an idle registration of an open owner may stay live: the +/// firmware's effort idle hold. +pub(crate) const LINGER_NS: u64 = 16_000_000; + /// References to one prepopulated owner: preparation and two engine commands /// per render. Published releases do not gate reacquisition; their consumption /// witnesses are carried into the successor. @@ -707,10 +715,14 @@ impl Lease { } let generation = previous.generation.checked_add(1).ok_or(EOVERFLOW)?; let same_owner = previous.owner == owner; + // A lingering registration was never released: it continues like an + // overlap of the owner's previous render. let overlapping = same_owner - && previous.phase == LeasePhase::Ready - && previous.references >= 2 - && previous.commands >= 2; + && match previous.phase { + LeasePhase::Ready => previous.references >= 2 && previous.commands >= 2, + LeasePhase::Lingering(_) => true, + _ => false, + }; let reacquire = previous.reacquirable(); if !overlapping && !reacquire { return Err(EBUSY); @@ -772,16 +784,35 @@ impl Lease { Err(EIO) } /// Both engines have retired this render's exact retained submission. + /// With `linger`, a registration left without references stays live + /// instead of making its final release pending. pub(crate) fn complete_render( &mut self, + linger: bool, ) -> Result> { if self.phase != LeasePhase::Ready || self.references < 2 || self.commands < 2 { return Err(EINVAL); } self.references -= 2; self.commands -= 2; + if self.references == 0 && linger { + self.phase = LeasePhase::Lingering(super::now_ns().max(1)); + } Ok(self.pending_release()) } + /// The time since which an idle registration lingers, if it does. + pub(crate) fn lingering(&self) -> Option { + match self.phase { + LeasePhase::Lingering(since) => Some(since), + _ => None, + } + } + /// Ends a linger; the final release becomes pending. + pub(crate) fn end_linger(&mut self) { + if matches!(self.phase, LeasePhase::Lingering(_)) { + self.phase = LeasePhase::Ready; + } + } pub(crate) fn pending_release(&self) -> Option { (self.phase == LeasePhase::Ready && self.references == 0).then(|| { super::fw::channels::FreeListRelease::new(self.generation, self.owner.buffer_slot) diff --git a/drivers/gpu/drm/asahi/g17/runtime/events.rs b/drivers/gpu/drm/asahi/g17/runtime/events.rs index 1797ca5c0ce94..cdd8cc6d5dea5 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/events.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/events.rs @@ -399,6 +399,11 @@ impl crate::g17::Shared { .any(|queue| queue.in_flight()); let in_flight = compute || firmware.queues.render_in_flight(); let idle = firmware.idle_check(now, in_flight)?; + let lingers = if firmware.recovery.pending() { + None + } else { + firmware.expire_render_lingers(now)? + }; firmware.reclaim_drained_renders(deferred)?; firmware.service_compute_exit(deferred)?; let pools = firmware.service_compute_pools(deferred, now)?; @@ -441,11 +446,14 @@ impl crate::g17::Shared { } else { idle }; - // A retained compute pool gives its backing back when it expires. - let idle = match (idle, pools) { - (Some(delay), Some(expiry)) => Some(delay.min(expiry)), - (delay, expiry) => delay.or(expiry), - }; + // A retained compute pool gives its backing back when it expires, and + // so does an idle render free-list registration. + let idle = [pools, lingers] + .into_iter() + .fold(idle, |idle, expiry| match (idle, expiry) { + (Some(delay), Some(expiry)) => Some(delay.min(expiry)), + (delay, expiry) => delay.or(expiry), + }); if let Some(delay) = idle { self.queue_idle(delay); } diff --git a/drivers/gpu/drm/asahi/g17/runtime/render.rs b/drivers/gpu/drm/asahi/g17/runtime/render.rs index 2877dea91d0ea..d770c433528c9 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render.rs @@ -609,7 +609,16 @@ impl crate::g17::queue::render::Host for Host<'_> { buffer_id: u8, ) -> Result { let queues = &mut self.firmware.queues; - let next = queues.render.pools.completed(pool_id, generation)?; + // Each final release is a control record the firmware processes before + // it returns to the work rings. While other pairs keep it busy, a + // registration left idle lingers instead, and the owner's next render + // reuses it. On an otherwise idle device the release is cheaper than + // reusing a registration that went idle, so it is sent at once. + // The pair retiring here is lent out of its entry meanwhile. + let linger = queues.render.entries.iter().flatten().any(|entry| { + entry.pair.as_ref().is_some_and(|pair| pair.published_in_flight()) + }); + let next = queues.render.pools.completed(pool_id, generation, linger)?; queues.buffers.can_release(buffer_id)?; if let Some(owners) = owners { self.firmware.port.qos_complete(&owners)?; @@ -645,6 +654,30 @@ impl crate::g17::Firmware { result } + /// Event-worker turn: ends free-list lingers that elapsed or whose owner + /// closed or moved on, publishes their releases, and returns the delay to + /// the next expiry, which the worker turns into a timed wake. + pub(in crate::g17) fn expire_render_lingers(&mut self, now: u64) -> Result> { + let render = &mut self.queues.render; + let entries = &render.entries; + let (ended, next) = render.pools.expire_lingers(now, |owner| { + entries.iter().flatten().any(|entry| { + !entry.closed + && entry + .pool + .as_ref() + .is_some_and(|pool| pools::Pools::owner(pool) == *owner) + }) + }); + if ended { + match self.release_render_lists() { + Err(error) if error == EAGAIN => {} + result => result?, + } + } + Ok(next) + } + fn release_render_lists(&mut self) -> Result { if self.queues.render.grow.reply.is_some() { return Err(EAGAIN); diff --git a/drivers/gpu/drm/asahi/g17/runtime/render/pools.rs b/drivers/gpu/drm/asahi/g17/runtime/render/pools.rs index 4713eb5b6b152..78e5e3a219358 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render/pools.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render/pools.rs @@ -6,7 +6,7 @@ use crate::g17::{ channel::Rings, - freelist::{Lease, Owner, RenderPool}, + freelist::{Lease, Owner, RenderPool, LINGER_NS}, fw::{ channels::{ControlRecord, FreeListCompletion}, initdata::PagePoolDescriptor, @@ -49,7 +49,7 @@ impl Pools { Ok(slot) } - fn owner(pool: &RenderPool) -> Owner { + pub(super) fn owner(pool: &RenderPool) -> Owner { Owner { serial: pool.serial(), epoch: pool.epoch(), @@ -154,12 +154,12 @@ impl Pools { } /// Preflight a paired retirement before QoS or PB reference accounting. - pub(super) fn completed(&self, id: u16, generation: u64) -> Result { + pub(super) fn completed(&self, id: u16, generation: u64, linger: bool) -> Result { let mut next = self.leases[Self::slot(id)?].ok_or(EIO)?; if next.generation() != generation { return Err(EIO); } - next.complete_render()?; + next.complete_render(linger)?; Ok(next) } @@ -169,6 +169,32 @@ impl Pools { self.leases[usize::from(id)] = Some(next); } + /// Ends the linger of every idle registration that `keep` (an open owner + /// still bound to the pool) does not retain or whose linger has elapsed. + /// Returns whether a release became pending, and the next expiry delay. + pub(super) fn expire_lingers( + &mut self, + now: u64, + keep: impl Fn(&Owner) -> bool, + ) -> (bool, Option) { + let mut ended = false; + let mut next: Option = None; + for lease in self.leases.iter_mut().flatten() { + let Some(since) = lease.lingering() else { + continue; + }; + let age = now.saturating_sub(since); + if age >= LINGER_NS || !keep(&lease.owner()) { + lease.end_linger(); + ended = true; + } else { + let delay = LINGER_NS - age; + next = Some(next.map_or(delay, |next| next.min(delay))); + } + } + (ended, next) + } + pub(super) fn pending(&self) -> bool { self.leases .iter() From b7fa0d482eea670f9004422eaf57703a7ec8d4e5 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sat, 24 Oct 2026 00:26:24 -0400 Subject: [PATCH 033/388] drm/asahi: take the G17 device mutex less often to publish a render A render publication into an owner's installed pair took the device mutex six times from the scheduler (seven from the ioctl): to fetch the admission pool (and, from the ioctl, again to try it), to check the pair's identities, to find the pair already built (constructing a throwaway set of build resources), to look for a sibling pair to warm, to lend the pair out and to publish it. The admission pool lives as long as the firmware session, so the logical queue keeps a reference to it. The identity check also reports whether the owner's pair is installed, idle and backed, in which case the first pass skips graph construction; every retry still goes through it. An owner has exactly one pair (kick.rs asserts it), so there is no sibling to warm. Steady state now takes the device mutex three times from either path. gpubench unchanged within noise (render d1 151.6, d32 42.8, queues d8 58.8 us/submit); the mild stress gate passes. --- drivers/gpu/drm/asahi/g17/queue/admission.rs | 6 +++ drivers/gpu/drm/asahi/g17/runtime.rs | 4 ++ drivers/gpu/drm/asahi/g17/runtime/render.rs | 22 ++++++++++ .../drm/asahi/g17/runtime/render/prepare.rs | 4 ++ .../drm/asahi/g17/runtime/render/submit.rs | 41 +++++++++---------- 5 files changed, 56 insertions(+), 21 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/queue/admission.rs b/drivers/gpu/drm/asahi/g17/queue/admission.rs index 424bb1b7601f5..b3cd061e0e912 100644 --- a/drivers/gpu/drm/asahi/g17/queue/admission.rs +++ b/drivers/gpu/drm/asahi/g17/queue/admission.rs @@ -686,6 +686,12 @@ mod accounting_tests { pub(crate) struct QueueSlot { pool: Arc, } +impl QueueSlot { + /// The device's admission pool, which lives as long as the firmware session. + pub(crate) fn pool(&self) -> &Arc { + &self.pool + } +} impl Drop for QueueSlot { fn drop(&mut self) { self.pool.queues.fetch_sub(1, Ordering::AcqRel); diff --git a/drivers/gpu/drm/asahi/g17/runtime.rs b/drivers/gpu/drm/asahi/g17/runtime.rs index 1bd500bae6cb0..30816cd1cbdb6 100644 --- a/drivers/gpu/drm/asahi/g17/runtime.rs +++ b/drivers/gpu/drm/asahi/g17/runtime.rs @@ -664,6 +664,8 @@ pub(super) struct Backend { /// channel from being shared away between prepare_job and run_job. #[pin] claimed: kernel::sync::Mutex>>, + /// Render pair admission, shared by every logical queue of the session. + admission: Arc, } impl Backend { @@ -745,6 +747,7 @@ impl Backend { return Err(EAGAIN); } drop(state); + let admission = slot.pool().clone(); Ok(( Arc::pin_init( pin_init!(Self { @@ -756,6 +759,7 @@ impl Backend { closed: core::sync::atomic::AtomicBool::new(false), direct <- kernel::new_mutex!(None, "g17::Backend::direct"), claimed <- kernel::new_mutex!(None, "g17::Backend::claimed"), + admission, }), GFP_KERNEL, )?, diff --git a/drivers/gpu/drm/asahi/g17/runtime/render.rs b/drivers/gpu/drm/asahi/g17/runtime/render.rs index d770c433528c9..6360317b27851 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render.rs @@ -326,6 +326,28 @@ impl Registry { Ok(None) } + /// The installed pair in `slot` belongs to `owner` and its context, is + /// neither borrowed nor settling a failure, and its pool has backing: the + /// state in which building the pair would do nothing. + pub(super) fn render_ready(&self, slot: u8, owner: u64, context: &Arc) -> Result { + let Ok(entry) = self.render.entry(slot) else { + return Ok(false); + }; + if entry.pending_error.is_some() + || entry.pending_timeout + || entry.constructing + || entry.preparing.is_some() + || entry.growing + || entry.parked.is_some() + { + return Ok(false); + } + let Some(pair) = entry.pair.as_ref() else { + return Ok(false); + }; + Ok(pair.matches(owner, context)? && !pair.pool().vacant()) + } + fn has_render_ids(&self, owner: u64) -> bool { self.render .entries diff --git a/drivers/gpu/drm/asahi/g17/runtime/render/prepare.rs b/drivers/gpu/drm/asahi/g17/runtime/render/prepare.rs index 04afe2e05c107..62ca7895a86f8 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render/prepare.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render/prepare.rs @@ -593,6 +593,10 @@ impl super::super::Backend { pub(super) fn ensure_render(&self, packet: &Arc, slot: u8, sleep: bool) -> Result { self.build_render_pair(slot, packet, false, sleep)?; + // An owner has exactly one pair (kick.rs asserts it): no sibling to warm. + if crate::hw::t8140::queues::RENDER_PAIRS_PER_OWNER == 1 { + return Ok(()); + } let sibling = { let state = self.shared.state.lock(); (*state) diff --git a/drivers/gpu/drm/asahi/g17/runtime/render/submit.rs b/drivers/gpu/drm/asahi/g17/runtime/render/submit.rs index bd83c81b1d593..9d77a1e104328 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render/submit.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render/submit.rs @@ -44,16 +44,15 @@ impl Backend { /// Reserve identities before attaching a lease to the packet. An empty /// logical slot must not hide reusable physical channels when the shared /// queue-ID namespace is full. + /// Also reports whether the owner's installed pair is ready to take the + /// command, so that the publication skips graph construction. fn acquire_render_with_ids( &self, packet: &Arc, mut next: Option, sleep: bool, - ) -> Result { - let pool = { - let state = self.shared.state.lock(); - (*state).as_deref().ok_or(ENODEV)?.queues.admission.clone() - }; + ) -> Result<(RenderLease, bool)> { + let pool = &self.admission; let started = super::super::now_ns(); let deadline_ns = started.saturating_add(u64::from(crate::g17::job::TIMEOUT_MS) * 1_000_000); // A capacity refusal samples the render domain and retries once; only @@ -108,7 +107,10 @@ impl Backend { let mut state = self.shared.state.lock(); let firmware = (*state).as_deref_mut().ok_or(ENODEV)?; match firmware.queues.ensure_render_ids(slot, self.owner) { - Ok(()) => return Ok(lease), + Ok(()) => { + let ready = firmware.queues.render_ready(slot, self.owner, &self.context)?; + return Ok((lease, ready)); + } Err(error) if error == ENOSPC => { let reclaimed = firmware.queues.reclaim_unused_render_spare(None, true)?; if reclaimed.is_some() { @@ -282,7 +284,7 @@ impl Backend { // A publication from the submitting ioctl (an early lease) never // sleeps for contention; it falls back to the scheduler instead. let sleep = early.is_none(); - let admission = self.acquire_render_with_ids(packet, early, sleep)?; + let (admission, mut ready) = self.acquire_render_with_ids(packet, early, sleep)?; let slot = admission.slot(); packet.completion.attach_render_lease(admission)?; let mut busy_since = None; @@ -335,7 +337,14 @@ impl Backend { result?; } } - match self.ensure_render(packet, slot, sleep) { + // A pair found ready under the identity check needs no build; any + // retry revalidates through ensure_render. + let ensured = if core::mem::take(&mut ready) { + Ok(()) + } else { + self.ensure_render(packet, slot, sleep) + }; + match ensured { Err(error) if error == ENOSPC => { self.wait_render_capacity(&mut seen, capacity_started, sleep)?; continue; @@ -386,19 +395,9 @@ impl Backend { if self.backlog.load(Ordering::Acquire) != 0 || packet.has_dependencies() { return false; } - let admission = { - let state = self.shared.state.lock(); - let Some(firmware) = (*state).as_deref() else { - return false; - }; - match firmware - .queues - .admission - .try_acquire_render(self.owner, Request::of(packet)) - { - Ok(Some(lease)) => lease, - _ => return false, - } + let admission = match self.admission.try_acquire_render(self.owner, Request::of(packet)) { + Ok(Some(lease)) => lease, + _ => return false, }; if !packet.begin_publication() { packet.completion.fail_unpublished(ECANCELED); From d272a7060d20a0beb635ca8426c8822b8e823a43 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sat, 24 Oct 2026 00:43:52 -0400 Subject: [PATCH 034/388] drm/asahi: publish a G17 render into its pair without the device mutex Every render publication held the device mutex, which the event worker also needs for each pass, across the whole publication: dependency and hazard collection, kick and descriptor commits, QoS accounting, both work-ring records and producers, and the doorbell. In the steady state (the owner's graph is installed and registered at its priority, last ran this owner's scheduler, needs no growth, has nothing to replay or settle, and the command has no compute prefix) the device mutex now only lends the pair out, together with its free-list preparation and a parameter-buffer reference, and takes it back. Preparation and publication in between take the port: a render publication lock that every render publication holds, with or without the device mutex (the tiling and fragment work rings keep one host producer at a time, and attachment overlaps keep their publication order), QoS accounting, the completion stamps, the doorbell and the full-effort witness. The port gains views of the render work rings and of the stamp page. The free-list lease leaves preparation, or is rolled back with the parameter-buffer reference, when the pair returns; while it is lent the event worker skips the pair, so no retirement can touch its commands or references. A recovery that starts during the publication fails it with EIO, as on the compute path. Anything else, including a first publication, growth and owner changes, still publishes under the device mutex. During gpubench render rows 306k of 320k publications took the new path. render.light d2/d8/d32 68.9/45.2/42.8 -> 65.9/43.5/41.1 us/submit, d1 150.2 (151.6), queues d8 58.7 (58.8); the rows stay firmware bound. Mild stress gate passes. --- drivers/gpu/drm/asahi/g17/initdata.rs | 25 +- drivers/gpu/drm/asahi/g17/port.rs | 125 +++++- drivers/gpu/drm/asahi/g17/queue/render.rs | 49 +++ drivers/gpu/drm/asahi/g17/runtime/render.rs | 1 + .../drm/asahi/g17/runtime/render/direct.rs | 366 ++++++++++++++++++ .../drm/asahi/g17/runtime/render/submit.rs | 14 +- 6 files changed, 566 insertions(+), 14 deletions(-) create mode 100644 drivers/gpu/drm/asahi/g17/runtime/render/direct.rs diff --git a/drivers/gpu/drm/asahi/g17/initdata.rs b/drivers/gpu/drm/asahi/g17/initdata.rs index 6d0b2f9951956..8f7551b1763c9 100644 --- a/drivers/gpu/drm/asahi/g17/initdata.rs +++ b/drivers/gpu/drm/asahi/g17/initdata.rs @@ -659,15 +659,16 @@ impl InitData { Ok(()) } - /// Raw views of the memory publishers share without the device mutex: every compute work - /// ring, the QoS table and the recovery state word. They stay valid for this session. + /// Raw views of the memory publishers share without the device mutex: every work ring, + /// the completion stamps, the QoS table and the recovery state word. They stay valid for + /// this session. pub(super) fn port_views( &self, mailbox: *const super::Mailbox, ) -> Result { use super::fw::{channels, queue::{DataMaster, Policy}}; - let ring = |priority: u8| -> Result { - let channel = channels::work_ring_index(priority, DataMaster::Compute).ok_or(EINVAL)?; + let ring_of = |priority: u8, engine: DataMaster| -> Result { + let channel = channels::work_ring_index(priority, engine).ok_or(EINVAL)?; let base = self.state_offset(Role::Primary) + abi::work_state_offset(channel); let state = self.object(self.cluster)?; let word = |offset: usize| -> Result<*const AtomicU32> { @@ -686,12 +687,18 @@ impl InitData { producer: word(abi::ChannelState::PRODUCER)?, }) }; - let mut compute = [ring(0)?; Policy::PRIORITIES as usize]; - for (priority, view) in compute.iter_mut().enumerate().skip(1) { - *view = ring(priority as u8)?; - } + let rings = |engine: DataMaster| -> Result<[super::port::RingView; Policy::PRIORITIES as usize]> { + let mut views = [ring_of(0, engine)?; Policy::PRIORITIES as usize]; + for (priority, view) in views.iter_mut().enumerate().skip(1) { + *view = ring_of(priority as u8, engine)?; + } + Ok(views) + }; Ok(super::port::Views { - compute, + compute: rings(DataMaster::Compute)?, + tiling: rings(DataMaster::Tiling)?, + fragment: rings(DataMaster::Fragment)?, + stamps: self.object(self.stamps[0])?.word(0)?, qos: self .qos()? .pointer(0, core::mem::size_of::())?, diff --git a/drivers/gpu/drm/asahi/g17/port.rs b/drivers/gpu/drm/asahi/g17/port.rs index 63eab753632be..1adf9f9690456 100644 --- a/drivers/gpu/drm/asahi/g17/port.rs +++ b/drivers/gpu/drm/asahi/g17/port.rs @@ -7,7 +7,9 @@ //! through a preparation lease ([`super::Shared::port`]): shutdown and device loss join every //! lease before they detach the session. Each piece below is atomic or has its own lock. //! -//! Lock order: device mutex -> compute queue cell -> the locks of this port. +//! Lock order: device mutex -> compute queue cell -> the locks of this port. The render +//! publication lock comes first among the port's locks: device mutex -> render publication +//! lock -> admission pool, QoS, mailbox. Its holder never takes the device mutex. use super::{ fw::{ @@ -18,9 +20,9 @@ use super::{ }; use core::sync::atomic::{fence, AtomicBool, AtomicU32, AtomicU64, Ordering}; use kernel::{ - new_spinlock, + new_mutex, new_spinlock, prelude::*, - sync::{Arc, SetOnce, SpinLock}, + sync::{Arc, Mutex, MutexGuard, SetOnce, SpinLock}, }; const PRIORITIES: usize = Policy::PRIORITIES as usize; @@ -62,6 +64,10 @@ impl RingView { /// Raw views of the session's firmware-shared memory, fixed once the firmware is built. pub(super) struct Views { pub(super) compute: [RingView; PRIORITIES], + pub(super) tiling: [RingView; PRIORITIES], + pub(super) fragment: [RingView; PRIORITIES], + /// Completion stamp page A: one word per QID, written by the firmware. + pub(super) stamps: *const AtomicU32, pub(super) qos: *mut u8, pub(super) recovery: *const AtomicU32, pub(super) mailbox: *const super::Mailbox, @@ -81,6 +87,11 @@ pub(super) struct Port { rings: SpinLock<[u32; PRIORITIES]>, #[pin] qos: SpinLock, + /// Serializes every render publication, with or without the device mutex: the tiling + /// and fragment work rings have one host producer at a time, and same-VM attachment + /// overlaps are ordered by publication order (see hazard.rs). + #[pin] + render: Mutex<()>, /// The accepted recovery generation, mirrored for publishers without the device mutex. generation: AtomicU64, /// Device publication order shared by compute and render commands. @@ -114,6 +125,7 @@ impl Port { views: SetOnce::new(), rings <- new_spinlock!([0; PRIORITIES], "g17::Port::rings"), qos <- new_spinlock!(qos::Accounting::new(), "g17::Port::qos"), + render <- new_mutex!((), "g17::Port::render"), generation: AtomicU64::new(0), publications: AtomicU64::new(0), effort_full: AtomicBool::new(false), @@ -196,6 +208,113 @@ impl Port { Ok(next) } + /// Taken around every render publication; see `render`. + pub(super) fn render_lock(&self) -> MutexGuard<'_, ()> { + self.render.lock() + } + + fn render_rings(&self, priority: u8) -> Result<(RingView, RingView)> { + let views = self.views()?; + let index = usize::from(priority); + Ok(( + *views.fragment.get(index).ok_or(EINVAL)?, + *views.tiling.get(index).ok_or(EINVAL)?, + )) + } + + /// Both render work rings of `priority` have a free slot. Only firmware consumption, + /// never a publisher without the render lock, changes this. + pub(super) fn render_room(&self, _guard: &MutexGuard<'_, ()>, priority: u8) -> Result { + let (fragment, tiling) = self.render_rings(priority)?; + Ok(fragment.free()? > 0 && tiling.free()? > 0) + } + + /// Copies a render pair's fragment, then tiling record into the slots after each + /// producer without exposing them, as `InitData::publish_work_pair` does for a deferred + /// pair. Returns the producers that expose them. + pub(super) fn write_render_pair( + &self, + _guard: &MutexGuard<'_, ()>, + priority: u8, + fragment: &WorkSlot, + tiling: &WorkSlot, + commit_accounting: impl FnOnce() -> Result, + publish_late_tiling: impl FnOnce(), + ) -> Result<[u32; 2]> { + if fragment.data_master != DataMaster::Fragment as u32 + || tiling.data_master != DataMaster::Tiling as u32 + { + return Err(EINVAL); + } + let (fragment_ring, tiling_ring) = self.render_rings(priority)?; + let slot = |ring: &RingView| -> Result<(*mut u8, u32)> { + let [first, second, producer] = ring.cursors()?; + let next = (producer + 1) % WORK_RING_SLOTS; + if next == first || next == second { + return Err(EINVAL); + } + // SAFETY: The ring view spans WORK_RING_SLOTS records; `producer` is in range. + Ok((unsafe { ring.slots.add(producer as usize * core::mem::size_of::()) }, next)) + }; + let (fragment_slot, fragment_next) = slot(&fragment_ring)?; + let (tiling_slot, tiling_next) = slot(&tiling_ring)?; + let copy = |record: &WorkSlot, destination: *mut u8| { + // SAFETY: The slot after the producer is host-owned until the producer moves past + // it, and the render lock excludes every other host writer of this ring. + unsafe { + core::ptr::copy_nonoverlapping( + core::ptr::from_ref(record).cast::(), + destination, + core::mem::size_of::(), + ) + } + }; + copy(fragment, fragment_slot); + commit_accounting()?; + fence(Ordering::SeqCst); + publish_late_tiling(); + copy(tiling, tiling_slot); + fence(Ordering::SeqCst); + Ok([fragment_next, tiling_next]) + } + + /// Exposes a pair written by `write_render_pair`, fragment first, as + /// `InitData::publish_retained_work_pair` does: the producers must still precede `next`. + pub(super) fn expose_render_pair( + &self, + _guard: &MutexGuard<'_, ()>, + priority: u8, + next: [u32; 2], + ) -> Result { + let (fragment_ring, tiling_ring) = self.render_rings(priority)?; + for (ring, next) in [(&fragment_ring, next[0]), (&tiling_ring, next[1])] { + let [_, _, producer] = ring.cursors()?; + if next != (producer + 1) % WORK_RING_SLOTS { + return Err(EBUSY); + } + } + fence(Ordering::SeqCst); + // SAFETY: As for `RingView::cursors`. + unsafe { (*fragment_ring.producer).store(next[0], Ordering::Relaxed) }; + fence(Ordering::SeqCst); + // SAFETY: As above. + unsafe { (*tiling_ring.producer).store(next[1], Ordering::Relaxed) }; + fence(Ordering::SeqCst); + Ok(()) + } + + /// The completion stamp of `qid` in page A. + pub(super) fn stamp(&self, qid: u8) -> Result { + if qid > super::fw::kick::QID_MAX { + return Err(EINVAL); + } + let base = self.views()?.stamps; + // SAFETY: Page A holds one word per QID up to QID_MAX; the session retains it. + let value = unsafe { (*base.add(usize::from(qid))).load(Ordering::Relaxed) }; + fence(Ordering::Acquire); + Ok(value) + } + fn qos_view(&self) -> Result> { // SAFETY: The session retains the QoS table for at least as long as this port. unsafe { qos::View::from_raw(self.views()?.qos) } diff --git a/drivers/gpu/drm/asahi/g17/queue/render.rs b/drivers/gpu/drm/asahi/g17/queue/render.rs index faf31514a2f2f..3e65ea3b9a367 100644 --- a/drivers/gpu/drm/asahi/g17/queue/render.rs +++ b/drivers/gpu/drm/asahi/g17/queue/render.rs @@ -1787,6 +1787,55 @@ impl Pair { } /// The scheduler page these queues last ran a command for, when the next /// publication for `context` changes the queues' owner. + /// The steady state in which a command may publish without the device + /// mutex: the graph is installed and registered at `priority`, it last ran + /// `context`'s scheduler, and nothing is replayed, settled, aliased or + /// switched on it. + pub(crate) fn direct_ready(&self, context: &Context, priority: u8) -> bool { + self.graph_started + && self.installed == [true; 2] + && self.registered == Some(priority) + && priority < 8 + && self.outer_started & (1u8 << priority) != 0 + && self.executed == context.scheduler_va() + && self.switch.is_none() + && self.owner.is_some() + && !self.quarantined + && self.terminal.is_none() + && self.pending_aliases.is_none() + && self.kick_aliases.is_none() + && self.prepared.is_none() + && self.replay.len() == 0 + && self.displaced.is_none() + && self.active.room() + } + /// The render prefix of `packet` on this pair, as the registry collects it + /// for an owner whose only pair this is. + pub(crate) fn own_dependencies(&self, owner: u64, packet: &Arc) -> Result { + let mut dependencies = RenderDependencies::EMPTY; + let mut frontier = crate::g17::dependency::Frontier::new(); + self.collect_dependencies(owner, packet.completion.status(), packet.order.wait_through[0], &mut frontier)?; + for dependency in frontier.as_slice() { + *dependencies.render.get_mut(dependencies.render_count).ok_or(EOVERFLOW)? = *dependency; + dependencies.render_count += 1; + } + if dependencies.render_count != 0 + && matches!(&packet.command, Validated::Render { pass, .. } if pass.fragment_barriers) + { + let mut tiling = crate::g17::dependency::Frontier::new(); + self.collect_tiling_dependencies( + owner, + packet.completion.status(), + packet.order.wait_through[0], + &mut tiling, + )?; + for dependency in tiling.as_slice() { + *dependencies.tiling.get_mut(dependencies.tiling_count).ok_or(EOVERFLOW)? = *dependency; + dependencies.tiling_count += 1; + } + } + Ok(dependencies) + } pub(crate) fn owner_switch_from(&self, context: &Context) -> Option { (self.executed != 0 && self.executed != context.scheduler_va()).then_some(self.executed) } diff --git a/drivers/gpu/drm/asahi/g17/runtime/render.rs b/drivers/gpu/drm/asahi/g17/runtime/render.rs index 6360317b27851..aa437b8a21be2 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render.rs @@ -6,6 +6,7 @@ mod exit; mod dependencies; +mod direct; mod grow; mod observe; mod pools; diff --git a/drivers/gpu/drm/asahi/g17/runtime/render/direct.rs b/drivers/gpu/drm/asahi/g17/runtime/render/direct.rs new file mode 100644 index 0000000000000..00e0037ebae60 --- /dev/null +++ b/drivers/gpu/drm/asahi/g17/runtime/render/direct.rs @@ -0,0 +1,366 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT + +//! Steady-state render publication without the device mutex. +//! +//! The device mutex lends the owner's installed pair out, together with its +//! free-list preparation and a parameter-buffer reference, and takes it back. +//! Between the two, command preparation and publication take only the port: +//! its render publication lock (the render work rings' single producer and +//! the publication order of attachment hazards), QoS accounting, the stamps +//! and the doorbell. While the pair is lent, the event worker skips it, so no +//! retirement can complete its commands or their free-list references. + +use super::super::{Backend, DeferredBatch}; +use crate::g17::{ + job::Packet, + object::Allocator, + port::Port, + queue::render::{Pair, PublicationLease, RenderAddresses}, +}; +use core::sync::atomic::Ordering; +use kernel::{ + prelude::*, + sync::{Arc, MutexGuard}, +}; + +/// A pair lent out for one publication, with what that publication holds. +pub(in crate::g17::runtime) struct Loan { + pair: Option>, + preparation: Option, + buffer: u8, + lease: PublicationLease, + addresses: RenderAddresses, + uat: Arc, +} + +/// Publication host of a lent pair. Registration, first-publication and +/// growth paths never run here: the loan requires an installed, registered +/// graph that needs no growth. +struct DirectHost<'a> { + port: &'a Port, + render: &'a MutexGuard<'a, ()>, + generation: u64, + activation: Option<(u16, u64)>, +} + +impl crate::g17::queue::render::Host for DirectHost<'_> { + fn next_publication(&mut self) -> Result { + Ok(self.port.next_publication()) + } + fn epoch(&self) -> Result<(u64, u32)> { + Ok((self.generation, self.port.recovery_state()?)) + } + fn stamps(&self, qids: [u8; 2]) -> Result<[u32; 2]> { + Ok([self.port.stamp(qids[0])?, self.port.stamp(qids[1])?]) + } + fn qos_publish( + &mut self, + owner: crate::g17::qos::Owner, + scheduler: u64, + policy: crate::g17::fw::queue::Policy, + ) -> Result { + self.port.qos_publish(owner, scheduler, policy) + } + fn qos_cancel(&mut self, publication: crate::g17::qos::Publication) -> Result { + self.port.qos_cancel(publication) + } + fn install_pair(&mut self, _registration: &crate::g17::fw::kick::RenderRegistration) -> Result { + Err(EAGAIN) + } + fn publish_qid(&mut self, _id: crate::g17::kick::Id) -> Result { + // An installed graph published its identities with its first command. + Ok(()) + } + fn publish_outer_pair( + &mut self, + priority: u8, + fragment: &crate::g17::fw::channels::WorkSlot, + tiling: &crate::g17::fw::channels::WorkSlot, + commit: impl FnOnce() -> Result, + late_tiling: impl FnOnce(), + ) -> Result<[u32; 2]> { + self.port + .write_render_pair(self.render, priority, fragment, tiling, commit, late_tiling) + } + fn activate_render_pool(&mut self, id: u16, generation: u64) -> Result { + // The lease leaves preparation when the pair is returned; nothing can + // observe it in between because the pair is lent. + self.activation = Some((id, generation)); + Ok(()) + } + fn publish_retained_pair( + &mut self, + priority: u8, + _fragment: &crate::g17::fw::channels::WorkSlot, + _tiling: &crate::g17::fw::channels::WorkSlot, + next: [u32; 2], + ) -> Result { + self.port.expose_render_pair(self.render, priority, next) + } + /// Pipe 0 (tiling) in bits 1:0, priority class in bits 3:2. + fn notify_render(&mut self, priority: u8) -> Result { + self.port + .notify((0x83 << crate::g17::MSG_TYPE_SHIFT) | u64::from(priority) << 2) + } + fn note_submission(&mut self) -> Result { + // Counted, with full effort witnessed, before the pair was lent. + Ok(()) + } + fn complete_render( + &mut self, + _owners: Option<[crate::g17::qos::Owner; 2]>, + _pool_id: u16, + _generation: u64, + _buffer_id: u8, + ) -> Result { + Err(EINVAL) + } +} + +impl crate::g17::Firmware { + /// Lends `owner`'s pair in `slot` for a publication of `packet`, or None if + /// the pair is not in the steady state the direct path requires. + fn lend_render( + &mut self, + slot: u8, + owner: u64, + packet: &Arc, + priority: u8, + generation: u64, + ) -> Result> { + if self.recovery.pending() || self.recovery.generation() != generation { + return Ok(None); + } + let Ok(entry) = self.queues.render.entry_mut(slot) else { + return Ok(None); + }; + if !entry.published + || entry.closed + || entry.constructing + || entry.preparing.is_some() + || entry.growing + || entry.parked.is_some() + || entry.pending_error.is_some() + || entry.pending_timeout + { + return Ok(None); + } + let Some(pair) = entry.pair.as_mut() else { + return Ok(None); + }; + if !pair.matches(owner, &packet.context)? + || !pair.direct_ready(&packet.context, priority) + || pair.pool().vacant() + || !matches!(pair.host_growth_target(packet), Ok(None)) + { + return Ok(None); + } + let qids = pair.qids(); + let pool = pair.pool().clone(); + let buffer_id = pair.buffer_id(); + let addresses = RenderAddresses { + stamps: [ + self.init.stamp_addresses(qids[0])?, + self.init.stamp_addresses(qids[1])?, + ], + }; + let (buffer, buffer_token) = self.queues.buffers.acquire_owner(buffer_id)?; + let preparation = match self.queues.render.pools.prepare(&self.init, &pool) { + Ok(preparation) => preparation, + Err(error) => { + self.queues.buffers.release(buffer)?; + return if error == EAGAIN || error == EBUSY { + Ok(None) + } else { + Err(error) + }; + } + }; + let lease = PublicationLease { + free_list_generation: preparation.generation, + buffer_token, + }; + let pair = match self.queues.render.take_pair(slot, owner, packet) { + Ok(pair) => pair, + Err(error) => { + self.queues.render.pools.rollback(preparation); + self.queues.buffers.release(buffer)?; + return if error == EBUSY { Ok(None) } else { Err(error) }; + } + }; + // The selected scheduler is retained through release acknowledgement, + // as on the device-mutex path. + packet.context.mark_published(); + Ok(Some(Loan { + pair: Some(pair), + preparation: Some(preparation), + buffer, + lease, + addresses, + uat: self.uat.clone(), + })) + } + + /// Takes a lent pair back and settles what its publication held: the + /// free-list preparation is activated once published, rolled back with the + /// parameter-buffer reference otherwise. + fn return_render( + &mut self, + slot: u8, + loan: &mut Loan, + activation: Option<(u16, u64)>, + packet: &Arc, + ) -> Result { + self.queues.render.return_pair(slot, &mut loan.pair)?; + if self.queues.render.take_missed_event(slot) { + self.primary.state.shared.queue_events(); + } + let entry = self.queues.render.entry_mut(slot)?; + // A failure or timeout recorded against the lent pair is settled by the + // event worker's collector, as after any other loan. + if entry.pending_error.is_some() || entry.pending_timeout { + self.primary.state.shared.queue_events(); + } + let pair = entry.pair.as_mut().ok_or(EIO)?; + if entry.closed { + pair.release_owner(); + } + if !packet.is_published() { + pair.discard_prepared(packet)?; + } + if let Some((id, generation)) = activation { + if self.queues.render.pools.activate(id, generation).is_err() { + self.primary.state.shared.lose_device_quietly(); + } + } + let preparation = loan.preparation.take().ok_or(EIO)?; + if packet.is_published() { + self.queues.render.entry_mut(slot)?.published = true; + } else { + self.queues.render.pools.rollback(preparation); + self.queues.buffers.release(loan.buffer)?; + } + Ok(()) + } +} + +impl Backend { + /// Publishes `packet` into the owner's installed pair with the device + /// mutex held only to lend the pair out and take it back. None leaves the + /// packet unpublished for the device-mutex path. + pub(in crate::g17::runtime) fn render_direct( + &self, + packet: &Arc, + slot: u8, + deferred: &mut DeferredBatch, + ) -> Option { + // A compute prefix needs the compute queue's state; leave it to the + // device-mutex path. + if packet.is_published() + || packet.cancel_requested() + || packet.completion.status().get() != 0 + || packet.order.wait_through[1].unwrap_or(0) != 0 + || self.admission_open().is_err() + { + return None; + } + let lease = self.shared.preparations.try_enter().ok()?; + let port = self.shared.port(&lease)?; + if self.shared.crashed.load(Ordering::Acquire) || port.recovery_state().ok()? != 0 { + return None; + } + // Full effort is applied by the device-mutex path when it is not already. + if !port.note_submission() { + return None; + } + match packet.completion.work_state().ok()?.acquire() { + Ok(_) => {} + Err(error) if error == EBUSY => return None, + Err(error) => return Some(Err(error)), + } + let priority = packet.context.policy().priority(); + let generation = port.generation(); + let dev = self.shared.drm().ok()?; + let mut loan = { + let mut state = self.shared.state.lock(); + let firmware = (*state).as_deref_mut()?; + if !lease.is_current() { + return None; + } + match firmware.lend_render(slot, self.owner, packet, priority, generation) { + Ok(Some(loan)) => loan, + Ok(None) => return None, + Err(error) => return Some(Err(error)), + } + }; + let mut activation = None; + let result = (|| { + let pair = loan.pair.as_mut().ok_or(EIO)?; + let alloc = Allocator { + dev: &dev, + uat: &loan.uat, + }; + pair.prepare(&alloc, packet, loan.addresses)?; + let render = port.render_lock(); + if !lease.is_current() || !port.render_room(&render, priority)? { + return Err(EBUSY); + } + let mut dependencies = pair.own_dependencies(self.owner, packet)?; + if let Some(footprint) = packet.footprint.as_ref() { + dependencies.hazard_count = self.admission.publication_barriers( + self.owner, + slot, + footprint, + &mut dependencies.hazards, + )?; + } + let mut host = DirectHost { + port, + render: &render, + generation, + activation: None, + }; + let result = pair.publish( + packet.clone(), + &dependencies, + loan.lease, + &mut host, + &mut |event| deferred.push(event), + ); + activation = host.activation; + result?; + // A recovery that started meanwhile may have discarded the commands. + if port.generation() != generation || port.recovery_state()? != 0 { + return Err(EIO); + } + Ok(()) + })(); + let returned = { + let mut state = self.shared.state.lock(); + match (*state).as_deref_mut() { + Some(firmware) => { + let returned = firmware.return_render(slot, &mut loan, activation, packet); + let grow_ready = firmware.render_grow_ready(); + drop(state); + if grow_ready { + self.shared.queue_grow(); + } + returned + } + // Shutdown joins every preparation lease before detaching the + // session, so this cannot happen while `lease` is held. + None => Err(ENODEV), + } + }; + self.shared.note_changed(); + drop(lease); + if let Err(error) = returned { + self.shared.lose_device_quietly(); + return Some(Err(error)); + } + match result { + Err(error) if (error == EAGAIN || error == EBUSY) && !packet.is_published() => None, + result => Some(result), + } + } +} diff --git a/drivers/gpu/drm/asahi/g17/runtime/render/submit.rs b/drivers/gpu/drm/asahi/g17/runtime/render/submit.rs index 9d77a1e104328..a9dac3dab2f73 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render/submit.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render/submit.rs @@ -287,6 +287,11 @@ impl Backend { let (admission, mut ready) = self.acquire_render_with_ids(packet, early, sleep)?; let slot = admission.slot(); packet.completion.attach_render_lease(admission)?; + if ready { + if let Some(result) = self.render_direct(packet, slot, deferred) { + return result; + } + } let mut busy_since = None; let capacity_started = super::super::now_ns(); let deadline_ns = capacity_started.saturating_add(super::super::WORK_RING_WAIT_NS); @@ -549,15 +554,20 @@ impl crate::g17::Firmware { if packet.completion.status().get() != 0 { return Err(EIO); } + let mut dependencies = self.queues.render_dependencies(owner, packet, state)?; + // Every render publication, with or without the device mutex, holds the + // port's render lock: the render work rings have one host producer at a + // time and attachment overlaps are ordered by publication order. + let port = self.port.clone(); + let _render = port.render_lock(); // Both outer destinations must have room before pool accounting or - // either inner queue becomes visible. All host producers share this lock. + // either inner queue becomes visible. let priority = packet.context.policy().priority(); if !self.init.work_ready(priority, crate::g17::fw::queue::DataMaster::Fragment)? || !self.init.work_ready(priority, crate::g17::fw::queue::DataMaster::Tiling)? { return Err(EBUSY); } - let mut dependencies = self.queues.render_dependencies(owner, packet, state)?; if let Some(footprint) = packet.footprint.as_ref() { dependencies.hazard_count = self.queues.admission.publication_barriers( owner, From 6428f7fd42fe075981984da569b0fa076f59a127 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sat, 24 Oct 2026 00:52:57 -0400 Subject: [PATCH 035/388] drm/asahi: lend a G17 render pair without the identity check first After the direct render path, a steady-state publication still took the device mutex three times: once to check the pair's identities and readiness, then to lend the pair and to take it back. The lend itself validates everything the identity check does. The logical queue remembers the pair it last published to directly. A lease on that pair (the ioctl's, or a non-blocking admission from the scheduler) goes straight to the lend; anything else, and any lend that declines, takes the full path with the identity check. gpubench unchanged within noise (render d1 150.8, d32 42.9, queues d8 58.4 us/submit); the mild stress gate passes. --- drivers/gpu/drm/asahi/g17/runtime.rs | 4 +++ .../drm/asahi/g17/runtime/render/direct.rs | 7 +++++- .../drm/asahi/g17/runtime/render/submit.rs | 25 +++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/asahi/g17/runtime.rs b/drivers/gpu/drm/asahi/g17/runtime.rs index 30816cd1cbdb6..b07423bd22115 100644 --- a/drivers/gpu/drm/asahi/g17/runtime.rs +++ b/drivers/gpu/drm/asahi/g17/runtime.rs @@ -666,6 +666,9 @@ pub(super) struct Backend { claimed: kernel::sync::Mutex>>, /// Render pair admission, shared by every logical queue of the session. admission: Arc, + /// The render pair this owner last published to without the device mutex + /// (zero for none): a hint, revalidated by every lend. + direct_slot: core::sync::atomic::AtomicU32, } impl Backend { @@ -760,6 +763,7 @@ impl Backend { direct <- kernel::new_mutex!(None, "g17::Backend::direct"), claimed <- kernel::new_mutex!(None, "g17::Backend::claimed"), admission, + direct_slot: core::sync::atomic::AtomicU32::new(0), }), GFP_KERNEL, )?, diff --git a/drivers/gpu/drm/asahi/g17/runtime/render/direct.rs b/drivers/gpu/drm/asahi/g17/runtime/render/direct.rs index 00e0037ebae60..cd7bad6ecce5d 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render/direct.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render/direct.rs @@ -360,7 +360,12 @@ impl Backend { } match result { Err(error) if (error == EAGAIN || error == EBUSY) && !packet.is_published() => None, - result => Some(result), + result => { + if result.is_ok() { + self.direct_slot.store(u32::from(slot), Ordering::Relaxed); + } + Some(result) + } } } } diff --git a/drivers/gpu/drm/asahi/g17/runtime/render/submit.rs b/drivers/gpu/drm/asahi/g17/runtime/render/submit.rs index a9dac3dab2f73..733a570f0e861 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render/submit.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render/submit.rs @@ -284,6 +284,31 @@ impl Backend { // A publication from the submitting ioctl (an early lease) never // sleeps for contention; it falls back to the scheduler instead. let sleep = early.is_none(); + let mut early = early; + // The pair this owner last published to directly: a lease on it goes + // straight to the direct path, whose lend checks everything the + // identity check would. + let known = self.direct_slot.load(Ordering::Relaxed); + let lease = match early.take() { + Some(lease) => Some(lease), + None if known != 0 => self + .admission + .try_acquire_render(self.owner, Request::of(packet)) + .ok() + .flatten(), + None => None, + }; + let early = match lease { + Some(lease) if u32::from(lease.slot()) == known => { + let slot = lease.slot(); + packet.completion.attach_render_lease(lease)?; + if let Some(result) = self.render_direct(packet, slot, deferred) { + return result; + } + packet.completion.detach_render_lease() + } + lease => lease, + }; let (admission, mut ready) = self.acquire_render_with_ids(packet, early, sleep)?; let slot = admission.slot(); packet.completion.attach_render_lease(admission)?; From 8cf02f589bce34b13002cbdf0f3b0361c4a8e66b Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sat, 24 Oct 2026 01:04:35 -0400 Subject: [PATCH 036/388] drm/asahi: wait for G17 render admission before run_job The scheduler's prepare step returned no fence for render jobs, so a job that could not get a work state or a pair admission (a cross-VM attachment overlap, a crowded VM, no pair capacity) waited for it inside run_job, holding the scheduler worker. prepare_job now checks both without taking anything: a work state of the context, and whether the admission pool would admit the command (its owner's pair has room and no overlap plan says wait, or a free pair exists for an owner without one). When either is missing it returns a fence of the domain whose return provides it: the context's work-state domain, or the render domain that every admission-pool return signals. The scheduler calls prepare again once it fires. Each check runs once more after sampling its domain, so the common case takes no domain lock. gpubench unchanged within noise (render d1 151.8, d32 42.7, queues d8 58.8 us/submit); the mild stress gate passes. --- drivers/gpu/drm/asahi/g17/queue/admission.rs | 40 +++++++++++++++ drivers/gpu/drm/asahi/g17/runtime/backend.rs | 5 +- .../drm/asahi/g17/runtime/render/submit.rs | 49 +++++++++++++++++++ 3 files changed, 92 insertions(+), 2 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/queue/admission.rs b/drivers/gpu/drm/asahi/g17/queue/admission.rs index b3cd061e0e912..e1e7552c243f2 100644 --- a/drivers/gpu/drm/asahi/g17/queue/admission.rs +++ b/drivers/gpu/drm/asahi/g17/queue/admission.rs @@ -298,6 +298,28 @@ impl State { ) } + /// Whether `pick` would return a slot, without taking it. + fn can_pick(&self, owner: u64, plan: Plan) -> bool { + let usable = |slot: usize| { + self.owners[slot] == owner + && (self.quarantined | self.kill_hold) & (1u64 << slot) == 0 + && self.refs[slot] < cfg::RENDER_DEPTH + }; + match plan { + Plan::Wait => false, + Plan::Pair(slot) => usize::from(slot) < cfg::RENDER_SLOTS && usable(usize::from(slot)), + Plan::Any if owner == 0 => false, + Plan::Any if self.owners.contains(&owner) => (0..cfg::RENDER_SLOTS).any(usable), + Plan::Any => { + let free = !self.active & Self::MASK & !self.quarantined & !self.kill_hold; + (0..cfg::RENDER_SLOTS).any(|slot| { + free & (1u64 << slot) != 0 + && (self.released & (1u64 << slot) != 0 || self.owners[slot] == 0) + }) + } + } + } + fn pick(&mut self, owner: u64, plan: Plan) -> Option { match plan { Plan::Any => self.acquire(owner), @@ -555,6 +577,24 @@ impl Pool { record, })) } + /// Whether `acquire_render` would admit this request now, without + /// admitting it: the scheduler's prepare step waits for this. + pub(crate) fn admits_render(&self, owner: u64, request: Option>) -> bool { + let state = self.state.lock(); + if owner == 0 || state.refusal(owner).is_err() { + return false; + } + if state + .owners + .iter() + .position(|id| *id == owner) + .is_some_and(|slot| state.quarantined & (1u64 << slot) != 0) + { + return false; + } + let plan = state.plan(owner, request, None); + state.can_pick(owner, plan) + } pub(crate) fn warmable_sibling( self: &Arc, owner: u64, diff --git a/drivers/gpu/drm/asahi/g17/runtime/backend.rs b/drivers/gpu/drm/asahi/g17/runtime/backend.rs index 39f628d007e18..d19f51c08dd29 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/backend.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/backend.rs @@ -216,11 +216,12 @@ impl job::Backend for Backend { /// Firmware kick parents order the same engine. Explicit cross-engine and /// input dependencies were already attached to the DRM scheduled job. - /// Compute admission waits for its shared capacity here, as a fence. + /// Compute and render admission wait for their shared capacity here, as a + /// fence. fn prepare(&self, packet: &Arc) -> Option { match packet.engine() { Engine::Compute => self.prepare_compute(packet), - Engine::Render => None, + Engine::Render => self.admit_render(packet), } } diff --git a/drivers/gpu/drm/asahi/g17/runtime/render/submit.rs b/drivers/gpu/drm/asahi/g17/runtime/render/submit.rs index 733a570f0e861..1ef6742b689a7 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render/submit.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render/submit.rs @@ -141,6 +141,55 @@ impl Backend { } } + /// Scheduler admission of a render packet. A work state of its context or + /// a pair admission it lacks becomes a fence that the return providing it + /// signals, so run_job finds both ready instead of waiting for them. + /// Errors, and anything else publication may wait for, are left to it. + pub(in crate::g17::runtime) fn admit_render(&self, packet: &Arc) -> Option { + if packet.early_result().is_some() + || packet.cancel_requested() + || packet.completion.status().get() != 0 + || self.admission_open().is_err() + { + return None; + } + // Each check runs once more after sampling its domain, so the common + // case takes no domain lock and no return between check and wait is lost. + let work = packet.completion.work_state().ok()?; + loop { + match work.acquire() { + Err(error) if error == EBUSY => { + let states = work.returns().observe(); + match work.acquire() { + Err(error) if error == EBUSY => match work.returns().wait(states) { + Ok(Some(fence)) => return Some(fence), + Ok(None) => continue, + Err(_) => return None, + }, + Err(_) => return None, + Ok(_) => {} + } + } + Err(_) => return None, + Ok(_) => {} + } + let request = Request::of(packet); + if self.admission.admits_render(self.owner, request) { + return None; + } + let observed = self.shared.renders.observe(); + if self.admission.admits_render(self.owner, request) { + return None; + } + self.shared.queue_events(); + match self.shared.renders.wait(observed) { + Ok(Some(fence)) => return Some(fence), + Ok(None) => continue, + Err(_) => return None, + } + } + } + /// Conditions which end a publication that has not become visible yet. fn publication_open(&self, packet: &Packet) -> Result { self.admission_open()?; From 550fb355e9a30c0db9c59fe35a931f1db9c6b5c4 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sat, 24 Oct 2026 00:16:03 -0400 Subject: [PATCH 037/388] drm/asahi: trace the G17 core power state at each work doorbell Add an asahi_g17_doorbell tracepoint that samples the core power register just before a compute or render work doorbell rings. The register is read only while the event is enabled, so tracing can tell whether a slow doorbell-to-start interval coincides with powered-down cores. --- drivers/gpu/drm/asahi/g17/mod.rs | 2 +- drivers/gpu/drm/asahi/g17/port.rs | 10 ++++++++-- drivers/gpu/drm/asahi/g17/runtime/render.rs | 3 +++ drivers/gpu/drm/asahi/g17/trace.c | 10 ++++++++++ drivers/gpu/drm/asahi/g17/trace.h | 17 +++++++++++++++++ drivers/gpu/drm/asahi/g17/trace.rs | 13 +++++++++++++ drivers/gpu/drm/asahi/regs.rs | 5 +++++ 7 files changed, 57 insertions(+), 3 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/mod.rs b/drivers/gpu/drm/asahi/g17/mod.rs index 5e4c0dcc756cb..96a0991cfd143 100644 --- a/drivers/gpu/drm/asahi/g17/mod.rs +++ b/drivers/gpu/drm/asahi/g17/mod.rs @@ -1361,7 +1361,7 @@ impl Gpu { session.uat.confirm_firmware_root_entries(root_entries)?; } let mailbox: *const Mailbox = &*session.primary.rtkit; - session.port.attach(session.init.port_views(mailbox)?)?; + session.port.attach(session.init.port_views(mailbox)?, session.bridge.regs.clone())?; session.uat.mirror_secondary_root()?; if cfg.fabric_bridge { session.bridge.regs.enable_g17_gating()?; diff --git a/drivers/gpu/drm/asahi/g17/port.rs b/drivers/gpu/drm/asahi/g17/port.rs index 1adf9f9690456..146b83972f203 100644 --- a/drivers/gpu/drm/asahi/g17/port.rs +++ b/drivers/gpu/drm/asahi/g17/port.rs @@ -82,6 +82,8 @@ unsafe impl Sync for Views {} #[pin_data] pub(super) struct Port { views: SetOnce, + /// Register access for tracing the core power state at the doorbell. + regs: SetOnce, /// Compute work-ring slots reserved by publishers that have not exposed them yet. #[pin] rings: SpinLock<[u32; PRIORITIES]>, @@ -123,6 +125,7 @@ impl Port { Arc::pin_init( pin_init!(Self { views: SetOnce::new(), + regs: SetOnce::new(), rings <- new_spinlock!([0; PRIORITIES], "g17::Port::rings"), qos <- new_spinlock!(qos::Accounting::new(), "g17::Port::qos"), render <- new_mutex!((), "g17::Port::render"), @@ -136,8 +139,8 @@ impl Port { } /// Called once while the session is built, before any publisher can see the port. - pub(super) fn attach(&self, views: Views) -> Result { - if self.views.populate(views) { + pub(super) fn attach(&self, views: Views, regs: crate::regs::G17Resources) -> Result { + if self.views.populate(views) && self.regs.populate(regs) { Ok(()) } else { Err(EBUSY) @@ -344,6 +347,9 @@ impl Port { pub(super) fn notify(&self, message: u64) -> Result { // SAFETY: The session's primary coprocessor owns the mailbox and outlives the port. let mailbox = unsafe { &*self.views()?.mailbox }; + super::trace::doorbell(super::trace::COMPUTE, || { + self.regs.as_ref().map_or(0, |regs| regs.g17_core_power()) + }); mailbox.send(super::EP_DOORBELL, message) } diff --git a/drivers/gpu/drm/asahi/g17/runtime/render.rs b/drivers/gpu/drm/asahi/g17/runtime/render.rs index aa437b8a21be2..f4f45a98005c3 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render.rs @@ -612,6 +612,9 @@ impl crate::g17::queue::render::Host for Host<'_> { /// Pipe 0 (tiling) in bits 1:0, priority class in bits 3:2. fn notify_render(&mut self, priority: u8) -> Result { self.release_registration(); + crate::g17::trace::doorbell(crate::g17::trace::RENDER, || { + self.firmware.bridge.regs.g17_core_power() + }); let result = self .firmware .primary diff --git a/drivers/gpu/drm/asahi/g17/trace.c b/drivers/gpu/drm/asahi/g17/trace.c index feffb96315845..fc189458af32d 100644 --- a/drivers/gpu/drm/asahi/g17/trace.c +++ b/drivers/gpu/drm/asahi/g17/trace.c @@ -31,6 +31,16 @@ void asahi_g17_trace_bind(u8 engine, u8 qid, u32 ctx, u8 kind) trace_asahi_g17_bind(engine, qid, ctx, kind); } +bool asahi_g17_trace_doorbell_enabled(void) +{ + return trace_asahi_g17_doorbell_enabled(); +} + +void asahi_g17_trace_doorbell(u8 engine, u32 power) +{ + trace_asahi_g17_doorbell(engine, power, __arch_counter_get_cntvct()); +} + void asahi_g17_trace_event(u8 phase) { if (trace_asahi_g17_event_enabled()) diff --git a/drivers/gpu/drm/asahi/g17/trace.h b/drivers/gpu/drm/asahi/g17/trace.h index e1ef55715e312..e00271249ce64 100644 --- a/drivers/gpu/drm/asahi/g17/trace.h +++ b/drivers/gpu/drm/asahi/g17/trace.h @@ -91,6 +91,23 @@ TRACE_EVENT(asahi_g17_bind, __entry->engine, __entry->qid, __entry->ctx, __entry->kind) ); +TRACE_EVENT(asahi_g17_doorbell, + TP_PROTO(u8 engine, u32 power, u64 counter), + TP_ARGS(engine, power, counter), + TP_STRUCT__entry( + __field(u8, engine) + __field(u32, power) + __field(u64, counter) + ), + TP_fast_assign( + __entry->engine = engine; + __entry->power = power; + __entry->counter = counter; + ), + TP_printk("engine=%u power=%#x counter=%llu", __entry->engine, __entry->power, + __entry->counter) +); + TRACE_EVENT(asahi_g17_event, TP_PROTO(u8 phase, u64 counter), TP_ARGS(phase, counter), diff --git a/drivers/gpu/drm/asahi/g17/trace.rs b/drivers/gpu/drm/asahi/g17/trace.rs index a616b68735225..d0150c7318958 100644 --- a/drivers/gpu/drm/asahi/g17/trace.rs +++ b/drivers/gpu/drm/asahi/g17/trace.rs @@ -12,6 +12,8 @@ unsafe extern "C" { fn asahi_g17_trace_complete(engine: u8, qid: u8, ordinal: u32, ctx: u32, seq: u64, gpu_start: u64, gpu_end: u64); fn asahi_g17_trace_bind(engine: u8, qid: u8, ctx: u32, kind: u8); + fn asahi_g17_trace_doorbell_enabled() -> bool; + fn asahi_g17_trace_doorbell(engine: u8, power: u32); fn asahi_g17_trace_event(phase: u8); } @@ -49,6 +51,17 @@ pub(super) fn bind(engine: u8, qid: u8, ctx: u32, kind: u8) { unsafe { asahi_g17_trace_bind(engine, qid, ctx, kind) }; } +/// A work doorbell is about to ring; `power` samples the core power state +/// only while the event is enabled. +pub(super) fn doorbell(engine: u8, power: impl FnOnce() -> u32) { + // SAFETY: Pure static-key query. + if !unsafe { asahi_g17_trace_doorbell_enabled() } { + return; + } + // SAFETY: Plain values only. + unsafe { asahi_g17_trace_doorbell(engine, power()) }; +} + /// Event worker pass boundaries: 0 begin, 1 fences signalled, 2 end. pub(super) fn event(phase: u8) { // SAFETY: Plain values only. diff --git a/drivers/gpu/drm/asahi/regs.rs b/drivers/gpu/drm/asahi/regs.rs index e7f6fd810b4ad..e8b845cba989f 100644 --- a/drivers/gpu/drm/asahi/regs.rs +++ b/drivers/gpu/drm/asahi/regs.rs @@ -709,6 +709,11 @@ impl G17Resources { self.sgx_read32::() & 0xf != 0 } + /// Raw core power state, for tracing only. + pub(crate) fn g17_core_power(&self) -> u32 { + self.sgx_read32::() + } + /// Enable dynamic power gating of idle G17 GPU cores. The firmware expects this before it is /// initialized. pub(crate) fn enable_g17_gating(&self) -> Result { From f381ac9b71ebcb70eb72497ec4115553856a31fc Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sat, 24 Oct 2026 00:32:18 -0400 Subject: [PATCH 038/388] drm/asahi: trace G17 host-to-firmware messages Add an asahi_g17_message tracepoint for every mailbox message, device-control record (by opcode) and effort update the host sends to the firmware, so a trace can show what the firmware was handling when a doorbell was slow to start. --- drivers/gpu/drm/asahi/g17/channel.rs | 1 + drivers/gpu/drm/asahi/g17/mod.rs | 2 ++ drivers/gpu/drm/asahi/g17/port.rs | 1 + drivers/gpu/drm/asahi/g17/trace.c | 6 ++++++ drivers/gpu/drm/asahi/g17/trace.h | 17 +++++++++++++++++ drivers/gpu/drm/asahi/g17/trace.rs | 12 ++++++++++++ 6 files changed, 39 insertions(+) diff --git a/drivers/gpu/drm/asahi/g17/channel.rs b/drivers/gpu/drm/asahi/g17/channel.rs index 8e4740b76a166..d96924cbbf9f9 100644 --- a/drivers/gpu/drm/asahi/g17/channel.rs +++ b/drivers/gpu/drm/asahi/g17/channel.rs @@ -138,6 +138,7 @@ impl Rings { record.opcode().asserts_gpu_power(), next, )?; + super::trace::message(super::trace::CONTROL, record.opcode() as u64); Ok(next) } } diff --git a/drivers/gpu/drm/asahi/g17/mod.rs b/drivers/gpu/drm/asahi/g17/mod.rs index 96a0991cfd143..0e29757a0c2cc 100644 --- a/drivers/gpu/drm/asahi/g17/mod.rs +++ b/drivers/gpu/drm/asahi/g17/mod.rs @@ -548,6 +548,7 @@ impl Coprocessor { /// Sends a host notification to the firmware. fn notify(&self, msg: u64) -> Result { + trace::message(trace::MAILBOX, msg); self.rtkit.send(EP_DOORBELL, msg) } @@ -706,6 +707,7 @@ impl Firmware { return Ok(()); } init.publish_effort(effort)?; + trace::message(trace::EFFORT, u64::from(effort)); state.did_apply(effort); port.set_effort_full(effort == power::FULL_EFFORT); Ok(()) diff --git a/drivers/gpu/drm/asahi/g17/port.rs b/drivers/gpu/drm/asahi/g17/port.rs index 146b83972f203..d8f7f4ddff8a1 100644 --- a/drivers/gpu/drm/asahi/g17/port.rs +++ b/drivers/gpu/drm/asahi/g17/port.rs @@ -350,6 +350,7 @@ impl Port { super::trace::doorbell(super::trace::COMPUTE, || { self.regs.as_ref().map_or(0, |regs| regs.g17_core_power()) }); + super::trace::message(super::trace::MAILBOX, message); mailbox.send(super::EP_DOORBELL, message) } diff --git a/drivers/gpu/drm/asahi/g17/trace.c b/drivers/gpu/drm/asahi/g17/trace.c index fc189458af32d..ae977c13aab3b 100644 --- a/drivers/gpu/drm/asahi/g17/trace.c +++ b/drivers/gpu/drm/asahi/g17/trace.c @@ -41,6 +41,12 @@ void asahi_g17_trace_doorbell(u8 engine, u32 power) trace_asahi_g17_doorbell(engine, power, __arch_counter_get_cntvct()); } +void asahi_g17_trace_message(u8 kind, u64 value) +{ + if (trace_asahi_g17_message_enabled()) + trace_asahi_g17_message(kind, value, __arch_counter_get_cntvct()); +} + void asahi_g17_trace_event(u8 phase) { if (trace_asahi_g17_event_enabled()) diff --git a/drivers/gpu/drm/asahi/g17/trace.h b/drivers/gpu/drm/asahi/g17/trace.h index e00271249ce64..7e5a50e774b59 100644 --- a/drivers/gpu/drm/asahi/g17/trace.h +++ b/drivers/gpu/drm/asahi/g17/trace.h @@ -108,6 +108,23 @@ TRACE_EVENT(asahi_g17_doorbell, __entry->counter) ); +TRACE_EVENT(asahi_g17_message, + TP_PROTO(u8 kind, u64 value, u64 counter), + TP_ARGS(kind, value, counter), + TP_STRUCT__entry( + __field(u8, kind) + __field(u64, value) + __field(u64, counter) + ), + TP_fast_assign( + __entry->kind = kind; + __entry->value = value; + __entry->counter = counter; + ), + TP_printk("kind=%u value=%#llx counter=%llu", __entry->kind, __entry->value, + __entry->counter) +); + TRACE_EVENT(asahi_g17_event, TP_PROTO(u8 phase, u64 counter), TP_ARGS(phase, counter), diff --git a/drivers/gpu/drm/asahi/g17/trace.rs b/drivers/gpu/drm/asahi/g17/trace.rs index d0150c7318958..57db5abaccef8 100644 --- a/drivers/gpu/drm/asahi/g17/trace.rs +++ b/drivers/gpu/drm/asahi/g17/trace.rs @@ -12,6 +12,7 @@ unsafe extern "C" { fn asahi_g17_trace_complete(engine: u8, qid: u8, ordinal: u32, ctx: u32, seq: u64, gpu_start: u64, gpu_end: u64); fn asahi_g17_trace_bind(engine: u8, qid: u8, ctx: u32, kind: u8); + fn asahi_g17_trace_message(kind: u8, value: u64); fn asahi_g17_trace_doorbell_enabled() -> bool; fn asahi_g17_trace_doorbell(engine: u8, power: u32); fn asahi_g17_trace_event(phase: u8); @@ -62,6 +63,17 @@ pub(super) fn doorbell(engine: u8, power: impl FnOnce() -> u32) { unsafe { asahi_g17_trace_doorbell(engine, power()) }; } +pub(super) const MAILBOX: u8 = 0; +pub(super) const CONTROL: u8 = 1; +pub(super) const EFFORT: u8 = 2; + +/// A host-to-firmware message: a mailbox message, a device-control record by +/// opcode, or a new performance effort value. +pub(super) fn message(kind: u8, value: u64) { + // SAFETY: Plain values only. + unsafe { asahi_g17_trace_message(kind, value) }; +} + /// Event worker pass boundaries: 0 begin, 1 fences signalled, 2 end. pub(super) fn event(phase: u8) { // SAFETY: Plain values only. From adcd56e020362664c7bc3af288e1ad812df7b4c7 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sat, 24 Oct 2026 02:09:59 -0400 Subject: [PATCH 039/388] drm/asahi: maintain cumulative completion scopes without repeated fence merges Publish queue-scoped completion outputs with bounded callback bookkeeping while preserving independent GPU execution and native input dependencies. Signal ready scopes iteratively, propagate errors and deadline hints, and release completed history. Validated on Neo with the full benchmark and stress gate, published software-fence ordering, 256 queued completions, deadline forwarding and injected errors. Same-boot compute submission CPU drops from about 14 us to 10 us; GPU throughput remains near 20 us. --- drivers/gpu/drm/asahi/Makefile | 2 +- drivers/gpu/drm/asahi/file.rs | 45 +++- drivers/gpu/drm/asahi/g17/fence.rs | 42 ++- drivers/gpu/drm/asahi/g17/mod.rs | 4 +- drivers/gpu/drm/asahi/g17/sync_input.c | 72 ++++-- drivers/gpu/drm/asahi/g17/sync_output.c | 138 ++++++++++ drivers/gpu/drm/asahi/g17/sync_output.h | 23 ++ drivers/gpu/drm/asahi/g17/sync_output.rs | 69 +++++ drivers/gpu/drm/asahi/g17/sync_scope.c | 315 +++++++++++++++++++++++ drivers/gpu/drm/asahi/g17/sync_scope.h | 15 ++ include/uapi/drm/asahi_drm.h | 17 ++ rust/kernel/drm/syncobj.rs | 8 + 12 files changed, 719 insertions(+), 31 deletions(-) create mode 100644 drivers/gpu/drm/asahi/g17/sync_output.c create mode 100644 drivers/gpu/drm/asahi/g17/sync_output.h create mode 100644 drivers/gpu/drm/asahi/g17/sync_output.rs create mode 100644 drivers/gpu/drm/asahi/g17/sync_scope.c create mode 100644 drivers/gpu/drm/asahi/g17/sync_scope.h diff --git a/drivers/gpu/drm/asahi/Makefile b/drivers/gpu/drm/asahi/Makefile index 9edbc6ba3b8d4..6d15b73fc4739 100644 --- a/drivers/gpu/drm/asahi/Makefile +++ b/drivers/gpu/drm/asahi/Makefile @@ -1,6 +1,6 @@ # SPDX-License-Identifier: GPL-2.0 obj-$(CONFIG_DRM_ASAHI) += asahi.o -asahi-y := asahi_core.o sysfs.o g17/sync_input.o g17/trace.o +asahi-y := asahi_core.o sysfs.o g17/sync_input.o g17/sync_output.o g17/sync_scope.o g17/trace.o CFLAGS_g17/trace.o := -I$(src)/g17 asahi-$(CONFIG_DRM_ASAHI_G16) += g16/glue.o diff --git a/drivers/gpu/drm/asahi/file.rs b/drivers/gpu/drm/asahi/file.rs index d1b8084902bcc..9c662ab6b85a2 100644 --- a/drivers/gpu/drm/asahi/file.rs +++ b/drivers/gpu/drm/asahi/file.rs @@ -127,10 +127,18 @@ pub(crate) struct SyncItem { pub(crate) fence: Option, pub(crate) chain_fence: Option, pub(crate) timeline_value: u64, + pub(crate) cumulative: Option, + pub(crate) timeline: bool, } impl SyncItem { - fn parse_one(file: &DrmFile, data: uapi::drm_asahi_sync, out: bool) -> Result { + fn parse_one(file: &DrmFile, mut data: uapi::drm_asahi_sync, out: bool, + bundled: bool) -> Result + { + if data.sync_type == crate::g17::sync_output::SYNC_CUMULATIVE { + if !out || !bundled || data.timeline_value == 0 { return Err(EINVAL); } + data.sync_type = uapi::drm_asahi_sync_type_DRM_ASAHI_SYNC_TIMELINE_SYNCOBJ; + } match data.sync_type { uapi::drm_asahi_sync_type_DRM_ASAHI_SYNC_SYNCOBJ => { if data.timeline_value != 0 { @@ -151,6 +159,8 @@ impl SyncItem { syncobj, chain_fence: None, timeline_value: data.timeline_value, + cumulative: None, + timeline: false, }) } uapi::drm_asahi_sync_type_DRM_ASAHI_SYNC_TIMELINE_SYNCOBJ => { @@ -173,12 +183,10 @@ impl SyncItem { Ok(SyncItem { fence, syncobj, - chain_fence: if out { - Some(dma_fence::FenceChain::new()?) - } else { - None - }, + chain_fence: None, timeline_value: data.timeline_value, + cumulative: None, + timeline: true, }) } _ => { @@ -193,12 +201,14 @@ impl SyncItem { ptr: u64, in_count: u32, out_count: u32, + allow_cumulative: bool, ) -> Result> { let count = in_count.checked_add(out_count).ok_or(EOVERFLOW)?; const STRIDE: usize = core::mem::size_of::(); let size = STRIDE.checked_mul(count as usize).ok_or(EOVERFLOW)?; let mut vec = KVec::with_capacity(count as usize, GFP_KERNEL)?; + let mut anchor = None; // SAFETY: We only read this once, so there are no TOCTOU issues. let mut reader = UserSlice::new(UserPtr::from_addr(ptr as _), size).reader(); @@ -214,7 +224,25 @@ impl SyncItem { // SAFETY: All bit patterns in the struct are valid let sync = unsafe { sync.assume_init() }; - vec.push(SyncItem::parse_one(file, sync, i >= in_count)?, GFP_KERNEL)?; + if sync.sync_type == crate::g17::sync_output::SYNC_CUMULATIVE { + if !allow_cumulative || i < in_count || anchor.is_some() { + return Err(EINVAL); + } + anchor = Some(i as usize); + } + vec.push(Self::parse_one(file, sync, i >= in_count, + allow_cumulative)?, GFP_KERNEL)?; + } + if let Some(anchor) = anchor { + let bundle = crate::g17::sync_output::SignalBundle::new( + &vec[in_count as usize..], anchor - in_count as usize)?; + vec[anchor].cumulative = Some(bundle); + } else { + for sync in &mut vec[in_count as usize..] { + if sync.timeline { + sync.chain_fence = Some(dma_fence::FenceChain::new()?); + } + } } Ok(vec) @@ -1396,7 +1424,8 @@ impl File { copy_commands(&mut vec)?; } let syncs = - SyncItem::parse_array(file, data.syncs, data.in_sync_count, data.out_sync_count)?; + SyncItem::parse_array(file, data.syncs, data.in_sync_count, data.out_sync_count, + gpu.as_any().is::())?; mod_dev_dbg!( device, diff --git a/drivers/gpu/drm/asahi/g17/fence.rs b/drivers/gpu/drm/asahi/g17/fence.rs index da883cede52df..8245e8bca71e6 100644 --- a/drivers/gpu/drm/asahi/g17/fence.rs +++ b/drivers/gpu/drm/asahi/g17/fence.rs @@ -19,6 +19,16 @@ use kernel::{ pub(crate) struct CompletionFence; +unsafe extern "C" { + fn asahi_g17_signal_fence(fence: *mut bindings::dma_fence); +} + +fn signal_exported(fence: &dyn RawDmaFence) { + // SAFETY: The borrowed fence remains owned throughout synchronous signaling. + // Ready completion scopes are drained iteratively after its lock is released. + unsafe { asahi_g17_signal_fence(fence.raw()) }; +} + #[vtable] impl FenceOps for CompletionFence { fn get_driver_name<'a>(self: &'a FenceObject) -> &'a CStr { @@ -52,6 +62,7 @@ pub(crate) fn allocation_failure(contexts: &FenceContexts) -> Result { /// Submission never owns this fence, so exported references cannot form a cycle. pub(crate) struct ExportFence { source: Option>, + scoped: bool, } static EXPORT_OPS: AtomicUsize = AtomicUsize::new(0); @@ -65,7 +76,7 @@ impl FenceOps for ExportFence { #[inline(never)] fn export_fallback(contexts: &FenceContexts) -> Result> { - let fence = contexts.new_fence(0, ExportFence { source: None })?; + let fence = contexts.new_fence(0, ExportFence { source: None, scoped: false })?; // SAFETY: Unique, initialized and still private. Only this constructor // publishes ExportFence objects; its payload is immutable after sharing. let ops = unsafe { @@ -179,7 +190,7 @@ impl Submission { let error = self.error.load(Ordering::Acquire); if error != 0 { fence.set_error(Error::from_errno(error)); } else { self.release_state(); } - fence.signal(); + if fence.scoped { signal_exported(fence); } else { fence.signal(); } } } } @@ -332,30 +343,42 @@ pub(crate) struct Outputs { input_count: usize, submission: Option, result: Result, + cumulative: Option, } impl Outputs { pub(crate) fn new(contexts: &FenceContexts, mut syncs: KVec, input_count: usize, allocation_failure: &Fence) -> Result { if input_count > syncs.len() { return Err(EINVAL); } + let mut cumulative = syncs.iter_mut().find_map(|sync| sync.cumulative.take()); let fallback = match export_fallback(contexts) { Ok(fence) => fence, Err(error) => { - install(syncs.drain(input_count..), allocation_failure); + if let Some(bundle) = &mut cumulative { + bundle.fail(allocation_failure); + } else { + install(syncs.drain(input_count..), allocation_failure); + } return Err(error); } }; Ok(Self { fallback: Some(fallback), syncs, input_count, - submission: None, result: Err(ECANCELED) }) + submission: None, result: Err(ECANCELED), cumulative }) } pub(crate) fn inputs(&self) -> &[SyncItem] { &self.syncs[..self.input_count] } pub(crate) fn publish(&mut self, context: Arc, firmware_source: bool) -> Result { let source = Submission::new(context, firmware_source)?; let mut fence = self.fallback.take().ok_or(EINVAL)?; fence.source = Some(source); + fence.scoped = self.cumulative.is_some(); let submission = Aggregate(fence.into()); - install(self.syncs.drain(self.input_count..), &submission.fence()); self.submission = Some(submission.clone()); + if let Some(bundle) = &mut self.cumulative { + bundle.publish(&submission.fence())?; + self.syncs.truncate(self.input_count); + } else { + install(self.syncs.drain(self.input_count..), &submission.fence()); + } Ok(submission) } pub(crate) fn finish(mut self, result: Result) -> Result { self.result = result; result } @@ -363,12 +386,19 @@ impl Outputs { impl Drop for Outputs { fn drop(&mut self) { if let Some(submission) = self.submission.take() { + if let Some(bundle) = &mut self.cumulative { + bundle.fail(&submission.fence()); + } submission.finish_enqueue(self.result); return; } if let Some(fallback) = self.fallback.take() { if let Err(error) = self.result { fallback.set_error(error); } - install(self.syncs.drain(self.input_count..), &Fence::from_fence(&fallback)); + if let Some(bundle) = &mut self.cumulative { + bundle.fail(&Fence::from_fence(&fallback)); + } else { + install(self.syncs.drain(self.input_count..), &Fence::from_fence(&fallback)); + } fallback.signal(); } } diff --git a/drivers/gpu/drm/asahi/g17/mod.rs b/drivers/gpu/drm/asahi/g17/mod.rs index 0e29757a0c2cc..8c6891785f1c2 100644 --- a/drivers/gpu/drm/asahi/g17/mod.rs +++ b/drivers/gpu/drm/asahi/g17/mod.rs @@ -31,6 +31,7 @@ pub(crate) mod context; mod data_segment; mod dependency; mod sync_input; +pub(crate) mod sync_output; mod trace; mod event; mod feed; @@ -1499,7 +1500,8 @@ impl gpu::Gpu for Gpu { u64::from(uapi::drm_asahi_feature_DRM_ASAHI_FEATURE_PER_COMMAND_INPUT_SYNCS) | u64::from(uapi::drm_asahi_feature_DRM_ASAHI_FEATURE_EXACT_PRIOR_BARRIERS) | u64::from(uapi::drm_asahi_feature_DRM_ASAHI_FEATURE_FRAGMENT_BARRIERS) - | u64::from(uapi::drm_asahi_feature_DRM_ASAHI_FEATURE_BOUNDED_AS_BUILD); + | u64::from(uapi::drm_asahi_feature_DRM_ASAHI_FEATURE_BOUNDED_AS_BUILD) + | sync_output::FEATURE_CUMULATIVE_SIGNALS; let variant = self.shared.variant; if variant.few_primitives { params.features |= diff --git a/drivers/gpu/drm/asahi/g17/sync_input.c b/drivers/gpu/drm/asahi/g17/sync_input.c index e6b91988c3c28..19d5b8b92f934 100644 --- a/drivers/gpu/drm/asahi/g17/sync_input.c +++ b/drivers/gpu/drm/asahi/g17/sync_input.c @@ -3,31 +3,73 @@ #include #include #include "sync_input.h" +#include "sync_scope.h" + +static int collect_leaf(struct dma_fence *leaf, + struct asahi_g17_fence_leaf *out, size_t capacity, + size_t *count) +{ + if (*count == capacity) + return -ENOSPC; + out[*count].fence = dma_fence_get(leaf); + out[*count].status = dma_fence_get_status(leaf); + ++*count; + return 0; +} /* Called without the device mutex: status polling may run fence callbacks. */ int asahi_g17_collect_fences(struct dma_fence *head, struct asahi_g17_fence_leaf *out, size_t capacity) { - struct dma_fence_unwrap cursor; - struct dma_fence *leaf; - size_t count = 0; + struct dma_fence *pending[128]; + size_t count = 0, depth = 1; int error = 0; - dma_fence_unwrap_for_each(leaf, &cursor, head) { - if (dma_fence_is_container(leaf)) { - error = -EOPNOTSUPP; - break; + pending[0] = dma_fence_get(head); + while (depth && !error) { + struct dma_fence *node = pending[--depth]; + struct dma_fence_unwrap cursor; + struct dma_fence *leaf; + + if (asahi_g17_is_scope(node)) { + struct dma_fence *children[2]; + unsigned int n = asahi_g17_scope_children(node, children), i; + if (!n) { + /* Preserve a failed scope as a failed leaf, never drop its error. */ + if (dma_fence_get_status(node) < 0) + error = collect_leaf(node, out, capacity, &count); + } else if (depth + n > ARRAY_SIZE(pending)) { + for (i = 0; i < n; ++i) + dma_fence_put(children[i]); + error = -ENOSPC; + } else { + for (i = 0; i < n; ++i) + pending[depth++] = children[i]; + } + dma_fence_put(node); + continue; } - if (count == capacity) { - error = -ENOSPC; - break; + + dma_fence_unwrap_for_each(leaf, &cursor, node) { + if (asahi_g17_is_scope(leaf)) { + if (depth == ARRAY_SIZE(pending)) + error = -ENOSPC; + else + pending[depth++] = dma_fence_get(leaf); + } else if (dma_fence_is_container(leaf)) { + error = -EOPNOTSUPP; + } else { + error = collect_leaf(leaf, out, capacity, &count); + } + if (error) + break; } - out[count].fence = dma_fence_get(leaf); - out[count].status = dma_fence_get_status(leaf); - ++count; + /* A bounded early exit still owns the iterator's current chain. */ + dma_fence_put(cursor.chain); + dma_fence_put(node); } - /* A bounded early exit still owns the iterator's current chain. */ - dma_fence_put(cursor.chain); + while (depth) + dma_fence_put(pending[--depth]); if (error) { while (count) dma_fence_put(out[--count].fence); diff --git a/drivers/gpu/drm/asahi/g17/sync_output.c b/drivers/gpu/drm/asahi/g17/sync_output.c new file mode 100644 index 0000000000000..08b4a95f26c9b --- /dev/null +++ b/drivers/gpu/drm/asahi/g17/sync_output.c @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +#include +#include +#include +#include +#include "sync_output.h" +#include "sync_scope.h" + +struct asahi_g17_signal_target { + struct drm_syncobj *syncobj; + struct dma_fence_chain *chain; + u64 point; +}; + +struct asahi_g17_signal_bundle { + size_t count; + size_t anchor; + bool published; + struct asahi_g17_signal_target targets[]; +}; + +void asahi_g17_signals_free(struct asahi_g17_signal_bundle *bundle) +{ + size_t i; + + if (!bundle) + return; + for (i = 0; i < bundle->count; ++i) { + struct asahi_g17_signal_target *target = &bundle->targets[i]; + if (target->chain) + dma_fence_chain_free(target->chain); + if (target->syncobj) + drm_syncobj_put(target->syncobj); + } + kfree(bundle); +} + +/* The Rust caller supplies its validated, immutable snapshot, never a user pointer. */ +struct asahi_g17_signal_bundle * +asahi_g17_signals_new(const struct asahi_g17_signal_spec *syncs, + size_t count, size_t anchor, int *error) +{ + struct asahi_g17_signal_bundle *bundle; + size_t i; + + *error = -EINVAL; + if (!count || anchor >= count || !syncs[anchor].point) + return NULL; + *error = -ENOMEM; + bundle = kzalloc(struct_size(bundle, targets, count), GFP_KERNEL); + if (!bundle) + return NULL; + bundle->count = count; + bundle->anchor = anchor; + for (i = 0; i < count; ++i) { + struct asahi_g17_signal_target *target = &bundle->targets[i]; + target->syncobj = syncs[i].syncobj; + if (target->syncobj) + drm_syncobj_get(target->syncobj); + if (!target->syncobj) { + *error = -ENOENT; + goto fail; + } + target->point = syncs[i].point; + if (syncs[i].timeline) { + target->chain = dma_fence_chain_alloc(); + if (!target->chain) + goto fail; + } + if (i != anchor && target->syncobj == bundle->targets[anchor].syncobj) { + *error = -EINVAL; + goto fail; + } + } + /* The anchor may follow the other outputs. Reject aliases in either order. */ + for (i = 0; i < count; ++i) { + if (i != anchor && bundle->targets[i].syncobj == bundle->targets[anchor].syncobj) { + *error = -EINVAL; + goto fail; + } + } + *error = 0; + return bundle; +fail: + asahi_g17_signals_free(bundle); + return NULL; +} + +static void asahi_g17_signal_install(struct asahi_g17_signal_target *target, + struct dma_fence *fence) +{ + if (target->chain) { + drm_syncobj_add_point(target->syncobj, target->chain, fence, target->point); + target->chain = NULL; + } else { + drm_syncobj_replace_fence(target->syncobj, fence); + } +} + +int asahi_g17_signals_publish(struct asahi_g17_signal_bundle *bundle, + struct dma_fence *submitted) +{ + struct dma_fence *previous, *scope; + size_t i; + + if (bundle->published) + return -EINVAL; + previous = drm_syncobj_fence_get(bundle->targets[bundle->anchor].syncobj); + scope = asahi_g17_scope_create(previous, submitted, + bundle->targets[bundle->anchor].point); + dma_fence_put(previous); + if (!scope) + return -ENOMEM; + /* Only exported completion includes earlier work. GPU execution gets no new wait. */ + asahi_g17_signal_install(&bundle->targets[bundle->anchor], scope); + previous = drm_syncobj_fence_get(bundle->targets[bundle->anchor].syncobj); + asahi_g17_scope_bind(scope, previous); + dma_fence_put(previous); + for (i = 0; i < bundle->count; ++i) + if (i != bundle->anchor) + asahi_g17_signal_install(&bundle->targets[i], scope); + bundle->published = true; + dma_fence_put(scope); + return 0; +} + +/* Allocation or validation failed before enqueue: install an error promise without allocating. */ +void asahi_g17_signals_fail(struct asahi_g17_signal_bundle *bundle, + struct dma_fence *failure) +{ + size_t i; + + if (bundle->published) + return; + for (i = 0; i < bundle->count; ++i) + asahi_g17_signal_install(&bundle->targets[i], failure); + bundle->published = true; +} diff --git a/drivers/gpu/drm/asahi/g17/sync_output.h b/drivers/gpu/drm/asahi/g17/sync_output.h new file mode 100644 index 0000000000000..a752d9ceaf4b0 --- /dev/null +++ b/drivers/gpu/drm/asahi/g17/sync_output.h @@ -0,0 +1,23 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ +#ifndef ASAHI_G17_SYNC_OUTPUT_H +#define ASAHI_G17_SYNC_OUTPUT_H +#include + +struct dma_fence; +struct drm_syncobj; +struct asahi_g17_signal_bundle; +struct asahi_g17_signal_spec { + struct drm_syncobj *syncobj; + u64 point; + u32 timeline; +}; + +struct asahi_g17_signal_bundle * +asahi_g17_signals_new(const struct asahi_g17_signal_spec *specs, + size_t count, size_t anchor, int *error); +void asahi_g17_signals_free(struct asahi_g17_signal_bundle *bundle); +int asahi_g17_signals_publish(struct asahi_g17_signal_bundle *bundle, + struct dma_fence *submitted); +void asahi_g17_signals_fail(struct asahi_g17_signal_bundle *bundle, + struct dma_fence *failure); +#endif diff --git a/drivers/gpu/drm/asahi/g17/sync_output.rs b/drivers/gpu/drm/asahi/g17/sync_output.rs new file mode 100644 index 0000000000000..50585befb3b71 --- /dev/null +++ b/drivers/gpu/drm/asahi/g17/sync_output.rs @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +//! Atomic preparation of a queue completion scope and its exported signals. + +use kernel::{bindings, dma_fence::{Fence, RawDmaFence}, prelude::*}; + +// Wire values also documented in include/uapi/drm/asahi_drm.h. Keeping these +// local permits building this module against an older kernel's Rust bindings. +pub(crate) const SYNC_CUMULATIVE: u32 = 2; +pub(crate) const FEATURE_CUMULATIVE_SIGNALS: u64 = 1 << 17; + +unsafe extern "C" { + fn asahi_g17_signals_new(syncs: *const SignalSpec, count: usize, anchor: usize, + error: *mut i32) -> *mut core::ffi::c_void; + fn asahi_g17_signals_free(bundle: *mut core::ffi::c_void); + fn asahi_g17_signals_publish(bundle: *mut core::ffi::c_void, + current: *mut bindings::dma_fence) -> i32; + fn asahi_g17_signals_fail(bundle: *mut core::ffi::c_void, + failure: *mut bindings::dma_fence); +} + +#[repr(C)] +struct SignalSpec { + syncobj: *mut core::ffi::c_void, + point: u64, + timeline: u32, +} + +/// Owns syncobj references and preallocated chain nodes; never owns user pointers. +pub(crate) struct SignalBundle(*mut core::ffi::c_void); + +impl SignalBundle { + /// C retains references before the borrowed sync items can be dropped. + pub(crate) fn new(syncs: &[crate::file::SyncItem], anchor: usize) -> Result + { + let mut specs = KVec::with_capacity(syncs.len(), GFP_KERNEL)?; + for sync in syncs { + specs.push(SignalSpec { + syncobj: sync.syncobj.as_raw().cast(), point: sync.timeline_value, + timeline: u32::from(sync.timeline), + }, GFP_KERNEL)?; + } + let mut error = 0; + // SAFETY: All entries borrow live sync objects; C copies the entries and + // acquires its own references before returning. + let raw = unsafe { asahi_g17_signals_new( + specs.as_ptr(), specs.len(), anchor, &mut error) }; + if raw.is_null() { return Err(Error::from_errno(error)); } + Ok(Self(raw)) + } + + pub(crate) fn publish(&mut self, current: &Fence) -> Result { + // SAFETY: The bundle and current fence are owned for this call; C takes + // its own references on publication and reports failure before any store. + let error = unsafe { asahi_g17_signals_publish(self.0, current.raw()) }; + if error == 0 { Ok(()) } else { Err(Error::from_errno(error)) } + } + + pub(crate) fn fail(&mut self, failure: &Fence) { + // SAFETY: Live owned arguments; this only consumes uninstalled chain nodes. + unsafe { asahi_g17_signals_fail(self.0, failure.raw()) }; + } +} + +impl Drop for SignalBundle { + fn drop(&mut self) { + // SAFETY: Exactly one owner; published syncobjs retain their own fences. + unsafe { asahi_g17_signals_free(self.0) }; + } +} diff --git a/drivers/gpu/drm/asahi/g17/sync_scope.c b/drivers/gpu/drm/asahi/g17/sync_scope.c new file mode 100644 index 0000000000000..55feb19a7f83d --- /dev/null +++ b/drivers/gpu/drm/asahi/g17/sync_scope.c @@ -0,0 +1,315 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "sync_scope.h" +#include "sync_input.h" + +struct asahi_g17_scope; +struct asahi_g17_scope_callback { + struct dma_fence_cb callback; + struct asahi_g17_scope *scope; +}; + +struct asahi_g17_scope { + struct dma_fence base; + atomic_t pending; + struct dma_fence *children[2]; + struct asahi_g17_scope_callback callbacks[2]; + struct irq_work work; + struct llist_node ready; + /* Identifies the exact anchor point this scope replaced. */ + u64 anchor_context; + u64 anchor_point; +}; + +static const struct dma_fence_ops scope_ops; +static DEFINE_PER_CPU(struct llist_head *, active_scopes); +#define SCOPE_ATOMIC_BATCH 64 + +bool asahi_g17_is_scope(struct dma_fence *fence) +{ + return fence && fence->ops == &scope_ops; +} + +static struct asahi_g17_scope *to_scope(struct dma_fence *fence) +{ + return container_of(fence, struct asahi_g17_scope, base); +} + +static const char *scope_driver_name(struct dma_fence *fence) +{ + return "asahi"; +} + +static const char *scope_timeline_name(struct dma_fence *fence) +{ + return "g17-queue-scope"; +} + +static bool scope_signaled(struct dma_fence *fence) +{ + struct asahi_g17_scope *scope = to_scope(fence); + if (atomic_read_acquire(&scope->pending)) + return false; + cmpxchg(&fence->error, 1, 0); + return true; +} + +static bool scope_enable_signaling(struct dma_fence *fence) +{ + /* Child callbacks are already armed; no recursive callback installation. */ + return !scope_signaled(fence); +} + +unsigned int asahi_g17_scope_children(struct dma_fence *fence, + struct dma_fence *children[2]) +{ + struct asahi_g17_scope *scope = to_scope(fence); + unsigned long flags; + unsigned int i, count = 0; + + spin_lock_irqsave(&fence->inline_lock, flags); + if (!test_bit(DMA_FENCE_FLAG_SIGNALED_BIT, &fence->flags)) + for (i = 0; i < ARRAY_SIZE(scope->children); ++i) + if (scope->children[i]) + children[count++] = dma_fence_get(scope->children[i]); + spin_unlock_irqrestore(&fence->inline_lock, flags); + return count; +} + +static void scope_release_completed(struct asahi_g17_scope *scope) +{ + struct dma_fence *children[2]; + unsigned long flags; + unsigned int i; + + /* Readers take references under this lock. Signalled scopes need no history. */ + spin_lock_irqsave(&scope->base.inline_lock, flags); + for (i = 0; i < ARRAY_SIZE(children); ++i) { + children[i] = scope->children[i]; + scope->children[i] = NULL; + } + spin_unlock_irqrestore(&scope->base.inline_lock, flags); + for (i = 0; i < ARRAY_SIZE(children); ++i) + dma_fence_put(children[i]); + dma_fence_put(&scope->base); +} + +static void scope_complete(struct asahi_g17_scope *scope) +{ + cmpxchg(&scope->base.error, 1, 0); + dma_fence_signal(&scope->base); + scope_release_completed(scope); +} + +static void scope_work(struct irq_work *work) +{ + struct asahi_g17_scope *scope = container_of(work, struct asahi_g17_scope, work); + cmpxchg(&scope->base.error, 1, 0); + asahi_g17_signal_fence(&scope->base); + scope_release_completed(scope); +} + +static void scope_defer_list(struct llist_node *nodes) +{ + struct llist_node *node, *next; + llist_for_each_safe(node, next, nodes) { + struct asahi_g17_scope *scope = + container_of(node, struct asahi_g17_scope, ready); + /* Transfer the ready-list reference to the irq_work callback. */ + irq_work_queue(&scope->work); + } +} + +void asahi_g17_signal_fence(struct dma_fence *fence) +{ + struct llist_head ready = LLIST_HEAD_INIT(ready); + struct llist_node *nodes, *node, *next; + unsigned long flags; + unsigned int completed = 0; + + /* Fence callbacks are atomic. Keep the local ready list pinned while + * signaling, then finish ready scopes after the source fence is unlocked. + * New ready scopes append to this list instead of recursing or sending IPIs. + */ + preempt_disable(); + if (__this_cpu_read(active_scopes)) { + dma_fence_signal(fence); + preempt_enable(); + return; + } + __this_cpu_write(active_scopes, &ready); + dma_fence_signal(fence); + for (;;) { + /* An interrupt must not append after the final empty test. */ + local_irq_save(flags); + nodes = llist_del_all(&ready); + if (!nodes) + __this_cpu_write(active_scopes, NULL); + local_irq_restore(flags); + if (!nodes) + break; + llist_for_each_safe(node, next, nodes) { + if (completed == SCOPE_ATOMIC_BATCH) { + struct llist_node *more; + local_irq_save(flags); + more = llist_del_all(&ready); + __this_cpu_write(active_scopes, NULL); + local_irq_restore(flags); + preempt_enable(); + scope_defer_list(node); + scope_defer_list(more); + return; + } + scope_complete(container_of(node, struct asahi_g17_scope, ready)); + ++completed; + } + } + preempt_enable(); +} + +static void scope_arrived(struct asahi_g17_scope *scope, int error) +{ + if (error < 0) + cmpxchg(&scope->base.error, 1, error); + if (atomic_dec_and_test(&scope->pending)) { + struct llist_head *ready; + dma_fence_get(&scope->base); + preempt_disable(); + ready = __this_cpu_read(active_scopes); + if (ready) + llist_add(&scope->ready, ready); + preempt_enable(); + /* Foreign fence signalers have no driver completion pass to drain us. */ + if (!ready) + irq_work_queue(&scope->work); + } +} + +static void scope_callback(struct dma_fence *child, struct dma_fence_cb *callback) +{ + struct asahi_g17_scope_callback *cb = + container_of(callback, typeof(*cb), callback); + scope_arrived(cb->scope, child->error); + dma_fence_put(&cb->scope->base); +} + +static void scope_release(struct dma_fence *fence) +{ + struct asahi_g17_scope *scope = to_scope(fence); + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(scope->children); ++i) + dma_fence_put(scope->children[i]); + dma_fence_free(fence); +} + +static void scope_set_deadline(struct dma_fence *fence, ktime_t deadline) +{ + struct asahi_g17_fence_leaf *leaves; + int count, i; + + if (dma_fence_is_signaled(fence)) + return; + /* A deadline is an advisory hint. Keep its traversal bounded and iterative, + * including when a foreign timeline boundary contains earlier scopes. + */ + leaves = kmalloc_array(128, sizeof(*leaves), GFP_ATOMIC | __GFP_NOWARN); + if (!leaves) + return; + count = asahi_g17_collect_fences(fence, leaves, 128); + for (i = 0; i < count; ++i) { + if (!asahi_g17_is_scope(leaves[i].fence)) + dma_fence_set_deadline(leaves[i].fence, deadline); + dma_fence_put(leaves[i].fence); + } + kfree(leaves); +} + +static const struct dma_fence_ops scope_ops = { + .get_driver_name = scope_driver_name, + .get_timeline_name = scope_timeline_name, + .enable_signaling = scope_enable_signaling, + .signaled = scope_signaled, + .release = scope_release, + .set_deadline = scope_set_deadline, +}; + +void asahi_g17_scope_bind(struct dma_fence *fence, struct dma_fence *root) +{ + struct asahi_g17_scope *scope = to_scope(fence); + /* A concurrent raw syncobj reset is invalid for the private queue anchor, + * but must not turn a hostile userspace race into a kernel dereference. + */ + if (!root || dma_fence_chain_contained(root) != fence) + return; + /* Written once before the submit returns; an unmatched snapshot takes fallback. */ + WRITE_ONCE(scope->anchor_point, root->seqno); + WRITE_ONCE(scope->anchor_context, root->context); +} + +struct dma_fence *asahi_g17_scope_create(struct dma_fence *previous, + struct dma_fence *submitted, u64 point) +{ + struct dma_fence *contained = previous ? dma_fence_chain_contained(previous) : NULL; + struct dma_fence *parent = NULL; + struct asahi_g17_scope *scope, *prior = NULL; + u64 context; + unsigned int i; + + if (asahi_g17_is_scope(contained)) { + prior = to_scope(contained); + if (READ_ONCE(prior->anchor_context) != previous->context || + READ_ONCE(prior->anchor_point) != previous->seqno || + point <= contained->seqno) + prior = NULL; + } + if (prior) { + parent = dma_fence_get(contained); + context = contained->context; + } else { + /* Empty submissions or external edits may insert a foreign timeline point. + * Flatten that boundary once; the ordinary path retains just the prior scope. + */ + if (previous) { + parent = dma_fence_unwrap_merge(previous); + if (!parent) + return NULL; + } + context = dma_fence_context_alloc(1); + } + scope = kzalloc_obj(*scope); + if (!scope) { + dma_fence_put(parent); + return NULL; + } + dma_fence_init64(&scope->base, &scope_ops, NULL, context, point); + init_irq_work(&scope->work, scope_work); + scope->children[0] = parent; + scope->children[1] = dma_fence_get(submitted); + /* The constructor guard prevents completion while callbacks are being installed. */ + atomic_set(&scope->pending, 3); + scope->base.error = 1; + for (i = 0; i < ARRAY_SIZE(scope->children); ++i) { + struct dma_fence *child = scope->children[i]; + if (!child) { + scope_arrived(scope, 0); + continue; + } + scope->callbacks[i].scope = scope; + dma_fence_get(&scope->base); + if (dma_fence_add_callback(child, &scope->callbacks[i].callback, scope_callback)) { + scope_arrived(scope, child->error); + dma_fence_put(&scope->base); + } + } + scope_arrived(scope, 0); + return &scope->base; +} diff --git a/drivers/gpu/drm/asahi/g17/sync_scope.h b/drivers/gpu/drm/asahi/g17/sync_scope.h new file mode 100644 index 0000000000000..11c84d65f59f2 --- /dev/null +++ b/drivers/gpu/drm/asahi/g17/sync_scope.h @@ -0,0 +1,15 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ +#ifndef ASAHI_G17_SYNC_SCOPE_H +#define ASAHI_G17_SYNC_SCOPE_H +#include +struct dma_fence; +void asahi_g17_signal_fence(struct dma_fence *fence); + +struct dma_fence *asahi_g17_scope_create(struct dma_fence *previous, + struct dma_fence *submitted, u64 point); +void asahi_g17_scope_bind(struct dma_fence *scope, struct dma_fence *root); +bool asahi_g17_is_scope(struct dma_fence *fence); +/* Returns owned child references, or zero once the scope is signalled. */ +unsigned int asahi_g17_scope_children(struct dma_fence *fence, + struct dma_fence *children[2]); +#endif diff --git a/include/uapi/drm/asahi_drm.h b/include/uapi/drm/asahi_drm.h index 66396f89cc333..836739ffbbebf 100644 --- a/include/uapi/drm/asahi_drm.h +++ b/include/uapi/drm/asahi_drm.h @@ -368,6 +368,13 @@ enum drm_asahi_feature { * DRM_ASAHI_COMPUTE_AS_BUILD for a bounded long execution allowance. */ DRM_ASAHI_FEATURE_BOUNDED_AS_BUILD = (1UL) << 16, + + /** + * @DRM_ASAHI_FEATURE_CUMULATIVE_SIGNALS: SUBMIT accepts one + * DRM_ASAHI_SYNC_TIMELINE_SYNCOBJ_CUMULATIVE output. All outputs + * include the earlier points of that timeline without adding GPU waits. + */ + DRM_ASAHI_FEATURE_CUMULATIVE_SIGNALS = (1UL) << 17, }; #define DRM_ASAHI_PARAM_GROUP_VM_STATUS 1 @@ -901,6 +908,16 @@ enum drm_asahi_sync_type { /** @DRM_ASAHI_SYNC_TIMELINE_SYNCOBJ: Timeline sync object */ DRM_ASAHI_SYNC_TIMELINE_SYNCOBJ = 1, + + /** + * @DRM_ASAHI_SYNC_TIMELINE_SYNCOBJ_CUMULATIVE: Output-only queue + * timeline anchor, requiring CUMULATIVE_SIGNALS and a nonzero point. + * At most one is allowed. All outputs receive completion of this submit + * and the earlier anchor points. + * Other outputs must not alias the anchor. No execution dependency is + * added. Calls updating this private anchor must be externally serialized. + */ + DRM_ASAHI_SYNC_TIMELINE_SYNCOBJ_CUMULATIVE = 2, }; /** diff --git a/rust/kernel/drm/syncobj.rs b/rust/kernel/drm/syncobj.rs index a022e08223588..72ba50c57286b 100644 --- a/rust/kernel/drm/syncobj.rs +++ b/rust/kernel/drm/syncobj.rs @@ -15,6 +15,14 @@ pub struct SyncObj { } impl SyncObj { + /// Borrows the raw sync object for synchronous calls into DRM helpers. + /// The caller must not release the owned reference or retain this pointer + /// beyond the lifetime of `self` without taking another reference. + #[inline(always)] + pub fn as_raw(&self) -> *mut bindings::drm_syncobj { + self.ptr + } + /// Looks up a sync object by its handle for a given `File`. pub fn lookup_handle( file: &drm::File, From bcdfdafd80cf93fadc79df688dd0109b9c4ae8d2 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sat, 24 Oct 2026 03:45:04 -0400 Subject: [PATCH 040/388] drm/asahi: test G17 queue budgets with firmware-reserved identities Exercise dynamic render headroom and the full logical-queue budget with zero and nonzero firmware QID prefixes. Document that the shared watchdog counter orders reservations rather than concurrent firmware exposure. --- drivers/gpu/drm/asahi/g17/kick.rs | 35 +++++++++++++++++++++++++++++++ drivers/gpu/drm/asahi/g17/port.rs | 3 +++ 2 files changed, 38 insertions(+) diff --git a/drivers/gpu/drm/asahi/g17/kick.rs b/drivers/gpu/drm/asahi/g17/kick.rs index f3183bb005df6..a1f8f70f597d4 100644 --- a/drivers/gpu/drm/asahi/g17/kick.rs +++ b/drivers/gpu/drm/asahi/g17/kick.rs @@ -307,6 +307,41 @@ mod accounting_tests { assert_eq!(ids.leased, QID_COUNT - FULL_RENDER_RESERVE); } + #[test] + fn dynamic_compute_growth_preserves_render_headroom_above_firmware_floor() { + for floor in [0, 4] { + let mut ids = KBox::init(QueueIds::new(floor), GFP_KERNEL).unwrap(); + let queues = 2; + let mut owner = 1; + while ids.compute_fits(queues) { + let id = ids.reserve_for(owner, DataMaster::Compute, None, queues).unwrap(); + assert!(usize::from(id.qid()) >= floor); + ids.publish(id).unwrap(); + owner += 1; + } + assert_eq!(ids.leased, QID_COUNT - floor - render_reserve(queues)); + assert_eq!(ids.reserve_for(owner, DataMaster::Compute, None, queues), Err(ENOSPC)); + assert!(ids.render_fits(queues + RENDER_HEADROOM)); + assert!(!ids.render_fits(queues + RENDER_HEADROOM + 1)); + } + } + + #[test] + fn full_logical_queue_budget_keeps_compute_floor_and_excludes_firmware_ids() { + for floor in [0, 4] { + let mut ids = KBox::init(QueueIds::new(floor), GFP_KERNEL).unwrap(); + let queues = queue_limit(floor) as usize; + for owner in 1..=COMPUTE_FLOOR as u64 { + let id = ids.reserve_for(owner, DataMaster::Compute, None, queues).unwrap(); + assert!(usize::from(id.qid()) >= floor); + ids.publish(id).unwrap(); + } + assert!(!ids.compute_fits(queues)); + assert!(ids.render_fits(queues)); + assert!(!ids.render_fits(queues + 1)); + } + } + #[test] fn initial_reservations_yield_but_published_ids_do_not() { let mut ids = table(); diff --git a/drivers/gpu/drm/asahi/g17/port.rs b/drivers/gpu/drm/asahi/g17/port.rs index d8f7f4ddff8a1..9acd9ff6e5910 100644 --- a/drivers/gpu/drm/asahi/g17/port.rs +++ b/drivers/gpu/drm/asahi/g17/port.rs @@ -371,6 +371,9 @@ impl Port { self.generation.store(generation, Ordering::Release); } + /// Unique reservation order shared by both engines. Concurrent publishers may expose + /// their work in a different order; queue serialization and visibility use other locks. + /// The watchdog uses this number only to choose bounded cross-VM timeout grace. pub(super) fn next_publication(&self) -> u64 { self.publications.fetch_add(1, Ordering::Relaxed) + 1 } From 971d4316d91cce97b92db72c4f29e5155b76d626 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sat, 24 Oct 2026 03:46:24 -0400 Subject: [PATCH 041/388] drm/asahi: size cached compute records for every native wait Dynamic QID admission permits 128 native inputs. A 0x1000-byte record slot holds only 18 waits at offset 0x900 with 0x60 bytes each, so the nineteenth could overwrite the following live slot. Size cached slots for the complete bounded list and validate each write against its own slot before deriving an allocation offset. Keep announcement slots unchanged. Host execution of the production offset helper and regression test bodies covers all 128 waits in all 32 retained slots; the old-size control fails. --- drivers/gpu/drm/asahi/g17/queue/compute.rs | 62 ++++++++++++++++++---- 1 file changed, 51 insertions(+), 11 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/queue/compute.rs b/drivers/gpu/drm/asahi/g17/queue/compute.rs index 6ca63595154de..02ae0bbbb9634 100644 --- a/drivers/gpu/drm/asahi/g17/queue/compute.rs +++ b/drivers/gpu/drm/asahi/g17/queue/compute.rs @@ -30,6 +30,12 @@ const DESCRIPTORS: usize = 32; const GRAPH_SIZE: usize = 0x8000; const CONTEXT_SIZE: usize = 0x20000; const RECORD_STRIDE: usize = 0x1000; +const NATIVE_WAIT_OFFSET: usize = 0x900; +const NATIVE_WAIT_STRIDE: usize = 0x60; +// A submission can name every physical compute channel. Keep the complete +// wait list inside its own cached slot, including the last retained slot. +const CACHED_RECORD_STRIDE: usize = (NATIVE_WAIT_OFFSET + + crate::g17::kick::COMPUTE_CHANNELS * NATIVE_WAIT_STRIDE).next_power_of_two(); const CONFIG: usize = 0; const ANNOUNCE: usize = 0x800; const PREDECESSOR: usize = 0x400; @@ -193,8 +199,8 @@ impl Graph { let cached_records = objects.allocate( alloc, Family::QueueConfig, - RECORD_SLOTS * RECORD_STRIDE, - RECORD_SLOTS * RECORD_STRIDE, + RECORD_SLOTS * CACHED_RECORD_STRIDE, + RECORD_SLOTS * CACHED_RECORD_STRIDE, prot, cpu, )?; @@ -1196,6 +1202,32 @@ fn release_pending(released: bool, quarantined: bool, active: usize, retiring: b mod admission_tests { use super::*; + #[test] + fn complete_native_wait_list_fits_every_cached_slot() { + let waits = crate::g17::kick::COMPUTE_CHANNELS; + for slot in 0..RECORD_SLOTS { + for index in 0..waits { + let start = record_start(slot, NATIVE_WAIT_OFFSET + index * NATIVE_WAIT_STRIDE, + NATIVE_WAIT_STRIDE, CACHED_RECORD_STRIDE).unwrap(); + assert!(start >= slot * CACHED_RECORD_STRIDE); + assert!(start + NATIVE_WAIT_STRIDE <= (slot + 1) * CACHED_RECORD_STRIDE); + } + } + } + + #[test] + fn record_window_cannot_overwrite_the_next_slot() { + // The nineteenth wait used to cross the old slot boundary silently. + let offset = NATIVE_WAIT_OFFSET + 18 * NATIVE_WAIT_STRIDE; + assert_eq!(record_start(0, offset, NATIVE_WAIT_STRIDE, RECORD_STRIDE), Err(EINVAL)); + assert!(record_start(0, offset, NATIVE_WAIT_STRIDE, CACHED_RECORD_STRIDE).is_ok()); + assert_eq!(record_start(RECORD_SLOTS, 0, 1, CACHED_RECORD_STRIDE), Err(EINVAL)); + assert_eq!(record_start(0, usize::MAX, 1, CACHED_RECORD_STRIDE), Err(EINVAL)); + assert_eq!(record_start(0, CACHED_RECORD_STRIDE, 1, CACHED_RECORD_STRIDE), Err(EINVAL)); + assert_eq!(record_start(0, CACHED_RECORD_STRIDE - 1, 1, CACHED_RECORD_STRIDE), + Ok(CACHED_RECORD_STRIDE - 1)); + } + #[test] fn retained_ring_pressure_waits_without_discarding_work() { let mut active = Fifo::new(); @@ -1760,6 +1792,15 @@ impl Queue { } } +/// Validate the individual slot before deriving an offset into its allocation. +/// Allocation-wide checks cannot detect a write into the following live slot. +fn record_start(slot: usize, offset: usize, window: usize, stride: usize) -> Result { + if slot >= RECORD_SLOTS || offset > stride || window > stride - offset { + return Err(EINVAL); + } + slot.checked_mul(stride).and_then(|start| start.checked_add(offset)).ok_or(EINVAL) +} + impl Graph { fn record( &mut self, @@ -1769,17 +1810,15 @@ impl Graph { value: T, ) -> Result { let (object, start) = if let Some(slot) = slot { - if slot >= RECORD_SLOTS { - return Err(EINVAL); - } - let object: &mut KernelObject = if offset == ANNOUNCE { - &mut self.records + let (object, stride): (&mut KernelObject, usize) = if offset == ANNOUNCE { + (&mut self.records, RECORD_STRIDE) } else { - &mut self.cached_records + (&mut self.cached_records, CACHED_RECORD_STRIDE) }; - (object, slot * RECORD_STRIDE + offset) + (object, record_start(slot, offset, window, stride)?) } else { - (&mut *self.graph, 0x6800 + offset) + let start = record_start(0, offset, window, GRAPH_SIZE - 0x6800)?; + (&mut *self.graph, 0x6800 + start) }; if core::mem::size_of::() > window { return Err(EINVAL); @@ -2031,7 +2070,8 @@ impl Queue { for (index, wait) in native.iter().enumerate() { let record = wait.bytes(crate::g17::fw::stamp_value(kick_timestamp.get() as u32), packet.order.sequence); - let address = self.graph.record(record_slot, 0x900 + index * 0x60, 0x60, record)?; + let address = self.graph.record(record_slot, NATIVE_WAIT_OFFSET + index * NATIVE_WAIT_STRIDE, + NATIVE_WAIT_STRIDE, record)?; wait_vas.push(address, GFP_KERNEL)?; } let prepared_kick = self.kick.prepare_entry(&kick_args)?; From 9bf3039873a1e241991b5999aceff5610c660ade Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sat, 24 Oct 2026 04:00:14 -0400 Subject: [PATCH 042/388] drm/asahi: index pending compute completion identities before reading payloads The shared completion ring retains old records for active QIDs. Checking each one against every pending descriptor reads unrelated payload words and repeats a linear search, especially when a future pending command keeps the full scan open. Build a bounded per-QID kick index in the existing observation batch. Reject old kicks from the control word, then retain the exact descriptor, validity and timestamp checks. Keep all scan requests, full-ring fallbacks and retirement semantics unchanged. Add collision, descriptor-reuse, stale-payload, clear and partial-publication tests. --- .../drm/asahi/g17/queue/compute/retirement.rs | 156 ++++++++++++++++-- 1 file changed, 140 insertions(+), 16 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/queue/compute/retirement.rs b/drivers/gpu/drm/asahi/g17/queue/compute/retirement.rs index b268c1c40de00..46df3598362b7 100644 --- a/drivers/gpu/drm/asahi/g17/queue/compute/retirement.rs +++ b/drivers/gpu/drm/asahi/g17/queue/compute/retirement.rs @@ -166,12 +166,22 @@ struct Request { descriptor: u64, kick: u64, observed: Option, + next: u16, +} + +const NO_REQUEST: u16 = u16::MAX; +const _: () = assert!(QIDS * IN_FLIGHT < NO_REQUEST as usize); + +fn request_bucket(qid: usize, kick: u64) -> usize { + qid * IN_FLIGHT + kick as usize % IN_FLIGHT } /// Reusable heap scratch for one shared-ring scan. QID requests are contiguous and bounded; /// staging and folding never allocate under the device mutex. pub(crate) struct Batch { requests: KVVec, + /// Per-QID kick buckets; collisions retain the complete identity check. + heads: KVVec, slots: [(usize, usize); QIDS], open: Option, /// Requests with no observation yet; the scan stops when this reaches zero. @@ -180,8 +190,13 @@ pub(crate) struct Batch { impl Batch { pub(crate) fn new() -> Result { + let mut heads = KVVec::with_capacity(QIDS * IN_FLIGHT, GFP_KERNEL)?; + for _ in 0..QIDS * IN_FLIGHT { + heads.push_within_capacity(NO_REQUEST).map_err(|_| ENOSPC)?; + } Ok(Self { requests: KVVec::with_capacity(QIDS * IN_FLIGHT, GFP_KERNEL)?, + heads, slots: [(0, 0); QIDS], open: None, unobserved: 0, @@ -209,45 +224,74 @@ impl Batch { return Err(ENOSPC); } let start = if slot.1 == 0 { + let begin = ticket.qid as usize * IN_FLIGHT; + self.heads.get_mut(begin..begin + IN_FLIGHT).ok_or(EIO)?.fill(NO_REQUEST); self.requests.len() } else { slot.0 }; + let bucket = request_bucket(ticket.qid as usize, ticket.kick); + let next = *self.heads.get(bucket).ok_or(EIO)?; + let index = u16::try_from(self.requests.len()).map_err(|_| ENOSPC)?; self.requests .push_within_capacity(Request { descriptor: ticket.descriptor, kick: ticket.kick, observed: None, + next, }) .map_err(|_| ENOSPC)?; + *self.heads.get_mut(bucket).ok_or(EIO)? = index; *slot = (start, slot.1 + 1); self.open = Some(ticket.qid); self.unobserved += 1; Ok(()) } - /// Capture the control word once, then read only a requested identity's payload. - /// Records can be overwritten during a scan: incomplete or older identities are skipped. + /// Capture the control word once. Most slots belong to an older kick of a + /// selected QID; reject those before reading any additional shared words. pub(crate) fn fold(&mut self, record: ComputeCompletion<'_>) -> Result { - let control = record.read(0)?; + self.fold_reader(|word| record.read(word)) + } + + fn fold_reader(&mut self, mut read: impl FnMut(usize) -> Result) -> Result { + let control = read(0)?; let qid = ((control >> 9) & 0x7f) as usize; - let (start, count) = self.slots[qid]; + let (_, count) = self.slots[qid]; if count == 0 { return Ok(()); } - let descriptor = record.read(6)? & !1; - if descriptor == 0 || record.read(7)? == 0 { - return Ok(()); - } let kick = (((control >> 8) & 0x00ff_ffff_ff00) | ((control >> 48) & 0xff)) & 0xff_ffff_ffff; - let requests = self.requests.get_mut(start..start + count).ok_or(EIO)?; - let Some(request) = requests - .iter_mut() - .find(|r| r.descriptor == descriptor && r.kick == kick) - else { - return Ok(()); - }; - let timestamps = [record.read(2)? & TIME_MASK, record.read(5)? & TIME_MASK]; + let mut index = *self.heads.get(request_bucket(qid, kick)).ok_or(EIO)?; + let mut descriptor = None; + let mut found = None; + for _ in 0..count { + if index == NO_REQUEST { + break; + } + let request = self.requests.get(usize::from(index)).ok_or(EIO)?; + if request.kick == kick { + let value = if let Some(value) = descriptor { + value + } else { + let value = read(6)? & !1; + if value == 0 || read(7)? == 0 { + return Ok(()); + } + descriptor = Some(value); + value + }; + if request.descriptor == value { + // Chains run newest first. Retain the original first-match + // rule even if a caller stages the same identity twice. + found = Some(usize::from(index)); + } + } + index = request.next; + } + let Some(index) = found else { return Ok(()); }; + let request = self.requests.get_mut(index).ok_or(EIO)?; + let timestamps = [read(2)? & TIME_MASK, read(5)? & TIME_MASK]; if timestamps[0] == 0 || timestamps[1] == 0 || request @@ -415,3 +459,83 @@ mod tests { assert_eq!(result, Err(ENODATA)); } } + + +#[kunit_tests(asahi_g17_completion_index)] +mod index_tests { + use super::*; + + fn ticket(qid: u8, kick: u64, descriptor: u64) -> Ticket { + Ticket { owner: 1, submission: 1, context_id: 1, qid, descriptor, + work_node: 0x2000, kick, ordinal: 1, item_producer: 4, item_count: 64, epoch: 1 } + } + fn record(ticket: &Ticket, timestamps: [u64; 2]) -> [u64; 8] { + let mut words = [0; 8]; + words[0] = ((ticket.kick & !0xff) << 8) | ((ticket.kick & 0xff) << 48) + | (u64::from(ticket.qid) << 9); + words[2] = timestamps[0]; words[5] = timestamps[1]; + words[6] = ticket.descriptor | 1; words[7] = 1; + words + } + #[test] + fn stale_kick_reads_only_the_control_even_on_a_bucket_collision() { + let mut batch = Batch::new().unwrap(); + batch.request(&ticket(7, 33, 0x1000)).unwrap(); + let words = record(&ticket(7, 17, 0x1000), [10, 20]); + batch.fold_reader(|word| { + assert_eq!(word, 0); + Ok(words[word]) + }).unwrap(); + assert!(!batch.complete()); + } + #[test] + fn collisions_and_reused_descriptors_keep_full_kick_identity() { + let mut batch = Batch::new().unwrap(); + let first = ticket(7, 33, 0x1000); + let second = ticket(7, 49, 0x1000); + batch.request(&first).unwrap(); batch.request(&second).unwrap(); + let words = record(&first, [10, 20]); + batch.fold_reader(|word| Ok(words[word])).unwrap(); + assert!(!batch.complete()); + let words = record(&second, [30, 40]); + batch.fold_reader(|word| Ok(words[word])).unwrap(); + assert!(batch.complete()); + assert_eq!(batch.take(&first).unwrap().unwrap().timestamps, [10, 20]); + assert_eq!(batch.take(&second).unwrap().unwrap().timestamps, [30, 40]); + } + #[test] + fn same_kick_still_checks_descriptor_and_zero_duration_is_retained() { + let mut batch = Batch::new().unwrap(); + let first = ticket(7, 33, 0x1000); + let second = ticket(7, 33, 0x2000); + batch.request(&first).unwrap(); batch.request(&second).unwrap(); + let words = record(&first, [20, 20]); + batch.fold_reader(|word| Ok(words[word])).unwrap(); + assert_eq!(batch.take(&first).unwrap().unwrap().timestamps, [20, 20]); + assert!(batch.take(&second).unwrap().is_none()); + } + #[test] + fn duplicate_identity_keeps_the_first_staged_request() { + let mut batch = Batch::new().unwrap(); + let current = ticket(7, 33, 0x1000); + batch.request(¤t).unwrap(); batch.request(¤t).unwrap(); + let words = record(¤t, [10, 20]); + batch.fold_reader(|word| Ok(words[word])).unwrap(); + assert_eq!(batch.take(¤t).unwrap().unwrap().timestamps, [10, 20]); + assert_eq!(batch.unobserved, 1); + } + #[test] + fn clear_restarts_buckets_and_later_publication_stays_unobserved() { + let mut batch = Batch::new().unwrap(); + batch.request(&ticket(7, 33, 0x1000)).unwrap(); batch.clear(); + let current = ticket(7, 49, 0x1000); + batch.request(¤t).unwrap(); + let stale = record(&ticket(7, 33, 0x1000), [10, 20]); + batch.fold_reader(|word| Ok(stale[word])).unwrap(); + assert!(!batch.complete()); + assert!(batch.take(&ticket(7, 50, 0x2000)).unwrap().is_none()); + let words = record(¤t, [30, 40]); + batch.fold_reader(|word| Ok(words[word])).unwrap(); + assert!(batch.complete()); + } +} From 26446182da3aa8d98ce799478a025117b021eaba Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sat, 24 Oct 2026 04:16:00 -0400 Subject: [PATCH 043/388] drm/asahi: retain a deep working set of validated GPU page ranges Cache the full page footprint already checked by the VM, preserve permission-subset and mapping-epoch checks, and merge only compatible intervals. Keep the transient parser bound at 48 requests while retaining 128 intervals in the persistent cache so a depth-64 stream does not repeatedly flush every proof. Measured normal runs reduce compute depth-64 submit CPU from about 10.06 to 7.46 us and render depth-32 from 12.29 to 8.16 us. Wall throughput remains limited elsewhere. Range, permission, epoch, overflow and rotating-working-set regression tests pass. --- drivers/gpu/drm/asahi/g17/validation.rs | 174 ++++++++++++++++++++++-- 1 file changed, 164 insertions(+), 10 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/validation.rs b/drivers/gpu/drm/asahi/g17/validation.rs index b2a6cf6e8bfe3..b0f2861382ad7 100644 --- a/drivers/gpu/drm/asahi/g17/validation.rs +++ b/drivers/gpu/drm/asahi/g17/validation.rs @@ -8,19 +8,41 @@ use core::{cell::RefCell, ops::Range}; use kernel::prelude::*; const MAX_RANGES: usize = 48; +// The persistent proof must retain a deep queue's rotating command buffers. +// Keep the transient parser request set small to bound its stack frame. +const MAX_CACHED_RANGES: usize = 128; type Request = (u64, u64, u8); +/// Match the page footprint already checked by `Vm::covers_range_locked`. +fn pages((address, size, access): Request) -> Option { + if size == 0 || access == 0 || access & !3 != 0 { + return None; + } + let mask = mmu::UAT_PGSZ as u64 - 1; + let end = address.checked_add(size)?; + let end = end.checked_add(mask)? & !mask; + let start = address & !mask; + Some((start, end.checked_sub(start)?, access)) +} + +fn contains((start, size, access): Request, (other, length, needed): Request) -> bool { + access & needed == needed + && other >= start + && other.checked_add(length).is_some_and(|end| + start.checked_add(size).is_some_and(|limit| end <= limit)) +} + #[derive(Clone, Copy)] -struct Requests { - ranges: [Request; MAX_RANGES], +struct Requests { + ranges: [Request; CAPACITY], count: usize, overflow: bool, } -impl Requests { +impl Requests { const fn new() -> Self { Self { - ranges: [(0, 0, 0); MAX_RANGES], + ranges: [(0, 0, 0); CAPACITY], count: 0, overflow: false, } @@ -30,24 +52,55 @@ impl Requests { &self.ranges[..self.count] } + fn covers(&self, request: Request) -> bool { + self.as_slice().iter().any(|&range| contains(range, request)) + } + fn insert(&mut self, request: Request) { - if self.as_slice().contains(&request) { + let Some(mut request) = pages(request) else { + self.overflow = true; + return; + }; + if self.covers(request) { return; } - if self.count == MAX_RANGES { + // Merge only equal permissions, so an adjacent writable page never + // grants write access to a read-only page. Restart after each merge + // because it can join a range visited earlier. + let mut index = 0; + while index < self.count { + let range = self.ranges[index]; + let end = request.0 + request.1; + let other_end = range.0 + range.1; + if contains(request, range) + || (request.2 == range.2 && request.0 <= other_end && range.0 <= end) + { + if request.2 == range.2 { + let start = request.0.min(range.0); + request = (start, end.max(other_end) - start, request.2); + } + self.count -= 1; + self.ranges[index] = self.ranges[self.count]; + index = 0; + } else { + index += 1; + } + } + if self.count == CAPACITY { self.overflow = true; return; } self.ranges[self.count] = request; self.count += 1; } + } -/// One logical queue's bounded cache of exact address, size and permission tuples. +/// One logical queue's bounded cache of validated page ranges and permissions. /// Mapping mutations make the epoch unavailable until their final page-table write. pub(crate) struct Proof { epoch: Option, - requests: Requests, + requests: Requests, } impl Proof { @@ -64,7 +117,7 @@ impl Proof { && requests .as_slice() .iter() - .all(|request| self.requests.as_slice().contains(request)) + .all(|&request| self.requests.covers(request)) } fn record(&mut self, epoch: Option, requests: Requests) { @@ -79,7 +132,10 @@ impl Proof { } // An incomplete union cannot be reused; retain the latest complete set. self.epoch = if requests.overflow { None } else { epoch }; - self.requests = requests; + self.requests = Requests::new(); + for &request in requests.as_slice() { + self.requests.insert(request); + } } /// The VM-job guard must already pin the mappings for this command. Keep the @@ -144,3 +200,101 @@ impl AddressSpace for Space<'_> { } } } + + +#[kunit_tests(asahi_g17_validation_pages)] +mod tests { + use super::*; + const PAGE: u64 = mmu::UAT_PGSZ as u64; + + fn requests(range: Request) -> Requests { + let mut result = Requests::new(); + result.insert(range); + result + } + + #[test] + fn different_offsets_share_the_already_validated_page() { + let mut proof = Proof::new(); + proof.record(Some(7), requests((PAGE + 4, 4, 3))); + assert!(proof.covers(Some(7), &requests((PAGE + 0x100, 8, 1)))); + assert!(proof.covers(Some(7), &requests((2 * PAGE - 8, 8, 2)))); + assert!(!proof.covers(Some(7), &requests((2 * PAGE - 8, 16, 1)))); + assert!(!proof.covers(Some(8), &requests((PAGE + 4, 4, 1)))); + assert!(!proof.covers(None, &requests((PAGE + 4, 4, 1)))); + } + + #[test] + fn adjacent_equal_permissions_merge_but_holes_do_not() { + let mut ranges = requests((PAGE, 1, 1)); + ranges.insert((3 * PAGE, 1, 1)); + assert_eq!(ranges.count, 2); + assert!(!ranges.covers((2 * PAGE, PAGE, 1))); + ranges.insert((2 * PAGE, 1, 1)); + assert_eq!(ranges.count, 1); + assert_eq!(ranges.as_slice(), &[(PAGE, 3 * PAGE, 1)]); + } + + #[test] + fn adjacent_permissions_never_grant_access_to_another_page() { + let mut proof = Proof::new(); + let mut ranges = requests((PAGE, 1, 1)); + ranges.insert((2 * PAGE, 1, 3)); + proof.record(Some(1), ranges); + assert!(!proof.covers(Some(1), &requests((PAGE, 1, 2)))); + assert!(proof.covers(Some(1), &requests((2 * PAGE, 1, 2)))); + assert!(!proof.covers(Some(1), &requests((PAGE, 2 * PAGE, 3)))); + } + + #[test] + fn stronger_coverage_removes_only_fully_covered_ranges() { + let mut ranges = requests((PAGE, PAGE, 1)); + ranges.insert((2 * PAGE, PAGE, 2)); + ranges.insert((PAGE, 2 * PAGE, 3)); + assert_eq!(ranges.as_slice(), &[(PAGE, 2 * PAGE, 3)]); + assert!(!ranges.covers((0, PAGE, 1))); + } + + #[test] + fn persistent_cache_retains_a_deep_rotating_working_set() { + let mut proof = Proof::new(); + for index in 0..64 { + proof.record(Some(7), requests((index * 2 * PAGE, 8, 1))); + } + assert_eq!(proof.requests.count, 64); + for index in 0..64 { + assert!(proof.covers(Some(7), &requests((index * 2 * PAGE + 16, 8, 1)))); + } + assert_eq!(core::mem::size_of::(), core::mem::size_of::>()); + } + + #[test] + fn persistent_overflow_keeps_only_a_new_complete_proof() { + let mut proof = Proof::new(); + for index in 0..=MAX_CACHED_RANGES { + proof.record(Some(7), requests((index as u64 * 2 * PAGE, 8, 1))); + } + assert_eq!(proof.requests.count, 1); + assert!(!proof.covers(Some(7), &requests((0, 8, 1)))); + assert!(proof.covers(Some(7), + &requests((MAX_CACHED_RANGES as u64 * 2 * PAGE, 8, 1)))); + } + + #[test] + fn invalid_rounding_and_full_cache_disable_reuse() { + for range in [(0, 0, 1), (u64::MAX - 1, 1, 1), (0, 1, 0), (0, 1, 4)] { + assert!(pages(range).is_none()); + let mut proof = Proof::new(); + proof.record(Some(1), requests(range)); + assert!(!proof.covers(Some(1), &requests((0, 1, 1)))); + } + let mut ranges = Requests::new(); + for index in 0..=MAX_RANGES { + ranges.insert(((2 * index) as u64 * PAGE, PAGE, 1)); + } + assert!(ranges.overflow); + let mut proof = Proof::new(); + proof.record(Some(1), ranges); + assert!(!proof.covers(Some(1), &requests((0, PAGE, 1)))); + } +} From b6248acf900c1cf30deac52791c2c50d10055d7b Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sat, 24 Oct 2026 05:14:09 -0400 Subject: [PATCH 044/388] drm/asahi: avoid locking pending private completion fences Frontier pruning examines independently signalled driver completion fences. Check their signalled flag before taking the generic status lock; retain the locked terminal path for errors and leave external input fence handling unchanged. Seven Neo depth-64 repetitions reduce median submit API elapsed time from 7.52 to 5.79 us. Frontier scan CPU samples fall from 1.83% to 0.25% of the machine; throughput remains about 19.5 us per command. The full benchmark matrix, candidate-bound short stress, software-fence/error gates and 20-source native fan-in pass. --- drivers/gpu/drm/asahi/g17/job.rs | 12 ++++++++++++ drivers/gpu/drm/asahi/g17/queue/mod.rs | 8 ++++---- drivers/gpu/drm/asahi/g17/sync_scope.c | 12 ++++++++++++ drivers/gpu/drm/asahi/g17/sync_scope.h | 2 ++ 4 files changed, 30 insertions(+), 4 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/job.rs b/drivers/gpu/drm/asahi/g17/job.rs index 572cfd5d0f382..27e54613965cd 100644 --- a/drivers/gpu/drm/asahi/g17/job.rs +++ b/drivers/gpu/drm/asahi/g17/job.rs @@ -385,6 +385,18 @@ pub(crate) struct Fences { pub(crate) publication: Option>, } +impl Fences { + /// Scheduler::enqueue always supplies our explicit completion fence here. + /// It has no polling callback, so pending frontier checks need no lock. + pub(crate) fn status(&self) -> i32 { + unsafe extern "C" { + fn asahi_g17_completion_fence_status(fence: *mut bindings::dma_fence) -> i32; + } + // SAFETY: completed owns the live private fence throughout this read. + unsafe { asahi_g17_completion_fence_status(self.completed.raw()) } + } +} + impl Scheduler { pub(crate) fn new(dev: &AsahiDrmDevice, backend: Arc, engine: Engine) -> Result { let (credits, name) = match engine { diff --git a/drivers/gpu/drm/asahi/g17/queue/mod.rs b/drivers/gpu/drm/asahi/g17/queue/mod.rs index c959f8bc8c4b8..0cc64b8b94460 100644 --- a/drivers/gpu/drm/asahi/g17/queue/mod.rs +++ b/drivers/gpu/drm/asahi/g17/queue/mod.rs @@ -155,7 +155,7 @@ impl Queue { fn prune_frontier(&mut self, index: usize) { let failure = &mut self.failures[index]; self.frontiers[index].retain(|(sequence, fences)| { - let status = job::fence_status(&fences.completed); + let status = fences.status(); if status == 0 { return true; } @@ -203,11 +203,11 @@ impl Queue { && !destinations.has_outputs() && self.frontiers[0].iter().all(|(sequence, fences)| !Order::contains(order.wait_through[0], *sequence) - || job::fence_status(&fences.completed) > 0) + || fences.status() > 0) && self.frontiers[1].iter().all(|(sequence, fences)| !Order::contains(order.wait_through[1], *sequence) || fences.ready.raw() != fences.completed.raw() - || job::fence_status(&fences.completed) > 0) + || fences.status() > 0) && self.failures.iter().enumerate().all(|(index, failure)| failure.as_ref().is_none_or(|(sequence, _)| !Order::contains(order.wait_through[index], *sequence))) @@ -293,7 +293,7 @@ impl Queue { // Successful completion includes host timestamp publication. Its // ready fence adds no ordering and would prevent shallow inline // publication. Failed and pending producers still need checking. - if job::fence_status(&fences.completed) > 0 { + if fences.status() > 0 { continue; } dependencies.push(fences.ready.clone(), GFP_KERNEL)?; diff --git a/drivers/gpu/drm/asahi/g17/sync_scope.c b/drivers/gpu/drm/asahi/g17/sync_scope.c index 55feb19a7f83d..a3153f3122b94 100644 --- a/drivers/gpu/drm/asahi/g17/sync_scope.c +++ b/drivers/gpu/drm/asahi/g17/sync_scope.c @@ -11,6 +11,18 @@ #include "sync_scope.h" #include "sync_input.h" +int asahi_g17_completion_fence_status(struct dma_fence *fence) +{ + /* These private fences have no signaled callback. A pending flag is + * sufficient to keep the frontier member; a concurrent signal can only + * defer pruning until the next submission. Read terminal errors through + * the normal locked API after observing the signalled flag. + */ + if (!dma_fence_test_signaled_flag(fence)) + return 0; + return dma_fence_get_status(fence); +} + struct asahi_g17_scope; struct asahi_g17_scope_callback { struct dma_fence_cb callback; diff --git a/drivers/gpu/drm/asahi/g17/sync_scope.h b/drivers/gpu/drm/asahi/g17/sync_scope.h index 11c84d65f59f2..e88b78aacdd7b 100644 --- a/drivers/gpu/drm/asahi/g17/sync_scope.h +++ b/drivers/gpu/drm/asahi/g17/sync_scope.h @@ -4,6 +4,8 @@ #include struct dma_fence; void asahi_g17_signal_fence(struct dma_fence *fence); +/* Only for explicitly signalled driver completion fences, with no poll op. */ +int asahi_g17_completion_fence_status(struct dma_fence *fence); struct dma_fence *asahi_g17_scope_create(struct dma_fence *previous, struct dma_fence *submitted, u64 point); From b08c81722d896eee8da9771e99e38e6986a2a806 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sat, 24 Oct 2026 23:19:10 -0400 Subject: [PATCH 045/388] drm/asahi: give G17 firmware restarts time to drain long work The restart handshake waited 500 one-millisecond ticks for each of its two phases. The firmware completes a restart only after the work it interrupted has drained, and with several clients running long ray-traversal jobs that takes seconds: six concurrent ray-query stress clients timed the second phase out in state 2, and the host then declared the whole device lost, killing every client including the desktop. Wait up to ten seconds per phase, the longest execution allowance a job gets, and log which phase timed out and the state the firmware was in, as well as a restart storm stop. With this the same load completes every restart and only the clients whose work was blamed or timed out see a lost device. --- drivers/gpu/drm/asahi/g17/recovery.rs | 10 +++++++++- drivers/gpu/drm/asahi/g17/runtime/events.rs | 5 ++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/recovery.rs b/drivers/gpu/drm/asahi/g17/recovery.rs index 89f8d33bdb80c..8aac25ab80d68 100644 --- a/drivers/gpu/drm/asahi/g17/recovery.rs +++ b/drivers/gpu/drm/asahi/g17/recovery.rs @@ -24,7 +24,11 @@ const QIDS: u8 = 128; const GUILTY: u8 = 1 << 1; const CANCELLED: u8 = 1 << 2; const MAX_GUILTY_VMS: usize = 8; -const RECOVERY_POLLS: usize = 500; +/// One-millisecond ticks per handshake phase. The firmware finishes a restart only after the +/// work it interrupted has drained; with several clients running long ray-traversal jobs that +/// takes seconds, and giving up loses every client's device. Ten seconds matches the longest +/// execution allowance (`timeout::POLL_MS` plus renewals). +const RECOVERY_POLLS: usize = 10_000; /// Per-QID classifications from the complete firmware recovery table. /// Multiple records for a queue contribute all of their classification bits. @@ -192,6 +196,8 @@ pub(super) fn service( } } if !ready { + pr_warn!("G17: recovery gen {} did not reach state 1 (state {:?})\n", + request.generation, host.recovery_state()); return Err(ETIMEDOUT); } // Recheck ownership after the off-CPU waits, before any handshake mutation. @@ -217,6 +223,8 @@ pub(super) fn service( host.wait_tick(); } if !recovered { + pr_warn!("G17: recovery gen {} did not reach state 3 (state {:?})\n", + request.generation, host.recovery_state()); return Err(ETIMEDOUT); } // A failed table read leaves only the explicit blamed QID as evidence of guilt. diff --git a/drivers/gpu/drm/asahi/g17/runtime/events.rs b/drivers/gpu/drm/asahi/g17/runtime/events.rs index cdd8cc6d5dea5..c1a2ebe232fcb 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/events.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/events.rs @@ -266,7 +266,10 @@ impl Firmware { /// Preparation admission is already closed and all builders have joined. pub(super) fn service_runtime_recovery(&mut self, deferred: &mut DeferredBatch) -> Result { - if self.recovery.restart_limit(super::now_ns()) { return Err(ETIMEDOUT); } + if self.recovery.restart_limit(super::now_ns()) { + pr_warn!("G17: restart storm limit reached\n"); + return Err(ETIMEDOUT); + } let shared = self.primary.state.shared.clone(); let mut service = Service { firmware: self, From f1db16a83586e4bec596a71e12e0d8b87dd3b16c Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sat, 24 Oct 2026 23:23:44 -0400 Subject: [PATCH 046/388] drm/asahi: fail commands a GPU restart interrupted instead of completing them The firmware recovery table lists every command that was in flight when the firmware stopped: one record per command, with its queue and its completion stamp. The driver only folded the guilty and cancelled bits of those records into per-queue masks and ignored the rest. An innocent command that was interrupted part way could then still be retired by its stamps and completion record, and report success with partial output. Six concurrent ray query stress cases showed exactly that: after recoveries that cancelled nothing, three clients completed without error and with wrong results. Read the table as the packed list it is, up to its first invalid record, and mark every listed command interrupted before the firmware resumes. An interrupted command never reports success. Its failure always reaches the client, without poisoning the VM unless a recovery found it guilty; ECANCELED is no longer silent for it, and its output is not handed to consumers as valid. It is replayed only when it provably never started: a compute command with a zero-duration record, or a render group with neither its tiler start nor its fragment end written. A render group interrupted after its tiler started fails at once rather than being replayed, since blending or storage writes could apply twice. The recovery log line reports how many listed commands matched by stamp and how many fell back to the oldest command of their queue. --- drivers/gpu/drm/asahi/g17/completion.rs | 56 ++++++++-- drivers/gpu/drm/asahi/g17/initdata.rs | 23 ++-- drivers/gpu/drm/asahi/g17/queue/compute.rs | 21 ++++ drivers/gpu/drm/asahi/g17/queue/render.rs | 103 +++++++++++++++++- .../drm/asahi/g17/queue/render/retirement.rs | 5 + drivers/gpu/drm/asahi/g17/recovery.rs | 71 +++++++++++- drivers/gpu/drm/asahi/g17/recovery/safety.rs | 11 ++ drivers/gpu/drm/asahi/g17/runtime.rs | 2 +- drivers/gpu/drm/asahi/g17/runtime/events.rs | 20 +++- drivers/gpu/drm/asahi/g17/runtime/render.rs | 16 +++ 10 files changed, 301 insertions(+), 27 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/completion.rs b/drivers/gpu/drm/asahi/g17/completion.rs index d692a98acea8c..11b4c323d4668 100644 --- a/drivers/gpu/drm/asahi/g17/completion.rs +++ b/drivers/gpu/drm/asahi/g17/completion.rs @@ -270,6 +270,9 @@ pub(crate) struct Completion { /// A recovery blamed this command. Unlike `class`, which a later /// classification can change, this never clears. blamed: AtomicBool, + /// A firmware restart listed this command as in flight. Until a replay + /// republishes it from the start, it never reports success. + interrupted: AtomicBool, firmware_consumer: AtomicBool, class: AtomicU8, } @@ -327,6 +330,7 @@ impl Completion { replays: AtomicU8::new(0), replay_timeout: AtomicBool::new(false), blamed: AtomicBool::new(false), + interrupted: AtomicBool::new(false), firmware_consumer: AtomicBool::new(false), class: AtomicU8::new(0), }), @@ -379,6 +383,11 @@ impl Completion { } /// Called under the device mutex as a successful retirement is queued. pub(super) fn note_source_retired(&self) { + if self.interrupted() { + // complete() fails it; consumers must not take its output as valid. + self.note_input_error(ECANCELED); + return; + } match self.check_source_inputs() { Ok(()) => self.output.output().member.note_retired(), Err(error) => { self.note_input_error(error); } @@ -429,6 +438,9 @@ impl Completion { .is_ok(); if accepted { self.replay_timeout.store(true, Ordering::Release); + // Callers replay only work proven not to have started; the new + // publication runs from the beginning and reports its own result. + self.interrupted.store(false, Ordering::Release); } accepted } @@ -517,6 +529,35 @@ impl Completion { self.blamed.load(Ordering::Acquire) } + /// A firmware restart listed this command in its recovery table: it was in + /// flight, so it may have run partly. Its stamps and completion record may + /// still look like success. Unless a replay republishes it from the start, + /// it fails and the failure is always reported to the client. + pub(crate) fn note_interrupted(&self) { + self.interrupted.store(true, Ordering::Release); + } + + pub(crate) fn interrupted(&self) -> bool { + self.interrupted.load(Ordering::Acquire) + } + + /// Whether this failure makes the VM unusable. Spared work, and interrupted + /// work no recovery found guilty, fail without poisoning it. + pub(crate) fn poisons(&self, error: Error) -> bool { + let interrupted = self.interrupted(); + (error != ECANCELED || interrupted) && !self.spared() && !(interrupted && !self.guilty()) + } + + /// ECANCELED stays silent only for work that was never interrupted (closed + /// or cancelled by its owner); every other failure reaches the client. + fn report(&self, error: Error) { + if self.poisons(error) { + self.status.record(error); + } else if error != ECANCELED || self.interrupted() { + self.status.report_failure(error); + } + } + /// Installed before the first firmware-visible prefix. Cancellation takes /// the same lock after claiming the signal, so it cannot miss a late lease. pub(crate) fn attach_render_lease( @@ -589,6 +630,9 @@ impl Completion { if self.signalled.swap(true, Ordering::AcqRel) { return; } + if result.is_ok() && self.interrupted() { + result = Err(ECANCELED); + } if let (Some(feed), Ok(ticks)) = (&self.feed, &result) { // Compute reports its dispatch envelope in the first pair, a // render its fragment pass in the second. @@ -600,13 +644,7 @@ impl Completion { } if let Err(error) = result { // Spared work cancelled after another VM's recovery is not this client's device loss. - if self.spared() { - if error != ECANCELED { - self.status.report_failure(error); - } - } else if error != ECANCELED { - self.status.record(error); - } + self.report(error); } self.release_healthy(); self.signal_terminal(result); @@ -640,8 +678,8 @@ impl Completion { } if self.spared() { self.status.report_failure(error); - } else if error != ECANCELED { - self.status.record(error); + } else { + self.report(error); } self.signal_terminal(Err(error)); self.output.finish(Err(error)); diff --git a/drivers/gpu/drm/asahi/g17/initdata.rs b/drivers/gpu/drm/asahi/g17/initdata.rs index 8f7551b1763c9..0b92aa3cdd1d4 100644 --- a/drivers/gpu/drm/asahi/g17/initdata.rs +++ b/drivers/gpu/drm/asahi/g17/initdata.rs @@ -1564,17 +1564,22 @@ impl super::recovery::Memory for InitData { Ok(()) } - fn entries(&self) -> Result { + fn entry(&self, index: usize) -> Result<(u32, u32)> { + if index >= super::recovery::RECOVERY_RECORDS { + return Err(EINVAL); + } let cluster = self.object(self.cluster)?; - let base = self.status_b + abi::PrimaryStatusB::RECOVERY_INFO; + let offset = self.status_b + + abi::PrimaryStatusB::RECOVERY_INFO + + index * core::mem::size_of::(); fence(Ordering::Acquire); - let mut entries = super::recovery::Entries::default(); - for index in 0..256 { - let offset = base + index * core::mem::size_of::(); - let flags = cluster.word(offset)?.load(Ordering::Relaxed); - entries.include(flags); - } - Ok(entries) + let flags = cluster + .word(offset + core::mem::offset_of!(abi::RecoveryInfoEntry, flags))? + .load(Ordering::Relaxed); + let stamp = cluster + .word(offset + core::mem::offset_of!(abi::RecoveryInfoEntry, stamp))? + .load(Ordering::Relaxed); + Ok((flags, stamp)) } fn wait_tick(&self) { diff --git a/drivers/gpu/drm/asahi/g17/queue/compute.rs b/drivers/gpu/drm/asahi/g17/queue/compute.rs index 02ae0bbbb9634..16d6351678bb7 100644 --- a/drivers/gpu/drm/asahi/g17/queue/compute.rs +++ b/drivers/gpu/drm/asahi/g17/queue/compute.rs @@ -718,6 +718,27 @@ impl Queue { visit(active.packet.completion.status()); } } + /// A recovery record names this queue and the completion stamp of the + /// command it interrupted, `kick << 8` like the queue's stamp. Without a + /// stamp match the oldest published command is marked. + pub(crate) fn note_interrupted(&self, stamp: u32) -> recovery::Interrupted { + let sequence = stamp >> 8; + if let Some(active) = self.active.iter().find(|active| { + active + .ticket + .is_some_and(|ticket| (ticket.kick as u32) & 0xff_ffff == sequence) + }) { + active.packet.completion.note_interrupted(); + return recovery::Interrupted::Exact; + } + match self.active.iter().find(|active| active.ticket.is_some()) { + Some(active) => { + active.packet.completion.note_interrupted(); + recovery::Interrupted::Oldest + } + None => recovery::Interrupted::None, + } + } pub(crate) fn classify(&self, class: &dyn Fn(&Arc) -> recovery::Class) { for active in self.active.iter() { active diff --git a/drivers/gpu/drm/asahi/g17/queue/render.rs b/drivers/gpu/drm/asahi/g17/queue/render.rs index 3e65ea3b9a367..1d562da115b60 100644 --- a/drivers/gpu/drm/asahi/g17/queue/render.rs +++ b/drivers/gpu/drm/asahi/g17/queue/render.rs @@ -1088,6 +1088,28 @@ impl Pair { .front() .map(|active| active.packet.completion.spared()) } + /// A recovery record names one of this pair's queues and the completion + /// stamp of the group it interrupted, `(ordinal + 1) << 8` like the pair's + /// stamps. Without a stamp match the oldest published group is marked. + pub(crate) fn note_interrupted(&self, stamp: u32) -> recovery::Interrupted { + let sequence = stamp >> 8; + if let Some(active) = self + .active + .iter() + .filter(|active| active.outer_published) + .find(|active| (active.ticket.ordinal.wrapping_add(1) as u32) & 0xff_ffff == sequence) + { + active.packet.completion.note_interrupted(); + return recovery::Interrupted::Exact; + } + match self.active.iter().find(|active| active.outer_published) { + Some(active) => { + active.packet.completion.note_interrupted(); + recovery::Interrupted::Oldest + } + None => recovery::Interrupted::None, + } + } pub(crate) fn visit_owners(&self, visit: &mut dyn FnMut(&Arc)) { for active in self.active.iter() { visit(active.packet.completion.status()); @@ -1271,6 +1293,9 @@ impl Pair { if self.quarantined || self.active.len() == 0 || self.replay.len() != 0 { return Ok(false); } + if let Some(progressed) = self.settle_interrupted_front(host, defer, grace)? { + return Ok(progressed); + } use crate::g17::replay::{self, Decision, Group}; let qids = self.qids(); let memory = &self.memory; @@ -1295,10 +1320,11 @@ impl Pair { let stamps = observer.stamps()?; let (_, state) = observer.host.epoch()?; // A torn sample is no proof; a later poll or the timeout looks again. + // Interrupted work is run again only if it never started. Ok(matches!( retirement::abandoned(&active.ticket, snapshot, stamps, state, false), Ok(true) - )) + ) && (!active.packet.completion.interrupted() || snapshot.untouched())) })() .unwrap_or_else(|error: Error| { failure = Some(error); @@ -1391,6 +1417,81 @@ impl Pair { Ok(true) } + /// A front group a restart interrupted after its tiler started, which firmware + /// then retired without writing its fragment end, may have run partly. It is + /// not replayed (blending or storage writes could apply twice) and fails now, + /// reported to its owner, instead of waiting out the render timeout. Returns + /// None when the front is not such a group. + fn settle_interrupted_front( + &mut self, + host: &mut impl Host, + defer: &mut impl FnMut(Deferred) -> Result, + grace: bool, + ) -> Result> { + use retirement::Memory; + let qids = self.qids(); + let Some(front) = self.active.front() else { + return Ok(None); + }; + if !front.outer_published || front.failed || !front.packet.completion.interrupted() { + return Ok(None); + } + let ordinal = front.ticket.ordinal; + let ticket = front.ticket; + let mut observer = Observer { + memory: &self.memory, + qids, + host: &mut *host, + }; + core::sync::atomic::fence(core::sync::atomic::Ordering::Acquire); + let snapshot = observer.snapshot(ordinal)?; + let stamps = observer.stamps()?; + let (_, state) = observer.host.epoch()?; + if snapshot.untouched() + || !matches!( + retirement::abandoned(&ticket, snapshot, stamps, state, false), + Ok(true) + ) + { + return Ok(None); + } + let now = crate::g17::now_ns(); + let front = self.active.front_mut().ok_or(EIO)?; + if grace { + let Some(since) = front.abandoned_since else { + front.abandoned_since = Some(now); + return Ok(Some(false)); + }; + if now.saturating_sub(since) < ABANDON_GRACE_NS { + return Ok(Some(false)); + } + } + if !front.flist_completed { + let qos = self.qos_owners(); + let buffer = self.buffer_id(); + let pool = self.free_list.id(); + let front = self.active.front_mut().ok_or(EIO)?; + host.complete_render( + (!front.qos_completed).then_some(qos), + pool, + front.generation, + buffer, + )?; + front.flist_completed = true; + front.qos_completed = true; + } + pr_warn!( + "G17: render slot {}: group {} was interrupted by a GPU recovery after it started; failing it\n", + self.slot, + ordinal + ); + let packet = self.active.front().ok_or(EIO)?.packet.clone(); + defer(Deferred::Retired(packet.completion.clone(), Err(ECANCELED)))?; + self.memory.retire(ordinal)?; + self.active.pop().ok_or(EIO)?; + Ok(Some(true)) + } + /// The pending replay cannot proceed (closed owner, cancellation or a /// publication refusal). Its successors cannot run without it and fail with /// it. ECANCELED leaves the VM usable; any other error is the ordinary diff --git a/drivers/gpu/drm/asahi/g17/queue/render/retirement.rs b/drivers/gpu/drm/asahi/g17/queue/render/retirement.rs index 24aeed6bcd0ec..00e4997ef3661 100644 --- a/drivers/gpu/drm/asahi/g17/queue/render/retirement.rs +++ b/drivers/gpu/drm/asahi/g17/queue/render/retirement.rs @@ -122,6 +122,11 @@ impl Snapshot { fn executed(self) -> bool { self.pass[0] != 0 && self.pass[1] != 0 } + + /// Neither the tiler start nor the fragment end was written: nothing ran. + pub(crate) fn untouched(self) -> bool { + self.pass == [0, 0] + } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] diff --git a/drivers/gpu/drm/asahi/g17/recovery.rs b/drivers/gpu/drm/asahi/g17/recovery.rs index 8aac25ab80d68..cfadf4bb31767 100644 --- a/drivers/gpu/drm/asahi/g17/recovery.rs +++ b/drivers/gpu/drm/asahi/g17/recovery.rs @@ -30,18 +30,28 @@ const MAX_GUILTY_VMS: usize = 8; /// execution allowance (`timeout::POLL_MS` plus renewals). const RECOVERY_POLLS: usize = 10_000; -/// Per-QID classifications from the complete firmware recovery table. +/// Records in the firmware recovery table. +pub(super) const RECOVERY_RECORDS: usize = 256; + +/// Per-QID classifications from the firmware recovery table. /// Multiple records for a queue contribute all of their classification bits. #[derive(Default)] pub(super) struct Entries { guilty: u128, cancelled: u128, + /// Valid records, each naming one command that was in flight at the restart. + pub(super) listed: u32, + /// Listed commands found by their exact stamp, and by the oldest-work fallback. + pub(super) exact: u32, + pub(super) fallback: u32, } impl Entries { - pub(super) fn include(&mut self, flags: u32) { + /// Returns false at the first invalid record: the table is a packed list and + /// records after it are left over from an earlier restart. + pub(super) fn include(&mut self, flags: u32) -> bool { if flags & 1 == 0 { - return; + return false; } let qid = (flags >> 4) & 0x7f; let bit = 1u128 << qid; @@ -51,9 +61,22 @@ impl Entries { if flags & u32::from(CANCELLED) != 0 { self.cancelled |= bit; } + self.listed += 1; + true } } +/// How a listed recovery record was matched to retained work. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub(super) enum Interrupted { + /// No retained command of the queue. + None, + /// The command whose completion stamp equals the record's. + Exact, + /// No stamp matched; the queue's oldest unretired command was marked instead. + Oldest, +} + /// How an error completion affects its VM's sticky status and other jobs. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub(super) enum Class { @@ -75,6 +98,10 @@ pub(super) trait Queues { fn visit_owners(&self, qid: u8, visit: &mut dyn FnMut(&Arc)); /// Classify every in-flight completion; return QIDs of render slots with spared work. fn classify_work(&mut self, class: &dyn Fn(&Arc) -> Class) -> u128; + /// Mark the command a recovery record lists as interrupted. It may have + /// partly executed, so it must never report success; see + /// `Completion::note_interrupted`. + fn note_interrupted(&mut self, qid: u8, stamp: u32) -> Interrupted; fn settle_blamed_render(&mut self, qid: u8, error: Error); fn settle_cancelled_renders(&mut self, qids: u128); fn rescan_compute(&mut self) -> Result; @@ -89,7 +116,9 @@ pub(super) trait Memory { fn fault_sources(&self) -> Result; fn transition(&self, expected: u32, next: u32) -> Result; fn clear_timestamps(&self) -> Result; - fn entries(&self) -> Result; + /// One recovery record's flags and completion stamp, read while firmware + /// is halted in state 3. + fn entry(&self, index: usize) -> Result<(u32, u32)>; /// One millisecond wait; this is polled because the firmware sends no state-3 notification. fn wait_tick(&self); } @@ -228,14 +257,26 @@ pub(super) fn service( return Err(ETIMEDOUT); } // A failed table read leaves only the explicit blamed QID as evidence of guilt. - let entries = host.entries().unwrap_or_default(); + // Every listed command was in flight when firmware stopped. Firmware may + // still retire it with stamps and records that look like success, so it is + // marked before firmware resumes and never reports success. + let entries = read_entries(host); host.transition(3, 0)?; if host.recovery_state()? != 0 { return Err(EIO); } host.recovery_mut().pending = None; + let (listed, exact, fallback) = (entries.listed, entries.exact, entries.fallback); let (cancelled, blame) = classify(host, entries, request.blamed, &mut sources); + if listed != 0 { + pr_warn!( + "G17: GPU recovery: {} commands were in flight ({} matched by stamp, {} by queue); they fail instead of completing\n", + listed, + exact, + fallback + ); + } if let Some(qid) = request.blamed { pr_warn!( "G17: GPU recovery (reason {:?}): firmware blamed queue {}, {}\n", @@ -255,6 +296,26 @@ pub(super) fn service( Ok(ServiceResult { scan_error, target_error }) } +/// Walks the packed recovery table up to its first invalid record and marks +/// every listed command interrupted. A failed read keeps what was read so far. +fn read_entries(host: &mut H) -> Entries { + let mut entries = Entries::default(); + for index in 0..RECOVERY_RECORDS { + let Ok((flags, stamp)) = host.entry(index) else { + break; + }; + if !entries.include(flags) { + break; + } + match host.note_interrupted(((flags >> 4) & 0x7f) as u8, stamp) { + Interrupted::Exact => entries.exact += 1, + Interrupted::Oldest => entries.fallback += 1, + Interrupted::None => {} + } + } + entries +} + /// Resolves guilty QIDs before changing any completion's classification. Unknown ownership or /// too many distinct guilty VMs leaves all prior classifications intact. The oldest spared work /// of a QID makes later blame collateral to its previous recovery, rather than new VM guilt. diff --git a/drivers/gpu/drm/asahi/g17/recovery/safety.rs b/drivers/gpu/drm/asahi/g17/recovery/safety.rs index c30e45e22579a..47675c9638395 100644 --- a/drivers/gpu/drm/asahi/g17/recovery/safety.rs +++ b/drivers/gpu/drm/asahi/g17/recovery/safety.rs @@ -74,6 +74,17 @@ mod tests { for _ in 0..1024 { assert!(storm.note(u64::MAX)); } } #[test] + fn recovery_table_ends_at_its_first_invalid_record() { + use crate::g17::recovery::Entries; + let mut entries = Entries::default(); + assert!(entries.include(1 | (12 << 4))); + assert!(entries.include(1 | 2 | (17 << 4))); + assert!(!entries.include(0)); + assert_eq!(entries.listed, 2); + assert_eq!(entries.guilty, 1u128 << 17); + assert_eq!(entries.cancelled, 0); + } + #[test] fn replay_dependency_checks_never_wait_for_event_service() { use crate::g17::dependency::replay_status; assert_eq!(replay_status(0), Err(EBUSY)); diff --git a/drivers/gpu/drm/asahi/g17/runtime.rs b/drivers/gpu/drm/asahi/g17/runtime.rs index b07423bd22115..f50e8e350e936 100644 --- a/drivers/gpu/drm/asahi/g17/runtime.rs +++ b/drivers/gpu/drm/asahi/g17/runtime.rs @@ -1275,7 +1275,7 @@ impl DeferredBatch { // it. Expose terminal VM status now, before off-lock fence callbacks. // Recovery-spared and cancelled commands never poison their VM. if let Deferred::FailedOwned(completion, error) = &value { - if *error != ECANCELED && !completion.spared() { + if completion.poisons(*error) { completion.status().record(*error); } } diff --git a/drivers/gpu/drm/asahi/g17/runtime/events.rs b/drivers/gpu/drm/asahi/g17/runtime/events.rs index c1a2ebe232fcb..e13ec3e35e13c 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/events.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/events.rs @@ -131,8 +131,8 @@ impl recovery::Memory for Service<'_> { fn clear_timestamps(&self) -> Result { self.firmware.init.clear_timestamps() } - fn entries(&self) -> Result { - self.firmware.init.entries() + fn entry(&self, index: usize) -> Result<(u32, u32)> { + self.firmware.init.entry(index) } fn wait_tick(&self) { self.firmware.init.wait_tick(); @@ -203,6 +203,22 @@ impl recovery::Queues for Service<'_> { } self.firmware.queues.classify_render(class) } + fn note_interrupted(&mut self, qid: u8, stamp: u32) -> recovery::Interrupted { + let render = self.firmware.queues.note_render_interrupted(qid, stamp); + if render != recovery::Interrupted::None { + return render; + } + self.firmware + .queues + .compute + .iter() + .flatten() + .filter_map(|entry| entry.queue.as_deref()) + .find(|queue| queue.qid() == qid) + .map_or(recovery::Interrupted::None, |queue| { + queue.note_interrupted(stamp) + }) + } fn settle_blamed_render(&mut self, qid: u8, error: Error) { let result = self .firmware diff --git a/drivers/gpu/drm/asahi/g17/runtime/render.rs b/drivers/gpu/drm/asahi/g17/runtime/render.rs index f4f45a98005c3..4aaad40eee765 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render.rs @@ -830,6 +830,22 @@ impl Registry { .find(|pair| pair.qids().contains(&qid)) .and_then(|pair| pair.oldest_spared()) } + /// Marks the interrupted group of the pair that owns `qid`. + pub(in crate::g17) fn note_render_interrupted( + &self, + qid: u8, + stamp: u32, + ) -> crate::g17::recovery::Interrupted { + self.render + .entries + .iter() + .flatten() + .filter_map(|entry| entry.pair.as_ref()) + .find(|pair| pair.qids().contains(&qid)) + .map_or(crate::g17::recovery::Interrupted::None, |pair| { + pair.note_interrupted(stamp) + }) + } pub(in crate::g17) fn visit_render_owners( &self, qid: u8, From 30f85560fd28ce05151c16eab7a58433d125a81e Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 25 Oct 2026 00:00:33 -0400 Subject: [PATCH 047/388] drm/asahi: stop the G17 device on a watchdog storm only without progress Eight job watchdog expirations within 30 seconds stopped the device, even while other work kept retiring. Under overload (six concurrent ray-query stress clients) that happened within a minute: each timeout already failed its own client, yet the ninth took every other client and the desktop with it. Count retired commands and start a new storm window whenever work has retired since the previous expiration. A device that stops retiring anything still stops after eight; an overloaded one only loses the clients whose work timed out. --- drivers/gpu/drm/asahi/g17/queue/compute.rs | 1 + drivers/gpu/drm/asahi/g17/queue/render.rs | 1 + drivers/gpu/drm/asahi/g17/recovery.rs | 5 +++- drivers/gpu/drm/asahi/g17/recovery/safety.rs | 26 +++++++++++++++++++- 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/queue/compute.rs b/drivers/gpu/drm/asahi/g17/queue/compute.rs index 16d6351678bb7..c0916b639ee97 100644 --- a/drivers/gpu/drm/asahi/g17/queue/compute.rs +++ b/drivers/gpu/drm/asahi/g17/queue/compute.rs @@ -968,6 +968,7 @@ impl Queue { } let front = self.active.front().ok_or(EIO)?; if let Some(ticket) = front.ticket { + crate::g17::recovery::note_retired(); crate::g17::trace::complete(crate::g17::trace::COMPUTE, ticket.qid, ticket.ordinal, &front.packet, [timestamps[0], timestamps[1]]); } diff --git a/drivers/gpu/drm/asahi/g17/queue/render.rs b/drivers/gpu/drm/asahi/g17/queue/render.rs index 1d562da115b60..5c201ee6ec965 100644 --- a/drivers/gpu/drm/asahi/g17/queue/render.rs +++ b/drivers/gpu/drm/asahi/g17/queue/render.rs @@ -1260,6 +1260,7 @@ impl Pair { front.qos_completed = true; front.flist_completed = true; } + crate::g17::recovery::note_retired(); crate::g17::trace::complete(crate::g17::trace::RENDER, qids[1], ordinal as u32, &front.packet, [timestamps[0], timestamps[3]]); defer(Deferred::Retired( diff --git a/drivers/gpu/drm/asahi/g17/recovery.rs b/drivers/gpu/drm/asahi/g17/recovery.rs index cfadf4bb31767..84a01d975c36d 100644 --- a/drivers/gpu/drm/asahi/g17/recovery.rs +++ b/drivers/gpu/drm/asahi/g17/recovery.rs @@ -10,6 +10,7 @@ mod attribution; mod slot; mod safety; +pub(crate) use safety::note_retired; pub(crate) use attribution::Sources; use attribution::Attribution; @@ -152,7 +153,9 @@ impl State { restarts: safety::Storm::new(), } } - pub(super) fn timeout_limit(&mut self, now: u64) -> bool { self.timeouts.note(now) } + pub(super) fn timeout_limit(&mut self, now: u64) -> bool { + self.timeouts.note_unless_progress(now, safety::retired()) + } pub(super) fn restart_limit(&mut self, now: u64) -> bool { self.restarts.note(now) } pub(super) fn generation(&self) -> u64 { self.accepted diff --git a/drivers/gpu/drm/asahi/g17/recovery/safety.rs b/drivers/gpu/drm/asahi/g17/recovery/safety.rs index 47675c9638395..b97fc26914329 100644 --- a/drivers/gpu/drm/asahi/g17/recovery/safety.rs +++ b/drivers/gpu/drm/asahi/g17/recovery/safety.rs @@ -2,17 +2,41 @@ //! Fail closed on repeated watchdog/restart traffic, even across client loss. +use core::sync::atomic::{AtomicU64, Ordering}; + const WINDOW_NS: u64 = 30_000_000_000; const LIMIT: u8 = 8; +/// Commands retired successfully. A watchdog storm means the GPU stopped making progress; +/// timeouts of overloaded clients while other work keeps retiring are not one. +static RETIRED: AtomicU64 = AtomicU64::new(0); + +/// Called for every command that retires without error. +pub(crate) fn note_retired() { + RETIRED.fetch_add(1, Ordering::Relaxed); +} + +pub(super) fn retired() -> u64 { + RETIRED.load(Ordering::Relaxed) +} + pub(super) struct Storm { start: Option, count: u8, stopped: bool, + progress: u64, } impl Storm { pub(super) const fn new() -> Self { - Self { start: None, count: 0, stopped: false } + Self { start: None, count: 0, stopped: false, progress: 0 } + } + /// Like [`Self::note`], but work retired since the previous event starts a new window. + pub(super) fn note_unless_progress(&mut self, now: u64, progress: u64) -> bool { + if !self.stopped && progress != self.progress { + self.progress = progress; + self.start = None; + } + self.note(now) } pub(super) fn note(&mut self, now: u64) -> bool { if self.stopped { return true; } From 2c0abe2d090a9701b13b22d5ac56fb24d87cfe12 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 25 Oct 2026 03:50:55 -0400 Subject: [PATCH 048/388] drm/asahi: scan only the G17 compute completion records written since the last pass The completion scan walked the shared compute completion ring from its cursor until it had found a record for every pending command. A command still running has no record yet, so every pass with work in flight read all 2048 slots: about 3 us on the completion path before any fence was signalled, and the cursor never advanced while a queue stayed busy. Records are appended in order, so remember each slot's control word as of the last pass over it; the first slot that still holds it is where the firmware writes next, and the pass ends there. Stage the pending identities of every queue rather than only those the batch retires, so each new record is folded once and later batches find it latched. A record of a command published after staging, or one whose payload is still being written, ends the pass and is read again by the next one. A command whose completion stamp already covers it has its record in the ring; if a pass ends without it, the whole ring is read once and the cursor moves after the newest record. Same-boot A/B against the previous scan (us per submission, medians of two rounds, app pinned to cpu0 / cpu5): compute.light d2 33.7 -> 31.0 / 43.0 -> 33.8, d64 26.6 -> 20.4 / 14.2 -> 14.3, render.light d32 38.6 -> 34.8 / 30.4 -> 29.8; d1, render d1/d2, queue hops and 16-process compute unchanged. Event worker CPU time at compute d64 drops by a quarter. --- drivers/gpu/drm/asahi/g17/initdata.rs | 84 ++++++-- drivers/gpu/drm/asahi/g17/queue/compute.rs | 27 ++- .../drm/asahi/g17/queue/compute/retirement.rs | 203 ++++++++++++++++-- drivers/gpu/drm/asahi/g17/runtime.rs | 36 +++- 4 files changed, 291 insertions(+), 59 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/initdata.rs b/drivers/gpu/drm/asahi/g17/initdata.rs index 0b92aa3cdd1d4..7e0c36ece43ad 100644 --- a/drivers/gpu/drm/asahi/g17/initdata.rs +++ b/drivers/gpu/drm/asahi/g17/initdata.rs @@ -38,6 +38,26 @@ pub(crate) struct ComputeCompletion<'a> { offset: usize, } +/// Records in the compute completion ring that the host scans. +pub(super) const COMPUTE_COMPLETION_SLOTS: usize = { + let slots = abi::COMPLETION_RING_SIZE / 0x40; + if slots < 0x800 { slots } else { 0x800 } +}; + +/// Timestamp bits of a completion record's start and end words. +const COMPLETION_TIME_MASK: u64 = 0x003f_ffff_ffff_ffff; + +/// What one completion-ring slot gave a scan. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum CompletionFold { + /// No new observation of a pending request; the scan continues. + Passed, + /// Every request now has an observation. + Done, + /// A pending request's record whose payload the firmware is still writing. + Incomplete, +} + impl ComputeCompletion<'_> { pub(crate) fn read(&self, word: usize) -> Result { if word >= 8 { @@ -711,28 +731,66 @@ impl InitData { /// Snapshot the shared compute completion ring once per service transaction. The queue /// scanner latches exact identities; firmware retains sole ownership of these slots. - /// Visits ring slots from `start`, wrapping once, until `record` reports - /// that nothing more is wanted. Returns the slot after the last one visited - /// when the scan stopped early, or `start` after a complete wrap. - pub(super) fn scan_compute_completions( + /// Folds the compute completion records the firmware appended since `start`, in ring + /// order. `seen` holds every slot's control word as of the previous pass over it: the + /// first slot still holding it is where the firmware writes next, and the pass ends there + /// instead of reading the rest of the ring. A pass also ends once `record` has found every + /// request, or at a requested record whose payload is still being written, which the next + /// pass reads again. Returns the slot the next pass starts at. + pub(super) fn fold_compute_completions( &self, start: usize, - mut record: impl FnMut(ComputeCompletion<'_>) -> Result, + seen: &mut [u64], + mut record: impl FnMut(ComputeCompletion<'_>) -> Result, + ) -> Result { + let object = self.object(self.completion_rings[0])?; + if seen.len() != COMPUTE_COMPLETION_SLOTS { + return Err(EINVAL); + } + fence(Ordering::Acquire); + let completion = |slot: usize| ComputeCompletion { + object, + offset: slot * 0x40, + }; + super::queue::compute::retirement::walk_completions( + start, + seen, + |slot| completion(slot).read(0), + |slot| record(completion(slot)), + ) + } + + /// Folds every slot of the compute completion ring and records its control word in + /// `seen`. For a pass that stopped before a record the firmware must already have + /// written. Returns the slot after the newest record (latest end timestamp), where the + /// firmware writes next. + pub(super) fn resync_compute_completions( + &self, + seen: &mut [u64], + mut record: impl FnMut(ComputeCompletion<'_>) -> Result, ) -> Result { let object = self.object(self.completion_rings[0])?; - let slots = (abi::COMPLETION_RING_SIZE / 0x40).min(0x800); - let start = start % slots; + if seen.len() != COMPUTE_COMPLETION_SLOTS { + return Err(EINVAL); + } fence(Ordering::Acquire); - for step in 0..slots { - let slot = (start + step) % slots; - if record(ComputeCompletion { + let mut newest = None; + for slot in 0..COMPUTE_COMPLETION_SLOTS { + let completion = ComputeCompletion { object, offset: slot * 0x40, - })? { - return Ok((slot + 1) % slots); + }; + let control = completion.read(0)?; + seen[slot] = control; + if control != 0 { + let end = completion.read(5)? & COMPLETION_TIME_MASK; + if newest.is_none_or(|(_, latest)| end > latest) { + newest = Some((slot, end)); + } } + record(completion)?; } - Ok(start) + Ok(newest.map_or(0, |(slot, _)| (slot + 1) % COMPUTE_COMPLETION_SLOTS)) } fn object(&self, index: usize) -> Result<&KernelObject> { diff --git a/drivers/gpu/drm/asahi/g17/queue/compute.rs b/drivers/gpu/drm/asahi/g17/queue/compute.rs index c0916b639ee97..c8e00e2b563c2 100644 --- a/drivers/gpu/drm/asahi/g17/queue/compute.rs +++ b/drivers/gpu/drm/asahi/g17/queue/compute.rs @@ -786,33 +786,32 @@ impl Queue { || self.retire_pending || masks.is_none_or(|m| m[self.qid() as usize / 64] & (1 << (self.qid() % 64)) != 0) } - /// Stage every unlatched exact identity before the device's single shared-ring scan. - pub(crate) fn request_observations( - &self, - batch: &mut Batch, - masks: Option<[u64; 2]>, - ) -> Result { - if self.quarantined || !self.selected(masks) { + /// Stage every unlatched exact identity before the device's single shared-ring scan, + /// whichever queues the scan's batch retires. + pub(crate) fn request_observations(&self, batch: &mut Batch) -> Result { + if self.quarantined { return Ok(false); } + batch.stage_frontier(self.qid(), self.kick.timestamp().get())?; let mut requested = false; + let mut stamp = None; for active in self.active.iter() { let Some(ticket) = active.ticket else { break; }; if active.observed.is_none() { - batch.request(&ticket)?; + let stamp = match stamp { + Some(stamp) => stamp, + None => *stamp.insert(self.graph.stamp()?), + }; + batch.request(&ticket, retirement::stamp_covers(stamp, ticket.kick))?; requested = true; } } Ok(requested) } - pub(crate) fn latch_observations( - &mut self, - batch: &mut Batch, - masks: Option<[u64; 2]>, - ) -> Result { - if self.quarantined || !self.selected(masks) { + pub(crate) fn latch_observations(&mut self, batch: &mut Batch) -> Result { + if self.quarantined { return Ok(()); } for active in self.active.iter_mut() { diff --git a/drivers/gpu/drm/asahi/g17/queue/compute/retirement.rs b/drivers/gpu/drm/asahi/g17/queue/compute/retirement.rs index 46df3598362b7..c2b49e7b436d7 100644 --- a/drivers/gpu/drm/asahi/g17/queue/compute/retirement.rs +++ b/drivers/gpu/drm/asahi/g17/queue/compute/retirement.rs @@ -3,7 +3,7 @@ //! Exact identities and visibility witnesses for a physical compute queue. //! A shared work-state counter can include other engines and cannot prove this queue retired. -use crate::g17::initdata::ComputeCompletion; +use crate::g17::initdata::{CompletionFold, ComputeCompletion}; use kernel::prelude::*; pub(crate) const IN_FLIGHT: usize = 16; @@ -161,11 +161,46 @@ pub(crate) fn quarantined_retired( && submitted == pending) } +/// Walks a completion ring of `seen.len()` slots from `start` over the records written since +/// the previous walk: `seen` holds each slot's control word as of the last walk that passed it, +/// so the first slot still holding it is where the firmware writes next. `record` folds one +/// slot; the walk also ends once it reports every request found, or at a requested record +/// still being written, which the next walk reads again. Returns where the next walk starts. +pub(crate) fn walk_completions( + start: usize, + seen: &mut [u64], + mut control: impl FnMut(usize) -> Result, + mut record: impl FnMut(usize) -> Result, +) -> Result { + let slots = seen.len(); + if slots == 0 { + return Err(EINVAL); + } + let start = start % slots; + for step in 0..slots { + let slot = (start + step) % slots; + let word = control(slot)?; + if word == seen[slot] { + return Ok(slot); + } + match record(slot)? { + CompletionFold::Incomplete => return Ok(slot), + CompletionFold::Passed => seen[slot] = word, + CompletionFold::Done => { + seen[slot] = word; + return Ok((slot + 1) % slots); + } + } + } + Ok(start) +} + #[derive(Clone, Copy)] struct Request { descriptor: u64, kick: u64, observed: Option, + covered: bool, next: u16, } @@ -186,8 +221,18 @@ pub(crate) struct Batch { open: Option, /// Requests with no observation yet; the scan stops when this reaches zero. unobserved: usize, + /// Unobserved requests whose queue's completion stamp already covered them when staged. + /// The firmware stores the stamp after the record, so each has a record in the ring. + covered: usize, + /// Per QID, the next kick its queue had not published when the batch was staged + /// ([`NO_FRONTIER`] for a QID without a staged queue). A record at or past it belongs + /// to a command published during the scan, which a later batch stages. + frontiers: [u64; QIDS], } +const NO_FRONTIER: u64 = u64::MAX; +const KICK_MASK: u64 = (1 << 40) - 1; + impl Batch { pub(crate) fn new() -> Result { let mut heads = KVVec::with_capacity(QIDS * IN_FLIGHT, GFP_KERNEL)?; @@ -200,6 +245,8 @@ impl Batch { slots: [(0, 0); QIDS], open: None, unobserved: 0, + covered: 0, + frontiers: [NO_FRONTIER; QIDS], }) } pub(crate) fn clear(&mut self) { @@ -207,12 +254,29 @@ impl Batch { self.slots.fill((0, 0)); self.open = None; self.unobserved = 0; + self.covered = 0; + self.frontiers.fill(NO_FRONTIER); + } + /// Records the first kick `qid`'s queue had not yet published when this batch was staged. + pub(crate) fn stage_frontier(&mut self, qid: u8, next: u64) -> Result { + *self.frontiers.get_mut(usize::from(qid)).ok_or(EINVAL)? = next & KICK_MASK; + Ok(()) + } + /// A record of `qid` with `kick` names a command published after this batch was staged. + fn published_after_staging(&self, qid: usize, kick: u64) -> bool { + let frontier = self.frontiers[qid]; + frontier != NO_FRONTIER && kick.wrapping_sub(frontier) & KICK_MASK < 1 << 39 } /// True once every requested identity has an observation. pub(crate) fn complete(&self) -> bool { self.unobserved == 0 } - pub(crate) fn request(&mut self, ticket: &Ticket) -> Result { + /// Some request whose record must already be in the ring is still unobserved. + pub(crate) fn covered_pending(&self) -> bool { + self.covered != 0 + } + /// Stages `ticket`; `covered` when its queue's completion stamp already covers its kick. + pub(crate) fn request(&mut self, ticket: &Ticket, covered: bool) -> Result { if ticket.descriptor == 0 || ticket.kick == 0 { return Err(EINVAL); } @@ -238,6 +302,7 @@ impl Batch { descriptor: ticket.descriptor, kick: ticket.kick, observed: None, + covered, next, }) .map_err(|_| ENOSPC)?; @@ -245,23 +310,37 @@ impl Batch { *slot = (start, slot.1 + 1); self.open = Some(ticket.qid); self.unobserved += 1; + self.covered += usize::from(covered); Ok(()) } /// Capture the control word once. Most slots belong to an older kick of a /// selected QID; reject those before reading any additional shared words. - pub(crate) fn fold(&mut self, record: ComputeCompletion<'_>) -> Result { - self.fold_reader(|word| record.read(word)) + pub(crate) fn fold(&mut self, record: ComputeCompletion<'_>) -> Result { + if !self.fold_reader(|word| record.read(word))? { + return Ok(CompletionFold::Incomplete); + } + Ok(self.folded()) + } + + /// Like [`Self::fold`], for a scan that reads every slot regardless of the outcome. + pub(crate) fn fold_any(&mut self, record: ComputeCompletion<'_>) -> Result { + self.fold_reader(|word| record.read(word)).map(|_| ()) } - fn fold_reader(&mut self, mut read: impl FnMut(usize) -> Result) -> Result { + fn folded(&self) -> CompletionFold { + if self.complete() { CompletionFold::Done } else { CompletionFold::Passed } + } + + /// Ok(false) when a pending request's record is still being written. + fn fold_reader(&mut self, mut read: impl FnMut(usize) -> Result) -> Result { let control = read(0)?; let qid = ((control >> 9) & 0x7f) as usize; let (_, count) = self.slots[qid]; - if count == 0 { - return Ok(()); - } let kick = (((control >> 8) & 0x00ff_ffff_ff00) | ((control >> 48) & 0xff)) & 0xff_ffff_ffff; + if count == 0 { + return Ok(!self.published_after_staging(qid, kick)); + } let mut index = *self.heads.get(request_bucket(qid, kick)).ok_or(EIO)?; let mut descriptor = None; let mut found = None; @@ -276,7 +355,7 @@ impl Batch { } else { let value = read(6)? & !1; if value == 0 || read(7)? == 0 { - return Ok(()); + return Ok(request.observed.is_some()); } descriptor = Some(value); value @@ -289,23 +368,28 @@ impl Batch { } index = request.next; } - let Some(index) = found else { return Ok(()); }; + let Some(index) = found else { + return Ok(!self.published_after_staging(qid, kick)); + }; let request = self.requests.get_mut(index).ok_or(EIO)?; let timestamps = [read(2)? & TIME_MASK, read(5)? & TIME_MASK]; - if timestamps[0] == 0 - || timestamps[1] == 0 - || request - .observed - .is_some_and(|old| old.timestamps[1] >= timestamps[1]) + if timestamps[0] == 0 || timestamps[1] == 0 { + return Ok(request.observed.is_some()); + } + if request + .observed + .is_some_and(|old| old.timestamps[1] >= timestamps[1]) { - return Ok(()); + return Ok(true); } if request.observed.is_none() { self.unobserved -= 1; + self.covered -= usize::from(request.covered); } request.observed = Some(Observation { timestamps }); - Ok(()) + Ok(true) } + /// A command published after this batch staged its queue's requests was not part of the /// ring snapshot: it stays unobserved until a later batch. pub(crate) fn take(&mut self, ticket: &Ticket) -> Result> { @@ -480,7 +564,7 @@ mod index_tests { #[test] fn stale_kick_reads_only_the_control_even_on_a_bucket_collision() { let mut batch = Batch::new().unwrap(); - batch.request(&ticket(7, 33, 0x1000)).unwrap(); + batch.request(&ticket(7, 33, 0x1000), false).unwrap(); let words = record(&ticket(7, 17, 0x1000), [10, 20]); batch.fold_reader(|word| { assert_eq!(word, 0); @@ -493,7 +577,7 @@ mod index_tests { let mut batch = Batch::new().unwrap(); let first = ticket(7, 33, 0x1000); let second = ticket(7, 49, 0x1000); - batch.request(&first).unwrap(); batch.request(&second).unwrap(); + batch.request(&first, false).unwrap(); batch.request(&second, false).unwrap(); let words = record(&first, [10, 20]); batch.fold_reader(|word| Ok(words[word])).unwrap(); assert!(!batch.complete()); @@ -508,7 +592,7 @@ mod index_tests { let mut batch = Batch::new().unwrap(); let first = ticket(7, 33, 0x1000); let second = ticket(7, 33, 0x2000); - batch.request(&first).unwrap(); batch.request(&second).unwrap(); + batch.request(&first, false).unwrap(); batch.request(&second, false).unwrap(); let words = record(&first, [20, 20]); batch.fold_reader(|word| Ok(words[word])).unwrap(); assert_eq!(batch.take(&first).unwrap().unwrap().timestamps, [20, 20]); @@ -518,7 +602,7 @@ mod index_tests { fn duplicate_identity_keeps_the_first_staged_request() { let mut batch = Batch::new().unwrap(); let current = ticket(7, 33, 0x1000); - batch.request(¤t).unwrap(); batch.request(¤t).unwrap(); + batch.request(¤t, false).unwrap(); batch.request(¤t, false).unwrap(); let words = record(¤t, [10, 20]); batch.fold_reader(|word| Ok(words[word])).unwrap(); assert_eq!(batch.take(¤t).unwrap().unwrap().timestamps, [10, 20]); @@ -527,9 +611,9 @@ mod index_tests { #[test] fn clear_restarts_buckets_and_later_publication_stays_unobserved() { let mut batch = Batch::new().unwrap(); - batch.request(&ticket(7, 33, 0x1000)).unwrap(); batch.clear(); + batch.request(&ticket(7, 33, 0x1000), false).unwrap(); batch.clear(); let current = ticket(7, 49, 0x1000); - batch.request(¤t).unwrap(); + batch.request(¤t, false).unwrap(); let stale = record(&ticket(7, 33, 0x1000), [10, 20]); batch.fold_reader(|word| Ok(stale[word])).unwrap(); assert!(!batch.complete()); @@ -538,4 +622,77 @@ mod index_tests { batch.fold_reader(|word| Ok(words[word])).unwrap(); assert!(batch.complete()); } + #[test] + fn a_requested_record_still_being_written_is_read_again() { + let mut batch = Batch::new().unwrap(); + let current = ticket(7, 33, 0x1000); + batch.request(¤t, true).unwrap(); + let mut words = record(¤t, [10, 20]); + words[7] = 0; + assert!(!batch.fold_reader(|word| Ok(words[word])).unwrap()); + words[7] = 1; + words[5] = 0; + assert!(!batch.fold_reader(|word| Ok(words[word])).unwrap()); + assert!(batch.covered_pending()); + let words = record(¤t, [10, 20]); + assert!(batch.fold_reader(|word| Ok(words[word])).unwrap()); + assert!(batch.complete() && !batch.covered_pending()); + } + #[test] + fn a_command_published_during_the_scan_is_left_for_the_next_batch() { + let mut batch = Batch::new().unwrap(); + batch.stage_frontier(7, 50).unwrap(); + batch.request(&ticket(7, 49, 0x1000), false).unwrap(); + batch.stage_frontier(9, 3).unwrap(); + let late = record(&ticket(7, 50, 0x3000), [30, 40]); + assert!(!batch.fold_reader(|word| Ok(late[word])).unwrap()); + let idle = record(&ticket(9, 3, 0x3000), [30, 40]); + assert!(!batch.fold_reader(|word| Ok(idle[word])).unwrap()); + let earlier = record(&ticket(9, 2, 0x3000), [30, 40]); + assert!(batch.fold_reader(|word| Ok(earlier[word])).unwrap()); + let wrapped = record(&ticket(7, 0xff_ffff_fff0, 0x3000), [30, 40]); + assert!(batch.fold_reader(|word| Ok(wrapped[word])).unwrap()); + } + fn walk(start: usize, seen: &mut [u64], ring: &[u64], outcome: impl Fn(usize) -> CompletionFold) + -> usize + { + walk_completions(start, seen, |slot| Ok(ring[slot]), |slot| Ok(outcome(slot))).unwrap() + } + #[test] + fn a_walk_reads_only_the_records_written_since_the_previous_one() { + let mut seen = [0u64; 8]; + let mut ring = [0u64; 8]; + assert_eq!(walk(0, &mut seen, &ring, |_| CompletionFold::Passed), 0); + ring[0] = 10; ring[1] = 11; ring[2] = 12; + let read = core::cell::Cell::new(0); + assert_eq!(walk(0, &mut seen, &ring, |_| { read.set(read.get() + 1); CompletionFold::Passed }), 3); + assert_eq!(read.get(), 3); + ring[3] = 13; + assert_eq!(walk(3, &mut seen, &ring, |_| CompletionFold::Passed), 4); + // A full lap later the firmware overwrote slots 4..8 and 0..2 with newer records. + for (slot, word) in [(4, 14), (5, 15), (6, 16), (7, 17), (0, 18), (1, 19)] { ring[slot] = word; } + assert_eq!(walk(4, &mut seen, &ring, |_| CompletionFold::Passed), 2); + } + #[test] + fn a_walk_stops_at_a_partial_record_and_after_the_last_request() { + let mut seen = [0u64; 8]; + let ring = [10, 11, 12, 13, 0, 0, 0, 0]; + assert_eq!(walk(0, &mut seen, &ring, |slot| if slot == 2 { CompletionFold::Incomplete } + else { CompletionFold::Passed }), 2); + assert_eq!(seen[..3], [10, 11, 0]); + assert_eq!(walk(2, &mut seen, &ring, |slot| if slot == 2 { CompletionFold::Done } + else { CompletionFold::Passed }), 3); + assert_eq!(walk(3, &mut seen, &ring, |_| CompletionFold::Passed), 4); + } + #[test] + fn an_unrequested_or_stale_record_is_passed() { + let mut batch = Batch::new().unwrap(); + batch.request(&ticket(7, 49, 0x1000), false).unwrap(); + let mut stale = record(&ticket(7, 33, 0x1000), [10, 20]); + stale[7] = 0; + assert!(batch.fold_reader(|word| Ok(stale[word])).unwrap()); + let other = record(&ticket(9, 33, 0x1000), [10, 20]); + assert!(batch.fold_reader(|word| Ok(other[word])).unwrap()); + assert!(!batch.complete() && !batch.covered_pending()); + } } diff --git a/drivers/gpu/drm/asahi/g17/runtime.rs b/drivers/gpu/drm/asahi/g17/runtime.rs index f50e8e350e936..7f89a25ae6d35 100644 --- a/drivers/gpu/drm/asahi/g17/runtime.rs +++ b/drivers/gpu/drm/asahi/g17/runtime.rs @@ -178,6 +178,8 @@ pub(super) struct Registry { pub(super) observations: compute::retirement::Batch, /// Completion-ring slot where the next compute scan starts. completion_cursor: usize, + /// Control word of each completion-ring slot as of the last scan that read it. + completion_seen: KVVec, worker_deferred: Option, teardown: super::teardown::Pending, } @@ -227,6 +229,14 @@ impl Registry { replay_outer_wait: None, observations: compute::retirement::Batch::new()?, completion_cursor: 0, + completion_seen: { + let slots = super::initdata::COMPUTE_COMPLETION_SLOTS; + let mut seen = KVVec::with_capacity(slots, GFP_KERNEL)?; + for _ in 0..slots { + seen.push_within_capacity(0).map_err(|_| ENOMEM)?; + } + seen + }, worker_deferred: Some(DeferredBatch::worker()?), teardown: super::teardown::Pending::new()?, }) @@ -1620,25 +1630,33 @@ impl super::Firmware { let registry = &mut self.queues; registry.observations.clear(); let mut requested = false; + // Every queue's pending identities are staged, not only the ones this batch + // retires: the scan passes each new record once, and a queue retired by a later + // batch finds its observation already latched. for queue in registry .compute .iter() .flatten() .filter_map(|entry| entry.queue.as_deref()) { - requested |= queue.request_observations(&mut registry.observations, masks)?; + requested |= queue.request_observations(&mut registry.observations)?; } - // Completion records are appended in order: resume after the last - // record a previous scan needed and stop once every request is found. + // Completion records are appended in order: fold only the records written since + // the previous scan. A request whose stamp already covers it has its record in the + // ring; if the scan ended before finding one, read the whole ring once. if requested { let observations = &mut registry.observations; - registry.completion_cursor = self.init.scan_compute_completions( + let seen = &mut registry.completion_seen; + registry.completion_cursor = self.init.fold_compute_completions( registry.completion_cursor, - |record| { - observations.fold(record)?; - Ok(observations.complete()) - }, + seen, + |record| observations.fold(record), )?; + if observations.covered_pending() { + registry.completion_cursor = self + .init + .resync_compute_completions(seen, |record| observations.fold_any(record))?; + } } let port = &self.port; let retired = deferred.pending(); @@ -1650,7 +1668,7 @@ impl super::Firmware { .filter_map(|entry| entry.queue.as_deref_mut()) { let busy = !queue.channel_free(); - queue.latch_observations(&mut registry.observations, masks)?; + queue.latch_observations(&mut registry.observations)?; queue.retire( masks, generation, From 39bdd5b8e2d584fff0d342a71a8945e64c8a9673 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 25 Oct 2026 05:46:58 -0400 Subject: [PATCH 049/388] drm/asahi: release pruned G17 frontier entries after the ioctl publishes Pruning a logical queue's frontier at the start of every enqueue dropped the last references of the commands it found finished. That tears their completions down, including the deferred timestamp cleanup, before the new command is prepared, so its doorbell waited for the teardown. Keep the finished entries until the ioctl has enqueued everything and release them then. Without room for them they are released at once, as before. Prune results, failure tracking and dependencies are unchanged. Same-boot phase timers, render.light d1 pinned to cpu5: the prune step before publication 857 -> 269 ns per submit; the teardown now follows the doorbell. --- drivers/gpu/drm/asahi/g17/queue/mod.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/drivers/gpu/drm/asahi/g17/queue/mod.rs b/drivers/gpu/drm/asahi/g17/queue/mod.rs index 0cc64b8b94460..16891524e25a0 100644 --- a/drivers/gpu/drm/asahi/g17/queue/mod.rs +++ b/drivers/gpu/drm/asahi/g17/queue/mod.rs @@ -95,6 +95,9 @@ pub(crate) struct Queue { proof: Proof, sequence: [u64; 2], frontiers: [KVec<(u64, Fences)>; 2], + /// Finished frontier entries pruned during this ioctl. Their last references + /// can tear a whole completion down; that waits until the ioctl has published. + finished: KVec, failures: [Option<(u64, Fence)>; 2], // Closing the userspace queue releases admission even if firmware still // retains one of its execution contexts or installed physical graphs. @@ -133,6 +136,7 @@ impl Queue { proof: Proof::new(), sequence: [0; 2], frontiers: core::array::from_fn(|_| KVec::new()), + finished: KVec::new(), failures: [None, None], _slot: slot, }) @@ -154,11 +158,20 @@ impl Queue { fn prune_frontier(&mut self, index: usize) { let failure = &mut self.failures[index]; + // Without room the entries are released here, as they always could be. + let mut finished = if self.finished.reserve(self.frontiers[index].len(), GFP_KERNEL).is_ok() { + Some(&mut self.finished) + } else { + None + }; self.frontiers[index].retain(|(sequence, fences)| { let status = fences.status(); if status == 0 { return true; } + if let Some(finished) = finished.as_mut() { + let _ = finished.push_within_capacity(fences.clone()); + } // One earliest failed ordinal covers every later prefix without // retaining its packet, mappings, or every failed fence. if status < 0 && failure.as_ref().is_none_or(|(first, _)| *sequence < *first) { @@ -461,6 +474,8 @@ impl crate::queue::Queue for Queue { } Ok(()) })(); + // Everything this ioctl publishes is visible; release what it pruned. + self.finished.clear(); if let Err(error) = result { self.context.status().record_if_device_loss(error); if [EIO, ETIMEDOUT, ENODEV].contains(&error) || self.context.status().get() != 0 { From b41ecb56c0775a43b747adaa37661b353651dbff Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 25 Oct 2026 05:46:59 -0400 Subject: [PATCH 050/388] drm/asahi: publish both G17 render QoS owners in one pass Each render publication published its fragment and tiling QoS owners one after the other. Every publish read back the row it had just stored byte by byte and read-modify-wrote the submission counters, all in the uncached QoS table, and the second publish read words in the cache lines the first had just written, so its reads waited for those stores to drain. Publish a command's owners together: check every owner first, read each table word once before the first store, then store the identities, one fence, and the counters. The firmware-visible values and their order per owner are those of one publish per owner; a refused publication now changes nothing instead of being cancelled. Compute keeps its single owner and gets the same read-before-store pass. Same-boot phase timers, pinned to cpu5: render QoS publication 1041 + 265 -> 262 ns per submit; compute ioctl entry to doorbell 9.05 -> 8.44 us. --- drivers/gpu/drm/asahi/g17/port.rs | 11 ++ drivers/gpu/drm/asahi/g17/qos.rs | 143 ++++++++++++------ drivers/gpu/drm/asahi/g17/queue/render.rs | 17 ++- drivers/gpu/drm/asahi/g17/runtime/render.rs | 8 +- .../drm/asahi/g17/runtime/render/direct.rs | 8 +- 5 files changed, 127 insertions(+), 60 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/port.rs b/drivers/gpu/drm/asahi/g17/port.rs index 9acd9ff6e5910..45ddae9719363 100644 --- a/drivers/gpu/drm/asahi/g17/port.rs +++ b/drivers/gpu/drm/asahi/g17/port.rs @@ -333,6 +333,17 @@ impl Port { self.qos.lock().publish(&view, owner, scheduler, policy, qos::clock) } + /// Publishes the two owners of a render command, fragment then tiling. + pub(super) fn qos_publish_pair( + &self, + owners: [qos::Owner; 2], + scheduler: u64, + policy: Policy, + ) -> Result<[qos::Publication; 2]> { + let view = self.qos_view()?; + self.qos.lock().publish_owners(&view, owners, scheduler, policy, qos::clock) + } + pub(super) fn qos_cancel(&self, publication: qos::Publication) -> Result { let view = self.qos_view()?; self.qos.lock().cancel(&view, publication) diff --git a/drivers/gpu/drm/asahi/g17/qos.rs b/drivers/gpu/drm/asahi/g17/qos.rs index 4f6913ad5ee95..b36656b0174a7 100644 --- a/drivers/gpu/drm/asahi/g17/qos.rs +++ b/drivers/gpu/drm/asahi/g17/qos.rs @@ -53,6 +53,14 @@ impl<'a> View<'a> { fn row(&self, qid: usize) -> [u8; 8] { core::array::from_fn(|index| self.rows[qid][index].load(Ordering::Relaxed)) } + /// The complete row in one access. The table is uncached: a publication reads + /// each row once, before it stores anything. + fn row_word(&self, qid: usize) -> [u8; 8] { + let row = core::ptr::from_ref(&self.rows[qid]).cast::(); + // SAFETY: Rows are eight bytes at eight-byte offsets of the eight-byte + // aligned table (checked in from_raw), so the row is one aligned word. + unsafe { (*row).load(Ordering::Relaxed) }.to_le_bytes() + } } #[derive(Clone, Copy, Debug)] @@ -108,66 +116,111 @@ impl Accounting { policy: Policy, clock: impl FnOnce() -> (u64, u64), ) -> Result { - let (qid, qos, dm) = owner.indices()?; + let [publication] = self.publish_owners(view, [owner], scheduler, policy, clock)?; + Ok(publication) + } + + /// Publishes the owners of one command's queues, in order, with the same + /// firmware-visible values as one `publish` per owner. The owners use + /// distinct queues and data masters. Every check precedes the first store, + /// so a refusal changes nothing; every table word is read before the first + /// store, so no uncached read waits for an earlier store to drain. + pub(crate) fn publish_owners( + &mut self, + view: &View<'_>, + owners: [Owner; N], + scheduler: u64, + policy: Policy, + clock: impl FnOnce() -> (u64, u64), + ) -> Result<[Publication; N]> { if scheduler == 0 { return Err(EINVAL); } - let next_qid = self.in_flight[qid].checked_add(1).ok_or(EOVERFLOW)?; - let next_dm = self.dm_in_flight[dm].checked_add(1).ok_or(EOVERFLOW)?; - let starts_busy = self.dm_in_flight[dm] == 0; - let time = if starts_busy { + let mut indices = [(0, 0, 0); N]; + for (index, owner) in owners.iter().enumerate() { + indices[index] = owner.indices()?; + let (qid, _, dm) = indices[index]; + if indices[..index] + .iter() + .any(|&(other_qid, _, other_dm)| other_qid == qid || other_dm == dm) + { + return Err(EINVAL); + } + self.in_flight[qid].checked_add(1).ok_or(EOVERFLOW)?; + self.dm_in_flight[dm].checked_add(1).ok_or(EOVERFLOW)?; + } + let rows = indices.map(|(qid, _, _)| view.row_word(qid)); + let queue_submitted = indices.map(|(qid, _, _)| view.queue_submitted[qid].load(Ordering::Relaxed)); + let owner_submitted = indices.map(|(_, qos, _)| view.owner_submitted[qos].load(Ordering::Relaxed)); + let dm_submitted = indices.map(|(_, _, dm)| view.times_and_counts[3 + dm].load(Ordering::Relaxed)); + let starts_busy = indices.map(|(_, _, dm)| self.dm_in_flight[dm] == 0); + let busy = starts_busy.iter().any(|&starts| starts); + let intervals = if busy { view.intervals.load(Ordering::Relaxed) } else { 0 }; + for (index, owner) in owners.iter().enumerate() { + let (qid, _, _) = indices[index]; + if self.in_flight[qid] != 0 + && (rows[index][0] != owner.qos || self.dm_by_qid[qid] != owner.data_master) + { + return Err(EBUSY); + } + } + let time = if busy { let (counter, frequency) = clock(); time_from_host(counter, frequency)? } else { 0 }; - if self.in_flight[qid] != 0 - && (view.rows[qid][0].load(Ordering::Relaxed) != owner.qos - || self.dm_by_qid[qid] != owner.data_master) - { - return Err(EBUSY); + let class = policy.qos_class(); + let mut first = false; + for (index, owner) in owners.iter().enumerate() { + let (qid, qos, _) = indices[index]; + view.rows[qid][0].store(owner.qos, Ordering::Relaxed); + view.rows[qid][1].store(class, Ordering::Relaxed); + view.scheduler[qos].store(scheduler, Ordering::Relaxed); + view.share[qos].store(policy.qos_share(), Ordering::Relaxed); + first |= self.in_flight[qid] == 0; } - view.rows[qid][0].store(owner.qos, Ordering::Relaxed); - view.rows[qid][1].store(policy.qos_class(), Ordering::Relaxed); - let record = view.row(qid); - view.scheduler[qos].store(scheduler, Ordering::Relaxed); - view.share[qos].store(policy.qos_share(), Ordering::Relaxed); - if self.in_flight[qid] == 0 { + if first { fence(Ordering::SeqCst); } - view.queue_submitted[qid].store( - view.queue_submitted[qid] - .load(Ordering::Relaxed) - .wrapping_add(1), - Ordering::Relaxed, - ); - view.owner_submitted[qos].store( - view.owner_submitted[qos] - .load(Ordering::Relaxed) - .wrapping_add(1), - Ordering::Relaxed, - ); - if starts_busy { - view.times_and_counts[dm].store(time, Ordering::Relaxed); - } - let submitted = &view.times_and_counts[3 + dm]; - let dm_submitted = submitted.load(Ordering::Relaxed).wrapping_add(1); - submitted.store(dm_submitted, Ordering::Relaxed); - if starts_busy { - view.intervals.store( - view.intervals.load(Ordering::Relaxed).wrapping_add(1), + let mut publications = [Publication { + record: [0; 8], + owner: owners[0], + dm_submitted: 0, + }; N]; + let mut intervals = intervals; + for (index, owner) in owners.iter().enumerate() { + let (qid, qos, dm) = indices[index]; + view.queue_submitted[qid].store(queue_submitted[index].wrapping_add(1), Ordering::Relaxed); + // Owners of one logical queue share their submission count. + let earlier = indices[..index].iter().filter(|&&(_, other, _)| other == qos).count(); + view.owner_submitted[qos].store( + owner_submitted[index].wrapping_add(earlier as u32 + 1), Ordering::Relaxed, ); + if starts_busy[index] { + view.times_and_counts[dm].store(time, Ordering::Relaxed); + } + let submitted = dm_submitted[index].wrapping_add(1); + view.times_and_counts[3 + dm].store(submitted, Ordering::Relaxed); + if starts_busy[index] { + intervals = intervals.wrapping_add(1); + view.intervals.store(intervals, Ordering::Relaxed); + } + let mut record = rows[index]; + record[0] = owner.qos; + record[1] = class; + self.in_flight[qid] += 1; + self.dm_in_flight[dm] += 1; + self.dm_by_qid[qid] = owner.data_master; + publications[index] = Publication { + record, + owner: *owner, + dm_submitted: submitted, + }; } - self.in_flight[qid] = next_qid; - self.dm_in_flight[dm] = next_dm; - self.dm_by_qid[qid] = owner.data_master; fence(Ordering::SeqCst); - Ok(Publication { - record, - owner, - dm_submitted, - }) + Ok(publications) } /// A possibly exposed producer must retain its reference until completion. diff --git a/drivers/gpu/drm/asahi/g17/queue/render.rs b/drivers/gpu/drm/asahi/g17/queue/render.rs index 5c201ee6ec965..e8540faa620a1 100644 --- a/drivers/gpu/drm/asahi/g17/queue/render.rs +++ b/drivers/gpu/drm/asahi/g17/queue/render.rs @@ -83,12 +83,13 @@ pub(crate) struct PublicationLease { pub(crate) trait Host { fn epoch(&self) -> Result<(u64, u32)>; fn stamps(&self, qids: [u8; 2]) -> Result<[u32; 2]>; - fn qos_publish( + /// Publishes `owners` in order with one pass over the QoS table. + fn qos_publish_pair( &mut self, - owner: qos::Owner, + owners: [qos::Owner; 2], scheduler: u64, policy: crate::g17::fw::queue::Policy, - ) -> Result; + ) -> Result<[qos::Publication; 2]>; fn qos_cancel(&mut self, publication: qos::Publication) -> Result; fn install_pair(&mut self, registration: &RenderRegistration) -> Result; fn publish_qid(&mut self, id: kick::Id) -> Result; @@ -800,10 +801,12 @@ impl Pair { tiling_kick.commit(); let mut qos_publications = [None; 2]; let result = (|| { - qos_publications[1] = - Some(host.qos_publish(qos_owners[1], context.scheduler_va(), policy)?); - qos_publications[0] = - Some(host.qos_publish(qos_owners[0], context.scheduler_va(), policy)?); + let [fragment, tiling] = host.qos_publish_pair( + [qos_owners[1], qos_owners[0]], + context.scheduler_va(), + policy, + )?; + qos_publications = [Some(tiling), Some(fragment)]; // A class change happens only at a rebind, after the pair drained. if self.registered != Some(policy.priority()) { host.install_pair(®istration)?; diff --git a/drivers/gpu/drm/asahi/g17/runtime/render.rs b/drivers/gpu/drm/asahi/g17/runtime/render.rs index 4aaad40eee765..b5649f059f5d2 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render.rs @@ -531,13 +531,13 @@ impl crate::g17::queue::render::Host for Host<'_> { core::sync::atomic::fence(Ordering::Acquire); Ok(stamps) } - fn qos_publish( + fn qos_publish_pair( &mut self, - owner: crate::g17::qos::Owner, + owners: [crate::g17::qos::Owner; 2], scheduler: u64, policy: crate::g17::fw::queue::Policy, - ) -> Result { - self.firmware.port.qos_publish(owner, scheduler, policy) + ) -> Result<[crate::g17::qos::Publication; 2]> { + self.firmware.port.qos_publish_pair(owners, scheduler, policy) } fn qos_cancel(&mut self, publication: crate::g17::qos::Publication) -> Result { self.firmware.port.qos_cancel(publication) diff --git a/drivers/gpu/drm/asahi/g17/runtime/render/direct.rs b/drivers/gpu/drm/asahi/g17/runtime/render/direct.rs index cd7bad6ecce5d..dd3a9c881a142 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render/direct.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render/direct.rs @@ -53,13 +53,13 @@ impl crate::g17::queue::render::Host for DirectHost<'_> { fn stamps(&self, qids: [u8; 2]) -> Result<[u32; 2]> { Ok([self.port.stamp(qids[0])?, self.port.stamp(qids[1])?]) } - fn qos_publish( + fn qos_publish_pair( &mut self, - owner: crate::g17::qos::Owner, + owners: [crate::g17::qos::Owner; 2], scheduler: u64, policy: crate::g17::fw::queue::Policy, - ) -> Result { - self.port.qos_publish(owner, scheduler, policy) + ) -> Result<[crate::g17::qos::Publication; 2]> { + self.port.qos_publish_pair(owners, scheduler, policy) } fn qos_cancel(&mut self, publication: crate::g17::qos::Publication) -> Result { self.port.qos_cancel(publication) From b86c445ba2829f644075ca663217abe65844e8c0 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 25 Oct 2026 05:46:59 -0400 Subject: [PATCH 051/388] drm/asahi: stop copying G17 fragment registers to the unused client page Every render preparation zeroed the pair's client register page (one uncached 16 KiB page) and copied the fragment descriptor's register arrays into it. Nothing reads that page: kick entries bind the register arrays embedded in the descriptor (register_bindings), and the page is kept only for its allocation position. Keep the array bounds check and drop the copy. Same-boot phase timers, render.light d1 pinned to cpu5: 426 -> 18 ns per submit. --- .../gpu/drm/asahi/g17/queue/render/memory.rs | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/queue/render/memory.rs b/drivers/gpu/drm/asahi/g17/queue/render/memory.rs index 80ee0b871cc82..b14ccbf879b5f 100644 --- a/drivers/gpu/drm/asahi/g17/queue/render/memory.rs +++ b/drivers/gpu/drm/asahi/g17/queue/render/memory.rs @@ -571,21 +571,10 @@ impl Memory { (&mut *tiling).write(args)?; let fragment = &mut *fragment; fragment.write(args)?; - self.registers.bytes_mut().fill(0); - for (index, array) in fragment.descriptor.arrays.iter().enumerate() { - for (entry, value) in array - .entries - .get(..(array.size & 0xffff) as usize) - .ok_or(EINVAL)? - .iter() - .enumerate() - { - self.registers.write( - index * size_of::() - + entry * size_of::(), - *value, - )?; - } + // Kick entries bind the arrays embedded in this descriptor + // (register_bindings); the client register page is never read. + for array in &fragment.descriptor.arrays { + array.entries.get(..(array.size & 0xffff) as usize).ok_or(EINVAL)?; } fragment.set_mcache( args.fragment.descriptor_va, From 5a55068193e213d7115a63f2222ffc304a4ced98 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 25 Oct 2026 05:46:59 -0400 Subject: [PATCH 052/388] drm/asahi: read the G17 render work rings once per direct publication The direct render path read both render work rings' cursors (uncached) for the room check, again to write the pair's records and again to expose them, all under the render publication lock. No other host producer moves while that lock is held and the firmware consumers only advance, so the first reading stays valid. Return it from the room check and use its producers for the write and the exposure. Same-boot phase timers, render.light d1 pinned to cpu5: record write and exposure 435 + 161 -> 410 + 113 ns per submit. --- drivers/gpu/drm/asahi/g17/port.rs | 75 +++++++++++++------ .../drm/asahi/g17/runtime/render/direct.rs | 17 ++++- 2 files changed, 64 insertions(+), 28 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/port.rs b/drivers/gpu/drm/asahi/g17/port.rs index 45ddae9719363..8d129c49c640c 100644 --- a/drivers/gpu/drm/asahi/g17/port.rs +++ b/drivers/gpu/drm/asahi/g17/port.rs @@ -103,6 +103,22 @@ pub(super) struct Port { submissions: AtomicU64, } +/// Room in both render work rings of one priority, observed under the render publication +/// lock. While that lock is held no other host producer moves, and the firmware consumers only +/// advance, so the observed producers stay current and the room stays available. +#[derive(Clone, Copy)] +pub(super) struct RenderRoom { + priority: u8, + /// Fragment, then tiling producer. + producers: [u32; 2], +} + +impl RenderRoom { + pub(super) fn priority(&self) -> u8 { + self.priority + } +} + /// A compute work-ring slot reserved before any command becomes visible. Dropping an unused /// claim returns the slot. pub(super) struct OuterClaim<'a> { @@ -225,20 +241,33 @@ impl Port { )) } - /// Both render work rings of `priority` have a free slot. Only firmware consumption, - /// never a publisher without the render lock, changes this. - pub(super) fn render_room(&self, _guard: &MutexGuard<'_, ()>, priority: u8) -> Result { + /// Reads both render work rings of `priority` once. Returns their producers if both have a + /// free slot; the room holds for as long as `guard`. Only firmware consumption, never a + /// publisher without the render lock, changes it. + pub(super) fn observe_render_room( + &self, + _guard: &MutexGuard<'_, ()>, + priority: u8, + ) -> Result> { let (fragment, tiling) = self.render_rings(priority)?; - Ok(fragment.free()? > 0 && tiling.free()? > 0) + let cursors = [fragment.cursors()?, tiling.cursors()?]; + let room = |[first, second, producer]: [u32; 3]| { + let room = |consumer: u32| (consumer + WORK_RING_SLOTS - producer - 1) % WORK_RING_SLOTS; + room(first).min(room(second)) + }; + Ok((room(cursors[0]) > 0 && room(cursors[1]) > 0).then_some(RenderRoom { + priority, + producers: [cursors[0][2], cursors[1][2]], + })) } - /// Copies a render pair's fragment, then tiling record into the slots after each - /// producer without exposing them, as `InitData::publish_work_pair` does for a deferred - /// pair. Returns the producers that expose them. + /// Copies a render pair's fragment, then tiling record into the slots after the producers + /// that `room` observed, without exposing them, as `InitData::publish_work_pair` does for a + /// deferred pair. Returns the producers that expose them. pub(super) fn write_render_pair( &self, _guard: &MutexGuard<'_, ()>, - priority: u8, + room: &RenderRoom, fragment: &WorkSlot, tiling: &WorkSlot, commit_accounting: impl FnOnce() -> Result, @@ -249,21 +278,20 @@ impl Port { { return Err(EINVAL); } - let (fragment_ring, tiling_ring) = self.render_rings(priority)?; - let slot = |ring: &RingView| -> Result<(*mut u8, u32)> { - let [first, second, producer] = ring.cursors()?; - let next = (producer + 1) % WORK_RING_SLOTS; - if next == first || next == second { + let (fragment_ring, tiling_ring) = self.render_rings(room.priority)?; + let slot = |ring: &RingView, producer: u32| -> Result<(*mut u8, u32)> { + if producer >= WORK_RING_SLOTS { return Err(EINVAL); } // SAFETY: The ring view spans WORK_RING_SLOTS records; `producer` is in range. - Ok((unsafe { ring.slots.add(producer as usize * core::mem::size_of::()) }, next)) + let slot = unsafe { ring.slots.add(producer as usize * core::mem::size_of::()) }; + Ok((slot, (producer + 1) % WORK_RING_SLOTS)) }; - let (fragment_slot, fragment_next) = slot(&fragment_ring)?; - let (tiling_slot, tiling_next) = slot(&tiling_ring)?; + let (fragment_slot, fragment_next) = slot(&fragment_ring, room.producers[0])?; + let (tiling_slot, tiling_next) = slot(&tiling_ring, room.producers[1])?; let copy = |record: &WorkSlot, destination: *mut u8| { - // SAFETY: The slot after the producer is host-owned until the producer moves past - // it, and the render lock excludes every other host writer of this ring. + // SAFETY: The room proves that the slot after the producer is host-owned until the + // producer moves past it, and the render lock excludes every other host writer. unsafe { core::ptr::copy_nonoverlapping( core::ptr::from_ref(record).cast::(), @@ -281,17 +309,16 @@ impl Port { Ok([fragment_next, tiling_next]) } - /// Exposes a pair written by `write_render_pair`, fragment first, as - /// `InitData::publish_retained_work_pair` does: the producers must still precede `next`. + /// Exposes a pair written by `write_render_pair` with `room`, fragment first, as + /// `InitData::publish_retained_work_pair` does: `next` must follow the observed producers. pub(super) fn expose_render_pair( &self, _guard: &MutexGuard<'_, ()>, - priority: u8, + room: &RenderRoom, next: [u32; 2], ) -> Result { - let (fragment_ring, tiling_ring) = self.render_rings(priority)?; - for (ring, next) in [(&fragment_ring, next[0]), (&tiling_ring, next[1])] { - let [_, _, producer] = ring.cursors()?; + let (fragment_ring, tiling_ring) = self.render_rings(room.priority)?; + for (producer, next) in room.producers.into_iter().zip(next) { if next != (producer + 1) % WORK_RING_SLOTS { return Err(EBUSY); } diff --git a/drivers/gpu/drm/asahi/g17/runtime/render/direct.rs b/drivers/gpu/drm/asahi/g17/runtime/render/direct.rs index dd3a9c881a142..1f42d1e6f404f 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render/direct.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render/direct.rs @@ -14,7 +14,7 @@ use super::super::{Backend, DeferredBatch}; use crate::g17::{ job::Packet, object::Allocator, - port::Port, + port::{Port, RenderRoom}, queue::render::{Pair, PublicationLease, RenderAddresses}, }; use core::sync::atomic::Ordering; @@ -39,6 +39,7 @@ pub(in crate::g17::runtime) struct Loan { struct DirectHost<'a> { port: &'a Port, render: &'a MutexGuard<'a, ()>, + room: RenderRoom, generation: u64, activation: Option<(u16, u64)>, } @@ -79,8 +80,11 @@ impl crate::g17::queue::render::Host for DirectHost<'_> { commit: impl FnOnce() -> Result, late_tiling: impl FnOnce(), ) -> Result<[u32; 2]> { + if priority != self.room.priority() { + return Err(EINVAL); + } self.port - .write_render_pair(self.render, priority, fragment, tiling, commit, late_tiling) + .write_render_pair(self.render, &self.room, fragment, tiling, commit, late_tiling) } fn activate_render_pool(&mut self, id: u16, generation: u64) -> Result { // The lease leaves preparation when the pair is returned; nothing can @@ -95,7 +99,10 @@ impl crate::g17::queue::render::Host for DirectHost<'_> { _tiling: &crate::g17::fw::channels::WorkSlot, next: [u32; 2], ) -> Result { - self.port.expose_render_pair(self.render, priority, next) + if priority != self.room.priority() { + return Err(EINVAL); + } + self.port.expose_render_pair(self.render, &self.room, next) } /// Pipe 0 (tiling) in bits 1:0, priority class in bits 3:2. fn notify_render(&mut self, priority: u8) -> Result { @@ -302,9 +309,10 @@ impl Backend { }; pair.prepare(&alloc, packet, loan.addresses)?; let render = port.render_lock(); - if !lease.is_current() || !port.render_room(&render, priority)? { + if !lease.is_current() { return Err(EBUSY); } + let room = port.observe_render_room(&render, priority)?.ok_or(EBUSY)?; let mut dependencies = pair.own_dependencies(self.owner, packet)?; if let Some(footprint) = packet.footprint.as_ref() { dependencies.hazard_count = self.admission.publication_barriers( @@ -317,6 +325,7 @@ impl Backend { let mut host = DirectHost { port, render: &render, + room, generation, activation: None, }; From b67018ed5974069c13c13249834dbc3b84e806ed Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 25 Oct 2026 05:46:59 -0400 Subject: [PATCH 053/388] drm/asahi: request the G17 tiling descriptor slot before preparing a render Clearing a render's tiling descriptor slot took about 480 ns for 2.5 KiB while clearing the 8.7 KiB fragment slot took 80 ns: the slot was last used by the firmware for an earlier command, and every line had to be fetched for writing one at a time. Issue store prefetches for the slot at the start of preparation, so the transfers overlap the target, scene and generation preparation that precedes the descriptor write. Same-boot phase timers, render.light d1 pinned to cpu5: tiling descriptor clear 483 -> 52 ns, ioctl entry to doorbell 12.04 -> 11.39 us. Same-boot A/B of this series (exp_mask diagnostic restoring each old path, 3 interleaved rounds, gpubench --runs 5 --run-ms 250, us/submit): render.light d1/d2/d32 cpu5 126.5/48.5/31.7 -> 106.1/46.7/30.8, cpu0 104.7/46.6/44.7 -> 103.6/46.0/37.5 (d32 cpu0 noisy); render ioctl entry to doorbell 14.4 -> 12.1 us (phase timers). compute.light, render queues d2/4/8, render procs d2/16, copy.bw buf2img d1/4 and sync.signal hop unchanged within noise. --- drivers/gpu/drm/asahi/g17/queue/render.rs | 1 + .../gpu/drm/asahi/g17/queue/render/memory.rs | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/drivers/gpu/drm/asahi/g17/queue/render.rs b/drivers/gpu/drm/asahi/g17/queue/render.rs index e8540faa620a1..b9f179495ab99 100644 --- a/drivers/gpu/drm/asahi/g17/queue/render.rs +++ b/drivers/gpu/drm/asahi/g17/queue/render.rs @@ -374,6 +374,7 @@ impl Pair { } let ordinal = self.ordinal; ordinal.checked_add(1).ok_or(EOVERFLOW)?; + self.memory.prefetch_descriptors(ordinal); let work = packet.completion.work_state()?; let node = work.node_va()?; let word = work.word(); diff --git a/drivers/gpu/drm/asahi/g17/queue/render/memory.rs b/drivers/gpu/drm/asahi/g17/queue/render/memory.rs index b14ccbf879b5f..e7d835e15eb47 100644 --- a/drivers/gpu/drm/asahi/g17/queue/render/memory.rs +++ b/drivers/gpu/drm/asahi/g17/queue/render/memory.rs @@ -537,6 +537,26 @@ const _: () = { }; impl Memory { + /// Requests the tiling descriptor slot of `ordinal` for writing. The firmware accessed + /// that slot for the command that used it last, so the host no longer holds its lines; + /// requesting them before the rest of the preparation overlaps those transfers with it + /// instead of stalling the descriptor write one line at a time. + pub(super) fn prefetch_descriptors(&self, ordinal: u64) { + const LINE: usize = 64; + let size = size_of::(); + let Ok(base) = self.graph.descriptors.pointer(Graph::offset(0, ordinal), size) else { + return; + }; + for offset in (0..size).step_by(LINE) { + // SAFETY: A prefetch hint inside the retained mapping; it has no + // architectural effect on memory. + unsafe { + core::arch::asm!("prfm pstl1keep, [{0}]", in(reg) base.add(offset), + options(nostack, preserves_flags, readonly)); + } + } + } + pub(super) fn write_descriptors( &mut self, args: &abi::RenderArgs<'_>, From 4d0362d0c18d288d3bd520fdfb69c2e7624cf6b0 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 25 Oct 2026 06:04:01 -0400 Subject: [PATCH 054/388] drm/asahi: store G17 register arrays without an intermediate copy Without scratch storage a render's first tiling and fragment register arrays drop their SCS-enable entry. Each was copied into a trimmed array on the stack, initialised entry by entry, and then copied again into the descriptor. Store the remaining entries straight into the descriptor's array instead; the stored entries, size and address are unchanged. Same-boot phase timers, render.light d1 pinned to cpu5: tiling register store 310 -> 45 ns, fragment array 0 store 245 -> 69 ns, ioctl entry to doorbell 11.15 -> 10.77 us. --- drivers/gpu/drm/asahi/g17/fw/kick.rs | 14 ++++++++++++++ drivers/gpu/drm/asahi/g17/fw/render.rs | 9 ++------- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/fw/kick.rs b/drivers/gpu/drm/asahi/g17/fw/kick.rs index 4f3a70671eb87..ca2b6604a302b 100644 --- a/drivers/gpu/drm/asahi/g17/fw/kick.rs +++ b/drivers/gpu/drm/asahi/g17/fw/kick.rs @@ -278,6 +278,20 @@ impl RegisterArray { self.size = (size_of_val(writes) as u32) << 16 | count as u32; Ok(()) } + + /// Stores `writes` without the entry at `skip`, exactly as [`Self::set`] stores the + /// remaining writes, without first copying them elsewhere. + pub(crate) fn set_without(&mut self, self_va: u64, writes: &[RegisterWrite], skip: usize) -> Result { + let count = writes.len().checked_sub(1).ok_or(EINVAL)?; + if skip > count || count > REGISTER_ARRAY_ENTRIES { + return Err(EINVAL); + } + self.entries[..skip].copy_from_slice(&writes[..skip]); + self.entries[skip..count].copy_from_slice(&writes[skip + 1..]); + self.self_va = self_va; + self.size = ((count * size_of::()) as u32) << 16 | count as u32; + Ok(()) + } } /// Binding of one [`RegisterArray`] to a kick entry. diff --git a/drivers/gpu/drm/asahi/g17/fw/render.rs b/drivers/gpu/drm/asahi/g17/fw/render.rs index 0e270d40392a8..aa8693bf7140b 100644 --- a/drivers/gpu/drm/asahi/g17/fw/render.rs +++ b/drivers/gpu/drm/asahi/g17/fw/render.rs @@ -1110,9 +1110,7 @@ impl TaDescriptor { } else if scs_register { self.registers.set(array_va, ®isters)?; } else { - let mut trimmed = [registers[0]; TA_REGISTERS]; - trimmed[TA_SCS_ENABLE_INDEX..].copy_from_slice(®isters[TA_SCS_ENABLE_INDEX + 1..]); - self.registers.set(array_va, &trimmed)?; + self.registers.set_without(array_va, ®isters, TA_SCS_ENABLE_INDEX)?; } self.tpc_va = resources.tpc_va; self.tpc_size = resources.tpc_size; @@ -1456,10 +1454,7 @@ impl FragmentSlot { } else if scs_register { desc.arrays[0].set(arrays[0], ®isters)?; } else { - let mut trimmed = [registers[0]; FRAGMENT_REGISTERS]; - trimmed[FRAGMENT_SCS_ENABLE_INDEX..] - .copy_from_slice(®isters[FRAGMENT_SCS_ENABLE_INDEX + 1..]); - desc.arrays[0].set(arrays[0], &trimmed)?; + desc.arrays[0].set_without(arrays[0], ®isters, FRAGMENT_SCS_ENABLE_INDEX)?; } desc.arrays[1].set(arrays[1], &partial_store_registers(pass, &words))?; desc.arrays[2].set(arrays[2], &partial_resume_registers(pass, &words))?; From 861b39d948f6c0588b3646e1246e660b52230e5b Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 25 Oct 2026 06:04:01 -0400 Subject: [PATCH 055/388] drm/asahi: also request the G17 queue configuration records early A render's two queue configuration records are rewritten for every command that carries one, in records the firmware accessed for an earlier command, the same pattern as the tiling descriptor slot. Request them for writing together with that slot at the start of preparation. Same-boot phase timers, render.light d1 pinned to cpu5: fragment prefix and kick 219 -> 176 ns, record write 328 -> 271 ns, ioctl entry to doorbell 10.77 -> 10.55 us. --- .../gpu/drm/asahi/g17/queue/render/memory.rs | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/queue/render/memory.rs b/drivers/gpu/drm/asahi/g17/queue/render/memory.rs index e7d835e15eb47..756f96e3fd048 100644 --- a/drivers/gpu/drm/asahi/g17/queue/render/memory.rs +++ b/drivers/gpu/drm/asahi/g17/queue/render/memory.rs @@ -537,22 +537,30 @@ const _: () = { }; impl Memory { - /// Requests the tiling descriptor slot of `ordinal` for writing. The firmware accessed - /// that slot for the command that used it last, so the host no longer holds its lines; - /// requesting them before the rest of the preparation overlaps those transfers with it - /// instead of stalling the descriptor write one line at a time. + /// Requests the tiling descriptor slot and both queue configuration records of + /// `ordinal` for writing. The firmware accessed them for the command that used them + /// last, so the host no longer holds their lines; requesting them before the rest of + /// the preparation overlaps those transfers with it instead of stalling the writes one + /// line at a time. pub(super) fn prefetch_descriptors(&self, ordinal: u64) { - const LINE: usize = 64; - let size = size_of::(); - let Ok(base) = self.graph.descriptors.pointer(Graph::offset(0, ordinal), size) else { - return; - }; - for offset in (0..size).step_by(LINE) { - // SAFETY: A prefetch hint inside the retained mapping; it has no - // architectural effect on memory. - unsafe { - core::arch::asm!("prfm pstl1keep, [{0}]", in(reg) base.add(offset), - options(nostack, preserves_flags, readonly)); + let record = ordinal as usize % RECORDS; + let config = size_of::(); + let ranges = [ + (&self.graph.descriptors, Graph::offset(0, ordinal), size_of::()), + (&self.graph.config, CONFIG[0] + record * CONFIG_STRIDE, config), + (&self.graph.config, CONFIG[1] + record * CONFIG_STRIDE, config), + ]; + for (object, offset, size) in ranges { + let Ok(base) = object.pointer(offset, size) else { + continue; + }; + for line in (0..size).step_by(64) { + // SAFETY: A prefetch hint inside the retained mapping; it has no + // architectural effect on memory. + unsafe { + core::arch::asm!("prfm pstl1keep, [{0}]", in(reg) base.add(line), + options(nostack, preserves_flags, readonly)); + } } } } From 66ef051d8bd4e66bdc145eb7fb7a920235a8e668 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 25 Oct 2026 06:04:01 -0400 Subject: [PATCH 056/388] drm/asahi: scan only render pool slots for G17 free-list leases Render free-list leases live only in slots from FIRST_RENDER_POOL up; Pools::slot() refuses every lower index. The release observation each render preparation performs, the release publication and the linger expiry still walked all 256 entries. Walk the render range only. Phase timers, render.light d1 pinned to cpu5 (two boots): lease observation 346 -> 213 ns per submit. --- drivers/gpu/drm/asahi/g17/runtime/render/pools.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/runtime/render/pools.rs b/drivers/gpu/drm/asahi/g17/runtime/render/pools.rs index 78e5e3a219358..1e3354d90dbfb 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render/pools.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render/pools.rs @@ -179,7 +179,7 @@ impl Pools { ) -> (bool, Option) { let mut ended = false; let mut next: Option = None; - for lease in self.leases.iter_mut().flatten() { + for lease in self.leases[FIRST_RENDER_POOL..].iter_mut().flatten() { let Some(since) = lease.lingering() else { continue; }; @@ -212,7 +212,7 @@ impl Pools { fence(Ordering::Acquire); let consumer = init.control_consumer()?.load(Ordering::Relaxed); let producer = init.control_producer()?.load(Ordering::Relaxed); - for lease in self.leases.iter_mut().flatten() { + for lease in self.leases[FIRST_RENDER_POOL..].iter_mut().flatten() { lease.observe_releases(consumer, producer)?; } Ok(()) @@ -225,7 +225,7 @@ impl Pools { init: &InitData, mut announce: impl FnMut() -> Result, ) -> Result { - for lease in self.leases.iter_mut().flatten() { + for lease in self.leases[FIRST_RENDER_POOL..].iter_mut().flatten() { let Some(release) = lease.pending_release() else { continue; }; From 1aec5be232da7bacbbe38b7f9b22c27906eac8c6 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 25 Oct 2026 07:49:37 -0400 Subject: [PATCH 057/388] drm/asahi: omit G17 render queue configurations that only reload the held completion A render published onto a pair whose queues have drained seeds each queue's completion value, and the seed forces a queue configuration record onto both item rings. When the previous publication of the pair in the same recovery epoch was the last kick of each queue, the queue already holds exactly the value the seed would load, so the record carries nothing new. Keep the completion values of each successful publication with the configuration cache and publish the record only when its identity changed or the seed differs from the held value. Recovery, replay and graph rebuilds drop the kept values, so those publications still seed. The firmware processes each record before starting the kick. On Neo it shortened the kick of a render on a drained pair by about 1.5 us per record (start-to-start 13.5 -> 10.7 us in render.light queues d4). Same-boot A/B, alternated per row, 3 rounds, medians in us per submit: render.light queues d2 54.70 -> 51.88 (-5.2%) render.light queues d4 45.25 -> 42.15 (-6.8%) render.light queues d8 41.65 -> 38.79 (-6.9%) render.light procs d2 54.68 -> 51.84 (-5.2%) render.light procs d16 40.84 -> 37.63 (-7.9%) Deep single-queue rows publish onto busy pairs, which never seed, and are unchanged. --- drivers/gpu/drm/asahi/g17/queue/render.rs | 31 ++++++++++++++++--- .../gpu/drm/asahi/g17/queue/render/parked.rs | 1 + 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/queue/render.rs b/drivers/gpu/drm/asahi/g17/queue/render.rs index b9f179495ab99..9469d526f49c8 100644 --- a/drivers/gpu/drm/asahi/g17/queue/render.rs +++ b/drivers/gpu/drm/asahi/g17/queue/render.rs @@ -192,6 +192,9 @@ pub(crate) struct Pair { /// An owner change in flight: the previous scheduler page and the ordinal of /// the new owner's first command, until that command leaves the pair. switch: Option<(u64, u64)>, + /// Completion values of the two kicks of the last publication, kept with the configuration + /// cache: what each queue holds once that publication completed. + published_completions: Option<[u64; 2]>, } impl Pair { #[inline(never)] @@ -290,6 +293,7 @@ impl Pair { context_update: true, executed: 0, switch: None, + published_completions: None, })) } pub(crate) fn qids(&self) -> [u8; 2] { @@ -649,10 +653,26 @@ impl Pair { } else { [None; 2] }; - let present = [ - publication::required(cache[0], configs[0]), - publication::required(cache[1], configs[1]), - ]; + // A covered queue holds the completion value of its newest kick. When that kick is the + // previous publication of this pair in this recovery epoch, the seed would reload the + // value already held, so the configuration record is needed only for its identity. + // Each record costs the firmware time before it starts the kick. + let held = self + .published_completions + .take() + .filter(|_| self.recovery_generation == epoch); + let present = [0, 1].map(|stage| { + let mut config = configs[stage]; + let reload = kicks[stage].prev().completion_value(payload.wrapping_sub(1)); + if config.completion_seed_valid != 0 + && self.installed[stage] + && held.is_some_and(|held| held[stage] == reload) + { + config.completion_seed_valid = 0; + config.completion_seed = 0; + } + publication::required(cache[stage], config) + }); let cursors = self.memory.graph.cursors()?; if cursors[0].write != self.cursors[0] || cursors[1].write != self.cursors[1] { return Err(EBUSY); @@ -865,6 +885,7 @@ impl Pair { } self.recovery_generation = epoch; self.installed = [true; 2]; + self.published_completions = Some(kicks.map(|kick| kick.completion_value(payload))); self.outer_started |= ring; if let Some(previous) = self.previous.as_mut().filter(|p| p.publication.is_none()) { previous.publication = Some(ordinal); @@ -1142,6 +1163,7 @@ impl Pair { pub(crate) fn resync(&mut self) { self.configs = [None; 2]; + self.published_completions = None; for kick in &mut self.kicks { kick.clear_parent_after_recovery(); } @@ -1417,6 +1439,7 @@ impl Pair { self.memory.retire(ordinal)?; self.active.pop().ok_or(EIO)?; packet.reset_publication_for_replay(); + self.published_completions = None; self.replay.push(packet).map_err(|_| EIO)?; } Ok(true) diff --git a/drivers/gpu/drm/asahi/g17/queue/render/parked.rs b/drivers/gpu/drm/asahi/g17/queue/render/parked.rs index 209ce0b8176ec..81d4306eda28c 100644 --- a/drivers/gpu/drm/asahi/g17/queue/render/parked.rs +++ b/drivers/gpu/drm/asahi/g17/queue/render/parked.rs @@ -252,6 +252,7 @@ impl ParkedPair { context_update: true, executed: self.executed, switch: None, + published_completions: None, })) } } From d0df244b8f023037bb0f9cc9793f1d0f285cfc87 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 25 Oct 2026 08:31:47 -0400 Subject: [PATCH 058/388] drm/asahi: keep one CPU mapping per G17 timestamp buffer Every timestamped command mapped the destination buffer into the kernel for each timestamp it carried: owned_vmap() takes the object's reservation lock to count a mapping user. Because dropping that mapping takes the lock too, which fence producers must not wait for, each command also allocated a cleanup work item and queued it when its completion was released, waking a worker per command. Map a timestamp buffer once, when it is bound, and keep that mapping for the buffer's lifetime. A command's destination keeps a reference to the buffer and an offset, so releasing it takes no lock. The buffer's own last reference can still be a command's, released where fence producers run, so the buffer defers the release of its mappings to the cleanup queue; that happens once per buffer instead of once per command. The mapping is made outside the device mutex. Destination validation and the write itself are unchanged; G16's CPU-retired buffers keep their per-command mapping. Same-boot phase timers (exp_mask diagnostic restoring the old path), render.light d1 pinned to cpu5: timestamp destination resolve 410 -> 132 ns, completion teardown at the end of the ioctl 578 -> 153 ns, ioctl 16.9 -> 16.2 us with the timers; compute.light d1 resolve 367 -> 152 ns; query kind=occlusion d1 resolve 337 -> 141 ns, teardown 636 -> 162 ns. Same-boot ABBA, 4 rounds (query occlusion d1/d16, query timestamp d1/d16, render.light d1/d2/d32, compute.light d1/d2/d64, buf2img d1, sync.signal hop, idle.wake), pinned to cpu5: unchanged within noise. Regression gate passes; deqp-vk query_pool.*, synchronization2 basic and timeline_semaphore, api.command_buffers and renderpass subsets: 22847 pass, 791 not supported, 0 fail. --- drivers/gpu/drm/asahi/g17/completion.rs | 36 ++++------ drivers/gpu/drm/asahi/g17/mod.rs | 28 ++++---- drivers/gpu/drm/asahi/mmu.rs | 93 ++++++++++++++++++------- drivers/gpu/drm/asahi/timestamp.rs | 44 ++++++++++-- 4 files changed, 135 insertions(+), 66 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/completion.rs b/drivers/gpu/drm/asahi/g17/completion.rs index 11b4c323d4668..02e7bbc5c33dc 100644 --- a/drivers/gpu/drm/asahi/g17/completion.rs +++ b/drivers/gpu/drm/asahi/g17/completion.rs @@ -27,9 +27,9 @@ mod publication; pub(crate) use publication::{Prepared as PreparedPublication, Publication}; struct Destination { - // Keep the existing GPU alias alive along with its prepared CPU view. - _mapping: Arc, - cpu: mmu::TimestampMapping, + // The buffer keeps its GPU alias and its CPU mapping alive. + buffer: Arc, + offset: usize, } impl Destination { @@ -45,13 +45,13 @@ impl Destination { .get(timestamp.handle.try_into()?) .ok_or(ENOENT)? .clone(); - let file::Object::TimestampBuffer(mapping) = object; + let file::Object::TimestampBuffer(buffer) = object; let offset: usize = timestamp.offset.try_into()?; - if offset.checked_add(size_of::()).ok_or(EINVAL)? > mapping.size() { + if offset.checked_add(size_of::()).ok_or(EINVAL)? > buffer.size() { return Err(ERANGE); } - let cpu = mapping.timestamp_mapping(offset)?; - Ok(Some(Self { _mapping: mapping, cpu })) + let offset = buffer.timestamp_offset(offset)?; + Ok(Some(Self { buffer, offset })) } fn write(&self, ticks: u64) { @@ -61,13 +61,14 @@ impl Destination { .wrapping_add( ticks % cfg::COMMAND_TIMESTAMP_HZ * NSEC_PER_SEC / cfg::COMMAND_TIMESTAMP_HZ, ); - self.cpu.write(ns); + self.buffer.write_timestamp(self.offset, ns); barrier(Ordering::Release); } } -/// Timestamp mappings resolved and retained before any command is enqueued. -pub(crate) struct Destinations(Option; 4]>>); +/// Timestamp destinations resolved and retained before any command is enqueued. Dropping +/// one releases only a buffer reference; the buffer defers its own mapping release. +pub(crate) struct Destinations([Option; 4]); impl Destinations { pub(crate) fn resolve( @@ -82,22 +83,13 @@ impl Destinations { destinations[2 * index] = Destination::resolve(objects, timestamp.start)?; destinations[2 * index + 1] = Destination::resolve(objects, timestamp.end)?; } - // Empty submissions need no cleanup allocation. Timestamped commands - // prepare the unique work item before any output fence is published. - let retained = if destinations.iter().any(Option::is_some) { - Some(crate::cleanup::Deferred::new(destinations)?) - } else { - None - }; - Ok(Self(retained)) + Ok(Self(destinations)) } - pub(crate) fn has_outputs(&self) -> bool { self.0.is_some() } + pub(crate) fn has_outputs(&self) -> bool { self.0.iter().any(Option::is_some) } fn iter(&self) -> core::slice::Iter<'_, Option> { - let destinations: &[Option] = - self.0.as_deref().map_or(&[], |destinations| destinations.as_slice()); - destinations.iter() + self.0.iter() } } diff --git a/drivers/gpu/drm/asahi/g17/mod.rs b/drivers/gpu/drm/asahi/g17/mod.rs index 8c6891785f1c2..c81760ad4646b 100644 --- a/drivers/gpu/drm/asahi/g17/mod.rs +++ b/drivers/gpu/drm/asahi/g17/mod.rs @@ -1639,18 +1639,22 @@ impl gpu::Gpu for Gpu { mut bo: gem::ObjectRef, range: Range, ) -> Result { - let state = self.shared.state.lock(); - let firmware = (*state).as_ref().ok_or(ENODEV)?; - let mut addresses = firmware.uat.geometry().kernel_range(); - addresses.start += hw::t8140::dynamic::KERNEL_OFFSET; - crate::timestamp::Buffer::firmware(bo.map_range_into_range( - firmware.uat.kernel_vm(), - range, - addresses, - mmu::UAT_PGSZ as u64, - mmu::PROT_FW_SHARED_RW, - false, - )?) + let mapping = { + let state = self.shared.state.lock(); + let firmware = (*state).as_ref().ok_or(ENODEV)?; + let mut addresses = firmware.uat.geometry().kernel_range(); + addresses.start += hw::t8140::dynamic::KERNEL_OFFSET; + bo.map_range_into_range( + firmware.uat.kernel_vm(), + range, + addresses, + mmu::UAT_PGSZ as u64, + mmu::PROT_FW_SHARED_RW, + false, + )? + }; + // The buffer's CPU mapping takes the reservation lock: not under the device mutex. + crate::timestamp::Buffer::firmware(mapping) } } diff --git a/drivers/gpu/drm/asahi/mmu.rs b/drivers/gpu/drm/asahi/mmu.rs index 6a80d1b484b34..f5b079e9b5382 100644 --- a/drivers/gpu/drm/asahi/mmu.rs +++ b/drivers/gpu/drm/asahi/mmu.rs @@ -1201,56 +1201,83 @@ pub(crate) struct KernelMappingInner { mapped_size: usize, } -/// Prepared CPU timestamp access; destruction happens after completion signalling. -pub(crate) struct TimestampMapping { - mapping: kernel::drm::gem::shmem::VMapOwned, - offset: usize, -} +/// A kernel CPU mapping of a timestamp buffer's GEM backing. Dropping it takes the object's +/// reservation lock, so it must not be dropped where fence producers run. +pub(crate) struct TimestampVmap(kernel::drm::gem::shmem::VMapOwned); -impl TimestampMapping { - /// Prepare one bounded CPU timestamp destination on retained GEM backing. - pub(crate) fn new(gem: &gem::Object, offset: usize) -> Result { +impl TimestampVmap { + pub(crate) fn new(gem: &gem::Object) -> Result { + Ok(Self(gem.owned_vmap::<0>()?)) + } + + fn size(&self) -> usize { use kernel::drm::gem::BaseObject; - if offset.checked_add(size_of::()).ok_or(EOVERFLOW)? > gem.size() { - return Err(ERANGE); - } - Ok(Self { - mapping: gem.owned_vmap::<0>()?, - offset, - }) + self.0.owner().size() } - pub(crate) fn write(&self, value: u64) { + /// Writes a timestamp at byte `offset` of the GEM object. Aligned results use a complete + /// atomic store; byte stores support unaligned destinations without creating mutable + /// aliases. An unaligned complete timestamp is not atomic. Out-of-range offsets are refused + /// when the destination is prepared, and ignored here. + pub(crate) fn write(&self, offset: usize, value: u64) { + if offset.checked_add(size_of::()).is_none_or(|end| end > self.size()) { + return; + } // Completion runs in task context. Bindings of the same BO share this lock, // so aligned u64 and overlapping unaligned byte stores never race in Rust. - let _guard = self.mapping.owner().timestamp_lock.lock(); - // SAFETY: timestamp_mapping checked the complete extent inside this pinned vmap. - let base = self.mapping.addr.cast::(); - let pointer = unsafe { base.add(self.offset) }; + let _guard = self.0.owner().timestamp_lock.lock(); + // SAFETY: The complete extent was checked above, inside this pinned vmap. + let base = self.0.addr.cast::(); + let pointer = unsafe { base.add(offset) }; if pointer as usize % core::mem::align_of::() == 0 { - // SAFETY: timestamp_mapping checked all eight bytes; the vmap pins the backing. + // SAFETY: All eight bytes were checked above; the vmap pins the backing. // A naturally aligned query result becomes available in one complete store. let destination = unsafe { &*pointer.cast::() }; destination.store(value.to_le(), Ordering::Relaxed); } else { for (index, byte) in value.to_le_bytes().into_iter().enumerate() { - // SAFETY: timestamp_mapping checked the complete eight-byte extent. - // The owned vmap pins its backing; byte atomics permit unaligned and - // overlapping timestamp destinations without mutable slice aliases. - let destination = unsafe { &*base.add(self.offset + index).cast::() }; + // SAFETY: The complete eight-byte extent was checked above. The owned + // vmap pins its backing; byte atomics permit unaligned and overlapping + // timestamp destinations without mutable slice aliases. + let destination = unsafe { &*base.add(offset + index).cast::() }; destination.store(byte, Ordering::Relaxed); } } // A CPU-retired query can be consumed by a later GPU command. WRITEBACK // objects use a cacheable vmap, while GPU mappings need not snoop that cache. // Publish the bytes before signalling the completion fence or queue frontier. - if self.mapping.owner().is_writeback() { + if self.0.owner().is_writeback() { crate::pgtable::clean_poc(pointer, size_of::()); } crate::pgtable::poc_barrier(); } } +/// Prepared CPU timestamp access with its own vmap; destruction happens after completion +/// signalling. +pub(crate) struct TimestampMapping { + mapping: TimestampVmap, + offset: usize, +} + +impl TimestampMapping { + /// Prepare one bounded CPU timestamp destination on retained GEM backing. + pub(crate) fn new(gem: &gem::Object, offset: usize) -> Result { + use kernel::drm::gem::BaseObject; + if offset.checked_add(size_of::()).ok_or(EOVERFLOW)? > gem.size() { + return Err(ERANGE); + } + Ok(Self { + mapping: TimestampVmap::new(gem)?, + offset, + }) + } + + pub(crate) fn write(&self, value: u64) { + self.mapping.write(self.offset, value); + } +} + /// An object mapping into a [`Vm`], which reserves the address range from use by other mappings. pub(crate) struct KernelMapping(ManuallyDrop>); @@ -1286,6 +1313,13 @@ impl KernelMapping { /// creating mutable aliases. An unaligned complete timestamp is not atomic. /// Retain CPU access before this destination's completion fence is published. pub(crate) fn timestamp_mapping(&self, offset: usize) -> Result { + let gem = self.0._gem.as_ref().ok_or(EINVAL)?; + TimestampMapping::new(gem, self.timestamp_offset(offset)?) + } + + /// The GEM object offset of the timestamp at `offset` in this mapping, whose complete + /// extent lies inside both the mapping and the object. + pub(crate) fn timestamp_offset(&self, offset: usize) -> Result { use kernel::drm::gem::BaseObject; let gem = self.0._gem.as_ref().ok_or(EINVAL)?; if offset.checked_add(size_of::()).ok_or(EOVERFLOW)? > self.size() { @@ -1296,7 +1330,12 @@ impl KernelMapping { if end > gem.size() { return Err(ERANGE); } - TimestampMapping::new(gem, start) + Ok(start) + } + + /// A kernel CPU mapping of this mapping's GEM backing, for timestamp writes. + pub(crate) fn timestamp_vmap(&self) -> Result { + TimestampVmap::new(self.0._gem.as_ref().ok_or(EINVAL)?) } /// Gives a fresh kernel mapping a firmware-cached prefix, preserving its tail. diff --git a/drivers/gpu/drm/asahi/timestamp.rs b/drivers/gpu/drm/asahi/timestamp.rs index 976ec9f6bf48b..0d365a8d930bb 100644 --- a/drivers/gpu/drm/asahi/timestamp.rs +++ b/drivers/gpu/drm/asahi/timestamp.rs @@ -13,10 +13,20 @@ use kernel::{prelude::*, sync::Arc}; #[cfg(CONFIG_DRM_ASAHI_G16)] use kernel::{uapi, xarray}; +/// A firmware-mapped timestamp buffer with a kernel CPU mapping of its backing, kept for the +/// buffer's lifetime so that commands need no mapping of their own. +pub(crate) struct FirmwareBuffer { + mapping: Arc, + cpu: mmu::TimestampVmap, +} + /// A bound timestamp destination. Only backends which publish firmware writes need a UAT alias. /// CPU-retired timestamps retain their backing without consuming global firmware address space. pub(crate) enum Buffer { - Firmware(Arc), + /// The last reference may be a command's, dropped where fence producers run, while + /// releasing the CPU mapping takes the reservation lock: the deferral runs the release + /// on the cleanup queue. + Firmware(crate::cleanup::Deferred), #[cfg(CONFIG_DRM_ASAHI_G16)] Cpu { // The GEM pin keeps the object's held SG table alive through CPU publication. @@ -27,7 +37,11 @@ pub(crate) enum Buffer { impl Buffer { pub(crate) fn firmware(mapping: mmu::KernelMapping) -> Result { - Ok(Self::Firmware(Arc::new(mapping, GFP_KERNEL)?)) + let cpu = mapping.timestamp_vmap()?; + Ok(Self::Firmware(crate::cleanup::Deferred::new(FirmwareBuffer { + mapping: Arc::new(mapping, GFP_KERNEL)?, + cpu, + })?)) } /// Retain a CPU-written byte range without publishing any firmware address. @@ -46,7 +60,7 @@ impl Buffer { pub(crate) fn size(&self) -> usize { match self { - Self::Firmware(mapping) => mapping.size(), + Self::Firmware(buffer) => buffer.mapping.size(), #[cfg(CONFIG_DRM_ASAHI_G16)] Self::Cpu { range, .. } => range.end - range.start, } @@ -54,7 +68,7 @@ impl Buffer { pub(crate) fn timestamp_mapping(&self, offset: usize) -> Result { match self { - Self::Firmware(mapping) => mapping.timestamp_mapping(offset), + Self::Firmware(buffer) => buffer.mapping.timestamp_mapping(offset), #[cfg(CONFIG_DRM_ASAHI_G16)] Self::Cpu { gem, range, .. } => { if offset.checked_add(size_of::()).ok_or(EOVERFLOW)? > self.size() { @@ -65,10 +79,30 @@ impl Buffer { } } + /// The object offset of the timestamp at `offset` of a firmware buffer, for + /// [`Self::write_timestamp`]. + pub(crate) fn timestamp_offset(&self, offset: usize) -> Result { + match self { + Self::Firmware(buffer) => buffer.mapping.timestamp_offset(offset), + #[cfg(CONFIG_DRM_ASAHI_G16)] + Self::Cpu { .. } => Err(EINVAL), + } + } + + /// Writes a timestamp at an object offset returned by [`Self::timestamp_offset`], + /// through the buffer's own CPU mapping. + pub(crate) fn write_timestamp(&self, offset: usize, value: u64) { + match self { + Self::Firmware(buffer) => buffer.cpu.write(offset, value), + #[cfg(CONFIG_DRM_ASAHI_G16)] + Self::Cpu { .. } => {} + } + } + /// Legacy command encoders publish firmware pointers and must never accept a CPU-only buffer. pub(crate) fn firmware_mapping(&self) -> Result> { match self { - Self::Firmware(mapping) => Ok(mapping.clone()), + Self::Firmware(buffer) => Ok(buffer.mapping.clone()), #[cfg(CONFIG_DRM_ASAHI_G16)] Self::Cpu { .. } => Err(EINVAL), } From 7907afd4df0bf3d5aa9f9a0b9138986895c95f67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Bal=C3=A1zsy?= Date: Sun, 25 Oct 2026 16:28:36 +0100 Subject: [PATCH 059/388] drm/apple: route the T6041 Type-C displays through the display fabric MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dcpext of a T6041 listed the DP PHY and crossbar of every Type-C port and picked the port whose PHY had entered DP alt mode at connect time. The display fabric already routes the T602x, T6030 and J700 Type-C ports: a typec-routes node under the DCP, one mode switch per port, driven by the port controller. Use it for desktop 26.x as well and drop the private route (apple,dptx-phys, dp-phyN/dp-xbarN, apple_t6040_phy_dp_alt()). Three things differ from the other fabric users: - The T6040 DP PHY must be powered when the display connects, not when a route is claimed. The port controller calls the PHY's and the route's mode switch in connector order, and the PHY has to have seen DP alt mode and the plug orientation before its lanes are programmed. - The compact DPTX of desktop 26.x takes the crossbar link down and up around a link configuration through dcp->xbar, while the fabric's own direct-link helper is T6030 only. The route sets dcp->xbar for the time it is active; without it the link never trains. - There is a single Type-C pipeline. A port in DP alt mode without a sink (a dock, or a pin assignment the PHY cannot drive) must not hold it against the port that has a display: claim it on HPD only. Tested on a J616s (Mac16,7) with a USB-C display on one port: attach at boot, rebind of the port controller (replug) and a device suspend test (pm_test=devices). The other ports and a second display are not tested. Signed-off-by: FrantiÅ¡ek Balázsy --- drivers/gpu/drm/apple/dcp-fabric.c | 78 +++++++++++- drivers/gpu/drm/apple/dcp-fabric.h | 3 + drivers/gpu/drm/apple/dcp-internal.h | 11 -- drivers/gpu/drm/apple/dcp.c | 181 ++++----------------------- drivers/phy/apple/t6040-usb2.c | 9 -- include/linux/soc/apple/dp-tunnel.h | 15 --- 6 files changed, 106 insertions(+), 191 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp-fabric.c b/drivers/gpu/drm/apple/dcp-fabric.c index eb4d0363afb6b..2ca975e28ffd4 100644 --- a/drivers/gpu/drm/apple/dcp-fabric.c +++ b/drivers/gpu/drm/apple/dcp-fabric.c @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -299,6 +300,54 @@ static int dcp_dpxbar_tunnel_select_source(struct mux_control *mux, int state) return ret; } +/* + * Desktop 26.x firmware does not power the DP PHY of a Type-C route itself: + * the route does, once its crossbar points at the pipeline, and gives it + * back before the crossbar is released. + */ +static int dcp_typec_route_phy_on(struct apple_dcp_typec_route *route) +{ + int ret; + + if (route->dcp->fw_compat != DCP_FIRMWARE_V_26_6_DESKTOP || + route->phy_powered) + return 0; + ret = phy_init(route->phy); + if (!ret) { + ret = phy_power_on(route->phy); + if (ret) + phy_exit(route->phy); + } + if (ret) { + dev_warn(route->dcp->dev, "Type-C DP PHY init/power: %d\n", ret); + return ret; + } + route->phy_powered = true; + msleep(25); + return 0; +} + +/* + * The PHY is powered when the display connects, not when the route is + * claimed: the port controller's mode switches run in connector order, and + * the PHY must have seen DP alt mode and the plug orientation first. + */ +int dcp_typec_phy_on(struct apple_dcp *dcp) +{ + struct apple_dcp_typec_route *route = READ_ONCE(dcp->active_typec_route); + + return route ? dcp_typec_route_phy_on(route) : 0; +} + +static void dcp_typec_route_phy_off(struct apple_dcp_typec_route *route) +{ + if (!route->phy_powered) + return; + route->phy_powered = false; + phy_power_off(route->phy); + phy_exit(route->phy); +} + static int dcp_typec_route_activate(struct apple_dcp_typec_route *route, struct mux_control *xbar) { @@ -363,6 +412,12 @@ static int dcp_typec_route_activate(struct apple_dcp_typec_route *route, dcp_modes_begin_attachment(dcp); apple_connector_edid_set_live(dcp->connector, false); dcp->phy = route->phy; + /* + * Desktop 26.x compact DPTX takes the crossbar link down and up around + * a link configuration (dptxport_compact_xbar_link()) through dcp->xbar. + */ + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) + dcp->xbar = route->xbar; dcp->dptx_phy = route->dptx_phy; dcp->connector_type = DRM_MODE_CONNECTOR_USB; WRITE_ONCE(dcp->ext_backlight, false); @@ -437,6 +492,7 @@ static int dcp_typec_route_deactivate(struct apple_dcp_typec_route *route) if (route->deactivate_error) return route->deactivate_error; if (!route->deactivate_xbar_done) { + dcp_typec_route_phy_off(route); if (!route->tunnel || route->xbar_up) ret = mux_control_deselect(route->active_xbar ?: route->xbar); else @@ -539,8 +595,15 @@ static int dcp_typec_route_deactivate(struct apple_dcp_typec_route *route) dcp->dptx_phy = dcp->typec_routes[0].dptx_phy; } } else { - /* Keep DPTX endpoint discovery working before a cable is attached. */ - dcp->phy = dcp->typec_routes[0].phy; + /* + * Keep DPTX endpoint discovery working before a cable is + * attached. Desktop 26.x powers the PHY with the route, so an + * idle pipeline must not point at one. + */ + dcp->phy = dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP ? + NULL : dcp->typec_routes[0].phy; + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) + dcp->xbar = NULL; dcp->dptx_phy = dcp->typec_routes[0].dptx_phy; dcp->connector_type = DRM_MODE_CONNECTOR_USB; } @@ -1514,6 +1577,17 @@ static int dcp_typec_route_set(struct typec_mux_dev *mux, } } + /* + * Desktop 26.x has one Type-C pipeline: only a port with a sink (HPD) + * takes it. A dock or cable in DP alt mode without a display behind it + * would otherwise hold it against the port that has one. + */ + if (!port->owner && !hpd && + route->dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) { + port->applied_valid = true; + return 0; + } + if (!port->owner) { best = dcp_typec_free_route(port); /* Every pipeline is driving another port's display. */ diff --git a/drivers/gpu/drm/apple/dcp-fabric.h b/drivers/gpu/drm/apple/dcp-fabric.h index 2647ddff4653d..2044a3a715837 100644 --- a/drivers/gpu/drm/apple/dcp-fabric.h +++ b/drivers/gpu/drm/apple/dcp-fabric.h @@ -39,6 +39,8 @@ struct apple_dcp_typec_route { u32 dptx_phy; u32 mux_index; bool selected; + /* desktop 26.x: the DP PHY is initialised and powered for this route */ + bool phy_powered; /* crossbar output actually selected: xbar (dpphy) or a Thunderbolt dpin */ unsigned int tunnel_dpin; u64 tunnel_generation; @@ -51,6 +53,7 @@ struct apple_dcp_typec_route { int deactivate_error; }; +int dcp_typec_phy_on(struct apple_dcp *dcp); bool dcp_is_typec_output(struct apple_dcp *dcp); bool dcp_is_usb4_output(struct apple_dcp *dcp); bool dcp_uses_t6020_tunnel_flow(struct apple_dcp *dcp); diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index bbc4c8453c536..b3db4d11516e6 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -270,17 +270,6 @@ struct dcp_desktop_state { * display (unplug); the next swap needs a power-on and a mode set. */ bool ext_pipe_off; - /* - * Dynamic Type-C DP route (T6041 dcpext1): the DP PHYs and crossbars - * of every Type-C port, the one in use (or -1), the DPTX target PHY - * of each and the crossbar state selecting this processor. - */ - unsigned int ndyn; - int dyn_active; - struct phy *dyn_phy[3]; - struct mux_control *dyn_xbar[3]; - u32 dyn_dptx_phy[3]; - u32 dyn_mux_index; }; /* TODO: move IOMFB members to its own struct */ diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 3b3ea4abac3b1..0563b07f67c1e 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -1893,119 +1893,30 @@ static int dcp_dptx_connect_tile(struct apple_dcp *dcp) /* * Desktop 26.x external processors (T6041 dcpext0 HDMI, dcpext1 USB-C). - * They keep the classic external model of the driver this was ported from: - * no Type-C display fabric, the DPTX link set up on HPD (HDMI) or on the - * DP alt mode hotplug of the port (USB-C), and the Type-C port chosen - * dynamically from the PHY that entered DP alt mode. + * The DPTX link is set up on HPD (HDMI) or when the Type-C display fabric + * routes a port's DP alt mode to the processor (USB-C). */ -/* The caller holds hpd_mutex, which also orders dcp->phy against a connect. */ -static int dcp_desktop_dyn_route_select_locked(struct apple_dcp *dcp) -{ - struct dcp_desktop_state *d = dcp->desktop; - typeof(&apple_t6040_phy_dp_alt) dp_alt; - unsigned int i; - int ret; - - lockdep_assert_held(&dcp->hpd_mutex); - if (!d->ndyn || d->dyn_active >= 0) - return 0; - /* Looked up at runtime so appledrm does not require the T6040 PHY driver. */ -#if IS_ENABLED(CONFIG_PHY_APPLE_T6040_USB2) - dp_alt = symbol_get(apple_t6040_phy_dp_alt); -#else - dp_alt = NULL; -#endif - if (!dp_alt) { - dev_dbg(dcp->dev, "T6040 PHY driver not loaded\n"); - return -ENODEV; - } - for (i = 0; i < d->ndyn; i++) - if (dp_alt(d->dyn_phy[i])) - break; -#if IS_ENABLED(CONFIG_PHY_APPLE_T6040_USB2) - symbol_put(apple_t6040_phy_dp_alt); -#endif - if (i == d->ndyn) { - dev_dbg(dcp->dev, "no Type-C port in DP alt mode\n"); - return -ENODEV; - } - ret = mux_control_select(d->dyn_xbar[i], d->dyn_mux_index); - if (ret) { - dev_warn(dcp->dev, "Type-C port %u: crossbar select: %d\n", i, ret); - return ret; - } - ret = phy_init(d->dyn_phy[i]); - if (!ret) { - ret = phy_power_on(d->dyn_phy[i]); - if (ret) - phy_exit(d->dyn_phy[i]); - } - if (ret) { - dev_warn(dcp->dev, "Type-C port %u: DP PHY init/power: %d\n", i, ret); - mux_control_deselect(d->dyn_xbar[i]); - return ret; - } - msleep(25); - dcp->phy = d->dyn_phy[i]; - dcp->xbar = d->dyn_xbar[i]; - dcp->dptx_phy = d->dyn_dptx_phy[i]; - d->dyn_active = i; - dev_dbg(dcp->dev, "Type-C port %u selected (DPTX target PHY %u, crossbar state %u)\n", - i, dcp->dptx_phy, d->dyn_mux_index); - return 0; -} - -static void dcp_desktop_dyn_route_release_locked(struct apple_dcp *dcp) -{ - struct dcp_desktop_state *d = dcp->desktop; - int i = d->dyn_active; - - lockdep_assert_held(&dcp->hpd_mutex); - if (!d->ndyn || i < 0) - return; - d->dyn_active = -1; - dcp->phy = NULL; - dcp->xbar = NULL; - phy_power_off(d->dyn_phy[i]); - phy_exit(d->dyn_phy[i]); - mux_control_deselect(d->dyn_xbar[i]); - dev_dbg(dcp->dev, "Type-C port %u released\n", i); -} - -/* Unbind: a selected route holds its crossbar and its powered PHY. */ -static void dcp_desktop_dyn_route_release(struct apple_dcp *dcp) -{ - if (!dcp->desktop) - return; - guard(mutex)(&dcp->hpd_mutex); - dcp_desktop_dyn_route_release_locked(dcp); -} - static int dcp_desktop_dptx_connect(struct apple_dcp *dcp, u32 port) { struct dptx_port *dptx = &dcp->dptxport[port]; unsigned long left; - bool selected; int ret = 0; mutex_lock(&dcp->hpd_mutex); - /* A route selected here is released again if the connect fails. */ - selected = !dcp->phy && dcp->desktop->ndyn; - if (selected) { - ret = dcp_desktop_dyn_route_select_locked(dcp); - if (ret) - goto out_unlock; - } if (!dcp->phy) { dev_warn(dcp->dev, "dcp_dptx_connect: missing phy\n"); ret = -ENODEV; goto out_unlock; } + /* A fabric route powers its PHY here (a no-op for the other routes). */ + ret = dcp_typec_phy_on(dcp); + if (ret) + goto out_unlock; if (!dptx_service_available(dptx)) { dev_warn(dcp->dev, "dcp_dptx_connect: dptx service for port %u not enabled\n", port); ret = -ENODEV; - goto out_release; + goto out_unlock; } if (dptx->connected) goto out_unlock; @@ -2015,13 +1926,13 @@ static int dcp_desktop_dptx_connect(struct apple_dcp *dcp, u32 port) ret = dptxport_connect(dptx->service, 0, dcp->dptx_phy, dcp->dptx_die, true); if (ret) { dev_warn(dcp->dev, "DPTX port %u: connect failed: %d\n", port, ret); - goto out_release; + goto out_unlock; } ret = dptxport_request_display(dptx->service); if (ret) { dev_warn(dcp->dev, "DPTX port %u: display request failed: %d\n", port, ret); - goto out_release; + goto out_unlock; } dptx->connected = true; @@ -2050,9 +1961,6 @@ static int dcp_desktop_dptx_connect(struct apple_dcp *dcp, u32 port) av_service_connect(dcp); return 0; -out_release: - if (selected) - dcp_desktop_dyn_route_release_locked(dcp); out_unlock: mutex_unlock(&dcp->hpd_mutex); return ret; @@ -2070,8 +1978,6 @@ static int dcp_desktop_dptx_disconnect_oob(struct apple_dcp *dcp, u32 port) dptxport_set_hpd(dcp->dptxport[port].service, false); } ret = dcp_dptx_disconnect(dcp, port); - scoped_guard(mutex, &dcp->hpd_mutex) - dcp_desktop_dyn_route_release_locked(dcp); if (!dcp->main_display) dcp->desktop->ext_pipe_off = true; return ret; @@ -3142,7 +3048,7 @@ static int dcp_start_desktop_external(struct apple_dcp *dcp, u32 map) if (ret) return ret; - if (!dcp->phy && !d->ndyn) { + if (!dcp->phy) { dev_warn(dcp->dev, "no DP PHY and no Type-C route: no display link\n"); return 0; } @@ -4975,8 +4881,6 @@ VISIBLE_IF_KUNIT void dcp_comp_unbind(struct device *dev, struct device *main, v dcp->systemep = NULL; } - dcp_desktop_dyn_route_release(dcp); - /* * The auxiliary endpoints a desktop 26.x start brought up, still * routed while their shutdown is acknowledged. @@ -5087,62 +4991,31 @@ static void dcp_desktop_phy_off(void *data) } /* - * The outputs of a desktop 26.x external processor (T6041 dcpext): the - * fixed HDMI route's DP PHY is powered here, as the boot loader's working - * sequence does; a dynamic Type-C route lists the DP PHY and crossbar of - * every port (dp-phyN, dp-xbarN, apple,dptx-phys, apple,dptx-mux-index), - * and the one in DP alt mode is claimed at connect. + * The fixed HDMI output of a desktop 26.x external processor (T6041 dcpext0): + * its DP PHY is powered here, as the boot loader's working sequence does. A + * Type-C output (dcpext1) has no fixed PHY; the display fabric powers the + * PHY of the route a port claims (see dcp_typec_phy_on()). */ static int dcp_desktop_probe_routes(struct apple_dcp *dcp) { - struct dcp_desktop_state *d = dcp->desktop; struct device *dev = dcp->dev; - unsigned int i; - int nd, ret; + int ret; - d->dyn_active = -1; - if (!dcp_desktop_external(dcp)) + if (!dcp_desktop_external(dcp) || !dcp->phy) return 0; - if (dcp->phy) { - ret = phy_init(dcp->phy); - if (ret) - return dev_err_probe(dev, ret, "Failed to initialize dp-phy\n"); - ret = phy_power_on(dcp->phy); - if (ret) { - phy_exit(dcp->phy); - return dev_err_probe(dev, ret, "Failed to power on dp-phy\n"); - } - ret = devm_add_action_or_reset(dev, dcp_desktop_phy_off, dcp->phy); - if (ret) - return ret; - msleep(25); + ret = phy_init(dcp->phy); + if (ret) + return dev_err_probe(dev, ret, "Failed to initialize dp-phy\n"); + ret = phy_power_on(dcp->phy); + if (ret) { + phy_exit(dcp->phy); + return dev_err_probe(dev, ret, "Failed to power on dp-phy\n"); } - - nd = of_property_count_u32_elems(dev->of_node, "apple,dptx-phys"); - if (nd <= 0) - return 0; - if (nd > ARRAY_SIZE(d->dyn_phy)) - return dev_err_probe(dev, -EINVAL, "too many Type-C routes: %d\n", nd); - - for (i = 0; i < nd; i++) { - char name[16]; - - snprintf(name, sizeof(name), "dp-phy%u", i); - d->dyn_phy[i] = devm_phy_get(dev, name); - if (IS_ERR(d->dyn_phy[i])) - return dev_err_probe(dev, PTR_ERR(d->dyn_phy[i]), "%s\n", name); - snprintf(name, sizeof(name), "dp-xbar%u", i); - d->dyn_xbar[i] = devm_mux_control_get(dev, name); - if (IS_ERR(d->dyn_xbar[i])) - return dev_err_probe(dev, PTR_ERR(d->dyn_xbar[i]), "%s\n", name); - of_property_read_u32_index(dev->of_node, "apple,dptx-phys", i, - &d->dyn_dptx_phy[i]); - } - of_property_read_u32(dev->of_node, "apple,dptx-mux-index", &d->dyn_mux_index); - d->ndyn = nd; - dev_info(dev, "%u Type-C DP routes, crossbar state %u\n", d->ndyn, - d->dyn_mux_index); + ret = devm_add_action_or_reset(dev, dcp_desktop_phy_off, dcp->phy); + if (ret) + return ret; + msleep(25); return 0; } diff --git a/drivers/phy/apple/t6040-usb2.c b/drivers/phy/apple/t6040-usb2.c index 868d4304b2395..c15c004d94c83 100644 --- a/drivers/phy/apple/t6040-usb2.c +++ b/drivers/phy/apple/t6040-usb2.c @@ -1509,15 +1509,6 @@ static int t6040_dp_mux_set(struct typec_mux_dev *mux, struct typec_mux_state *s return 0; } -/* For the DCP's Type-C route: is this PHY's port in DP alt mode (C/E)? */ -bool apple_t6040_phy_dp_alt(struct phy *phy) -{ - struct t6040_usb2_phy *tphy = phy_get_drvdata(phy); - - return tphy && tphy->dp_alt; -} -EXPORT_SYMBOL_GPL(apple_t6040_phy_dp_alt); - static void t6040_dp_mux_unregister(void *mux) { typec_mux_unregister(mux); diff --git a/include/linux/soc/apple/dp-tunnel.h b/include/linux/soc/apple/dp-tunnel.h index 0732ca5aabdf4..7bd546080f25b 100644 --- a/include/linux/soc/apple/dp-tunnel.h +++ b/include/linux/soc/apple/dp-tunnel.h @@ -51,21 +51,6 @@ int apple_atc_dp_tunnel_rate(struct phy *phy, unsigned int dpin, u8 rate); */ int apple_atc_dp_tunnel_open(struct phy *phy); -/* - * T6040 Type-C PHY: true while the Type-C controller holds the PHY's port in - * DisplayPort alt mode with pin assignment C or E (DP on all four lanes), so - * appledrm can route a DCP that lists several Type-C DP PHYs to the one that - * has a DisplayPort sink plugged. - */ -#if IS_ENABLED(CONFIG_PHY_APPLE_T6040_USB2) -bool apple_t6040_phy_dp_alt(struct phy *phy); -#else -static inline bool apple_t6040_phy_dp_alt(struct phy *phy) -{ - return false; -} -#endif - /* * Display crossbar: take the connection of an already selected output down or * bring it back up (clock gates and enables) around a link reconfiguration. From 4ce44bb60232452e328e5d046357258159a948e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Bal=C3=A1zsy?= Date: Sun, 25 Oct 2026 14:04:18 +0100 Subject: [PATCH 060/388] drm/apple: check the desktop 26.6 callback records against their handlers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The H17P callbacks are checked against the record sizes the firmware sends; the desktop 26.6 ones were not. The u16 offset keeps a record inside the shared memory, but a handler could still read past a short request or write past a short reply into the next record. Give every desktop 26.6 handler the request and reply size it reads and writes, and refuse a callback whose record is shorter, or whose lengths do not fit the message, the way H17P refuses a malformed one. Longer records stay accepted. Signed-off-by: FrantiÅ¡ek Balázsy --- drivers/gpu/drm/apple/iomfb.c | 9 ++ drivers/gpu/drm/apple/iomfb.h | 2 + drivers/gpu/drm/apple/iomfb_v26_6_desktop.c | 91 +++++++++++++++++++++ 3 files changed, 102 insertions(+) diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index df8a70b6f2a20..924139aad4c2d 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -1289,6 +1289,15 @@ static void dcpep_handle_cb(struct apple_dcp *dcp, enum dcp_context_id context, handled = tag >= 0 && tag < IOMFB_MAX_CB && dcp->cb_handlers && dcp->cb_handlers[tag]; + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && handled && + (length < sizeof(*hdr) || + (u64)sizeof(*hdr) + hdr->in_len + hdr->out_len > length || + !iomfb_check_callback_v26_6_desktop(tag, hdr->in_len, hdr->out_len))) { + dev_err(dev, "short IOMFB callback %d (%u, %u, length %u)\n", + tag, hdr->in_len, hdr->out_len, length); + dcp->crashed = true; + return; + } if (dcp->fw_compat == DCP_FIRMWARE_H17P && (tag < 0 || tag >= IOMFB_MAX_CB || (u64)sizeof(*hdr) + hdr->in_len + hdr->out_len != length || diff --git a/drivers/gpu/drm/apple/iomfb.h b/drivers/gpu/drm/apple/iomfb.h index 7e6e16f1dddf7..7c60a81207ad7 100644 --- a/drivers/gpu/drm/apple/iomfb.h +++ b/drivers/gpu/drm/apple/iomfb.h @@ -212,6 +212,8 @@ bool iomfb_validate_callback_h17p(int tag, u32 in_len, u32 out_len); struct apple_dcp; bool iomfb_check_callback_h17p(struct apple_dcp *dcp, int tag, u32 in_len, u32 out_len); +/* Desktop 26.6: does the record hold what the tag's handler reads and writes? */ +bool iomfb_check_callback_v26_6_desktop(int tag, u32 in_len, u32 out_len); typedef bool (*iomfb_cb_handler)(struct apple_dcp *, int, void *, void *); diff --git a/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c b/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c index 98a3504a9a5f0..f5963c594a476 100644 --- a/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c +++ b/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c @@ -155,6 +155,97 @@ static const iomfb_cb_handler cb_handlers[IOMFB_MAX_CB] = { [599] = trampoline_nop, }; +/* + * What each handler above reads from the request and writes to the reply. + * The firmware may send longer records; a shorter one is refused. + */ +struct dcp_cb_min_v26_6_desktop { + u32 in_len; + u32 out_len; + bool valid; +}; + +#define CB_MIN(in, out) { .in_len = (in), .out_len = (out), .valid = true } + +static const struct dcp_cb_min_v26_6_desktop cb_min[IOMFB_MAX_CB] = { + [0] = CB_MIN(0, sizeof(u8)), + [1] = CB_MIN(0, sizeof(u8)), + [2] = CB_MIN(0, 0), + [3] = CB_MIN(0, sizeof(struct dcp_rt_bandwidth)), + [6] = CB_MIN(sizeof(struct dcp_set_frame_sync_props_req_v26_6_desktop), + sizeof(struct dcp_set_frame_sync_props_resp_v26_6_desktop)), + [100] = CB_MIN(0, 0), + [101] = CB_MIN(0, sizeof(u32)), + [102] = CB_MIN(0, 0), + [104] = CB_MIN(0, 0), + [107] = CB_MIN(0, 0), + [108] = CB_MIN(0, sizeof(u8)), + [109] = CB_MIN(0, sizeof(u8)), + [110] = CB_MIN(0, sizeof(u8)), + [111] = CB_MIN(0, sizeof(u8)), + [112] = CB_MIN(0, sizeof(u8)), + [113] = CB_MIN(0, sizeof(u8)), + [114] = CB_MIN(sizeof(struct dcp_dict_passthrough_req_v26_6), + sizeof(struct dcp_dict_passthrough_resp_v26_6)), + [115] = CB_MIN(sizeof(struct dcpep_get_tiling_state_req), + sizeof(struct dcpep_get_tiling_state_resp)), + [121] = CB_MIN(0, 0), + [122] = CB_MIN(0, sizeof(u8)), + [123] = CB_MIN(0, sizeof(u8)), + [125] = CB_MIN(sizeof(struct dcp_read_edt_data_req), + sizeof(dcp_read_edt_data_resp_t)), + [127] = CB_MIN(sizeof(u32), sizeof(u8)), + [128] = CB_MIN(sizeof(struct dcp_set_dcpav_prop_chunk_req), sizeof(u8)), + [129] = CB_MIN(sizeof(struct dcp_set_dcpav_prop_end_req), sizeof(u8)), + [134] = CB_MIN(0, sizeof(u32)), + [201] = CB_MIN(sizeof(struct dcp_map_buf_req), + sizeof(struct DCP_FW_NAME(dcp_map_buf_resp))), + [206] = CB_MIN(0, sizeof(u8)), + [207] = CB_MIN(0, sizeof(u8)), + [208] = CB_MIN(0, 0), + [209] = CB_MIN(0, sizeof(u64)), + [300] = CB_MIN(sizeof(struct iomfb_property), 0), + [400] = CB_MIN(0, 0), + [401] = CB_MIN(sizeof(struct dcp_get_uint_prop_req), + sizeof(struct dcp_get_uint_prop_resp)), + [406] = CB_MIN(0, 0), + [408] = CB_MIN(0, sizeof(u64)), + [411] = CB_MIN(sizeof(struct DCP_FW_NAME(dcp_map_reg_req)), + sizeof(struct DCP_FW_NAME(dcp_map_reg_resp))), + [413] = CB_MIN(0, sizeof(u8)), + [414] = CB_MIN(sizeof(struct iomfb_sr_set_property_int_req), sizeof(u8)), + [415] = CB_MIN(0, sizeof(u8)), + [451] = CB_MIN(sizeof(struct dcp_allocate_buffer_req), + sizeof(struct dcp_allocate_buffer_resp)), + [552] = CB_MIN(0, sizeof(u8)), + [561] = CB_MIN(0, sizeof(u8)), + [563] = CB_MIN(0, sizeof(u8)), + [565] = CB_MIN(0, sizeof(u8)), + [567] = CB_MIN(0, sizeof(u8)), + [572] = CB_MIN(0, sizeof(u32)), + [575] = CB_MIN(sizeof(struct dcp_hotplug_req_v26_6), + sizeof(struct dcp_hotplug_resp_v26_6)), + [576] = CB_MIN(0, 0), + [581] = CB_MIN(0, 0), + [582] = CB_MIN(0, sizeof(u8)), + [590] = CB_MIN(sizeof(struct DCP_FW_NAME(dc_swap_complete_resp)), 0), + [592] = CB_MIN(sizeof(dcp_swap_intent_t), 0), + [593] = CB_MIN(sizeof(u32), 0), + [594] = CB_MIN(sizeof(u8), 0), + [595] = CB_MIN(0, 0), + [598] = CB_MIN(0, 0), + [599] = CB_MIN(0, 0), +}; + +#undef CB_MIN + +bool iomfb_check_callback_v26_6_desktop(int tag, u32 in_len, u32 out_len) +{ + if (tag < 0 || tag >= IOMFB_MAX_CB || !cb_min[tag].valid) + return false; + return in_len >= cb_min[tag].in_len && out_len >= cb_min[tag].out_len; +} + void DCP_FW_NAME(iomfb_start)(struct apple_dcp *dcp) { dcp->cb_handlers = cb_handlers; From 0012743f982a43c9b345156dd1523e723418c423 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Bal=C3=A1zsy?= Date: Sun, 25 Oct 2026 14:04:43 +0100 Subject: [PATCH 061/388] phy: apple: t6040: give the Type-C lanes back to USB3 after DP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit t6040_dp_power_on() shuts the USB3 side down, but t6040_dp_power_off() only cleared dp_up, and t6040_usb3_power_on() could re-initialise the PHY under a live DP mode. Once a Type-C DP session ended with the plug still in, USB3 on that port stayed down until dwc3 power cycled the PHY. Track whether dwc3 holds the USB3 phy on and asked for the PIPE switch. USB3 bring-up now waits while DP mode is programmed, and when DP mode ends and the port has left DP alt mode, from the DP power-off or the mode switch, whichever comes last, USB3 is brought up again and the PIPE goes back to the PHY. On an unplug or a DP mode change tipd drops the USB role first, dwc3 powers the USB3 phy off, and nothing changes there. Signed-off-by: FrantiÅ¡ek Balázsy --- drivers/phy/apple/t6040-usb2.c | 62 ++++++++++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 7 deletions(-) diff --git a/drivers/phy/apple/t6040-usb2.c b/drivers/phy/apple/t6040-usb2.c index c15c004d94c83..20fb5e9da5cb0 100644 --- a/drivers/phy/apple/t6040-usb2.c +++ b/drivers/phy/apple/t6040-usb2.c @@ -237,6 +237,9 @@ struct t6040_usb2_phy { struct completion app_reply; enum typec_orientation orientation; bool usb3_up; /* tuned by the coprocessor this power cycle */ + bool usb3_on; /* dwc3 holds the USB3 phy powered on */ + bool usb3_pipe; /* dwc3 asked for the PIPE switch this power cycle */ + bool usb3_lent; /* DP mode took the lanes of this Type-C port */ bool pipe_switched; /* the controller's PIPE runs on this PHY */ /* DisplayPort (PHY mode 3); dp_ok is false when the DT lacks a DP route */ @@ -882,15 +885,19 @@ static void t6040_usb3_pipe_park(struct t6040_usb2_phy *tphy) * its PIPE on the dummy PHY. A failure here leaves the port at USB2, so it is * logged and not returned: the USB2 PHY and the core must still come up. */ -static int t6040_usb3_power_on(struct phy *phy) +static void t6040_usb3_up_locked(struct t6040_usb2_phy *tphy) { - struct t6040_usb2_phy *tphy = phy_get_drvdata(phy); bool swap; int ret; - guard(mutex)(&tphy->lock); + lockdep_assert_held(&tphy->lock); if (!tphy->usb3_ok || tphy->usb3_up) - return 0; + return; + /* DP mode owns all four lanes; USB3 comes back in t6040_usb3_reclaim_locked(). */ + if (tphy->dp_up) { + dev_dbg(tphy->dev, "USB3: lanes held by DP, USB2 only for now\n"); + return; + } switch (tphy->orientation) { case TYPEC_ORIENTATION_NORMAL: @@ -901,7 +908,7 @@ static int t6040_usb3_power_on(struct phy *phy) break; default: dev_info(tphy->dev, "USB3: no plug orientation, USB2 only\n"); - return 0; + return; } /* PIPE_HANDLER_DEFAULT: the dummy PIPE clock the controller keeps using at USB2 */ @@ -918,7 +925,7 @@ static int t6040_usb3_power_on(struct phy *phy) tphy->usb3_up = true; dev_dbg(tphy->dev, "USB3 PHY up, lane %d\n", swap); - return 0; + return; usb2_only: /* power_off skips a PHY that never came up: undo here, so the next attach starts clean */ @@ -926,6 +933,15 @@ static int t6040_usb3_power_on(struct phy *phy) sleep: t6040_usb3_sleep(tphy); dev_warn(tphy->dev, "USB3 PHY bring-up failed (%d), USB2 only\n", ret); +} + +static int t6040_usb3_power_on(struct phy *phy) +{ + struct t6040_usb2_phy *tphy = phy_get_drvdata(phy); + + guard(mutex)(&tphy->lock); + tphy->usb3_on = true; + t6040_usb3_up_locked(tphy); return 0; } @@ -955,6 +971,8 @@ static int t6040_usb3_power_off(struct phy *phy) struct t6040_usb2_phy *tphy = phy_get_drvdata(phy); guard(mutex)(&tphy->lock); + tphy->usb3_on = false; + tphy->usb3_pipe = false; t6040_usb3_down_locked(tphy); return 0; } @@ -972,6 +990,7 @@ static int t6040_usb3_set_mode(struct phy *phy, enum phy_mode mode, int submode) return -EOPNOTSUPP; guard(mutex)(&tphy->lock); + tphy->usb3_pipe = true; if (!tphy->usb3_up || tphy->pipe_switched) return 0; t6040_usb3_pipe_switch(tphy); @@ -1353,6 +1372,31 @@ static void t6040_dp_drive(struct t6040_usb2_phy *tphy, unsigned int lane, u32 p t6040_usb2_mask32(base + (tx ? 0x100 : 0xa8), 0x7ffff, eq); } +/* + * Give the lanes back to USB3 once the DP session is over and the port has + * left DP alt mode while dwc3 still holds the USB3 phy on: the USB3 bring-up + * programs the PHY afresh, as it does after DP on the next attach, and the + * PIPE returns to the PHY if the controller had asked for it. Called when + * the DP phy powers off and when the mux leaves DP, as either can come last. + * tipd drops the USB role before it leaves DP alt mode, so on an unplug or a + * DP mode change dwc3 powers the USB3 phy off first and this does nothing. + */ +static void t6040_usb3_reclaim_locked(struct t6040_usb2_phy *tphy) +{ + lockdep_assert_held(&tphy->lock); + if (!tphy->usb3_lent || tphy->dp_up || tphy->dp_alt) + return; + tphy->usb3_lent = false; + if (!tphy->usb3_on || tphy->usb3_up) + return; + t6040_usb3_up_locked(tphy); + if (tphy->usb3_up && tphy->usb3_pipe && !tphy->pipe_switched) { + t6040_usb3_pipe_switch(tphy); + tphy->pipe_switched = true; + } + dev_info(tphy->dev, "USB3 %s after DP\n", tphy->usb3_up ? "back" : "not restored"); +} + /* ---- DP phy_ops ---- */ static int t6040_dp_power_on(struct phy *phy) @@ -1368,8 +1412,10 @@ static int t6040_dp_power_on(struct phy *phy) * lanes to DP: aciophy_phy_shutdown of the USB3 side first, then the * same mode-3 init as the fixed DP PHY. */ - if (!t6040_dp_only(tphy)) + if (!t6040_dp_only(tphy)) { t6040_usb3_down_locked(tphy); + tphy->usb3_lent = true; + } ret = t6040_dp_phy_init(tphy, tphy->dp_flip); if (ret) { dev_err(tphy->dev, "DP PHY init failed: %d\n", ret); @@ -1389,6 +1435,7 @@ static int t6040_dp_power_off(struct phy *phy) guard(mutex)(&tphy->lock); tphy->dp_up = false; + t6040_usb3_reclaim_locked(tphy); return 0; } @@ -1506,6 +1553,7 @@ static int t6040_dp_mux_set(struct typec_mux_dev *mux, struct typec_mux_state *s dp ? "DP" : "non-DP", state->mode, alt, tphy->dp_flip ? "reversed" : "normal"); if (alt && tphy->dp_up) t6040_dp_xbar_config(tphy, tphy->dp_flip); + t6040_usb3_reclaim_locked(tphy); return 0; } From 3ff8c49c5f12b908a72e6c076ff0d7336ffddcb8 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 25 Oct 2026 12:55:25 -0400 Subject: [PATCH 062/388] drm/asahi: withhold G17 compute doorbells behind two unretired commands Each compute publication rang its pipe doorbell. The firmware handles a doorbell by reading the whole work ring of the priority class, and a doorbell that arrives while it finishes a command delays that command's completion: writing its end timestamp and stamp, and releasing the commands that wait for it. In the cross-queue hop the stamp followed the GPU end by 11.5 us without a doorbell in that window and by about 24 us with one. Three quarters of the completions had one, because each hop publishes two commands. A command with two or more unretired commands of its queue ahead cannot be the next one to start. Withhold its doorbell, and ring the withheld doorbell from the compute scan once at most one unretired command precedes the first publication it covers. A doorbell rung meanwhile on the same ring announces the work slot earlier. Shallow queues ring as before. Same-boot A/B on Neo, alternated per row, 3 rounds, unpinned, medians: sync.signal queue timeline 30.13 -> 23.60 us per hop (-21.7%) sync.signal queue binary 29.14 -> 23.31 us per hop (-20.0%) compute.light d64 14.55 -> 13.91 us (-4.4%) compute.light d1/d2 unchanged (103.0 / 34.0 us) render.light d1/d32 unchanged Firmware side, from the completion words: GPU end to stamp went from a median of 24.3 us to 12.0 us, and doorbells per command from 1.0 to 0.2. --- drivers/gpu/drm/asahi/g17/queue/compute.rs | 41 ++++++++++++++++++++-- drivers/gpu/drm/asahi/g17/runtime.rs | 10 ++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/queue/compute.rs b/drivers/gpu/drm/asahi/g17/queue/compute.rs index c8e00e2b563c2..5359a732562c4 100644 --- a/drivers/gpu/drm/asahi/g17/queue/compute.rs +++ b/drivers/gpu/drm/asahi/g17/queue/compute.rs @@ -369,6 +369,10 @@ use crate::g17::{ }; use retirement::{Batch, Observation, Recordless, Ticket, IN_FLIGHT}; +/// Position in its queue's active ring from which a publication withholds its doorbell: +/// with two unretired commands ahead, it cannot be the next to start. +const WITHHOLD_DEPTH: usize = 3; + struct Fifo { entries: [Option; IN_FLIGHT], head: usize, @@ -589,9 +593,22 @@ pub(crate) struct Queue { retirement_proved: bool, source_readers: Arc, source_hint: crate::g17::sync_input::ReaderHint, + /// The doorbell withheld from publications that had [`WITHHOLD_DEPTH`] - 1 or more + /// unretired commands of this queue ahead of them, and the publication of the first. + withheld_doorbell: Option<(u64, u64)>, } impl Queue { + /// Returns the withheld doorbell once at most one unretired command of this queue + /// precedes the first publication it covers. + pub(crate) fn due_doorbell(&mut self) -> Option { + let (doorbell, first) = self.withheld_doorbell?; + if self.active.iter().filter(|active| active.published < first).count() > 1 { + return None; + } + self.withheld_doorbell = None; + Some(doorbell) + } /// The caller reserves a QID first, cancels it on construction failure, and retains every /// successfully installed queue until processor stop. Shared VM apertures precede this call. pub(crate) fn new( @@ -656,6 +673,7 @@ impl Queue { retirement_proved: false, source_readers: Arc::new(core::sync::atomic::AtomicU32::new(0), GFP_KERNEL)?, source_hint: crate::g17::sync_input::ReaderHint::new(), + withheld_doorbell: None, }) } pub(crate) fn qid(&self) -> u8 { @@ -1599,6 +1617,7 @@ impl Queue { ring: DescriptorRing, ) -> Result<(u16, u64)> { if !self.can_rearm() { return Err(EAGAIN); } + self.withheld_doorbell = None; self.graph.rearm_ring(ring)?; self.pool.rearm(pages)?; self.pool_phase = PoolPhase::Populated; @@ -2226,12 +2245,30 @@ impl Queue { previous.publication = Some((packet.order.sequence, epoch)); } } - // Every publication rings its own pipe and priority class, as render + // A publication rings its own pipe and priority class, as render // publication does: pipe 2 (compute) in bits 1:0, priority class in // bits 3:2. The generic work doorbell left the firmware to find the // ring itself and delayed dispatch. + // + // A doorbell makes the firmware read the whole work ring of its class, + // and each one it handles while finishing a command delays that + // command's completion and the release of its waiters. A command with + // two or more unretired commands of its queue ahead cannot start soon: + // its doorbell is withheld until at most one precedes it (see + // `due_doorbell`), and any doorbell rung meanwhile on the same ring + // announces its work slot earlier. fence(Ordering::SeqCst); - host.notify((0x83 << 48) | 0x02 | u64::from(policy.priority()) << 2)?; + let doorbell = (0x83 << 48) | 0x02 | u64::from(policy.priority()) << 2; + if self.active.len >= WITHHOLD_DEPTH + && self.withheld_doorbell.is_none_or(|(withheld, _)| withheld == doorbell) + { + self.withheld_doorbell.get_or_insert((doorbell, published)); + } else { + host.notify(doorbell)?; + if self.withheld_doorbell.is_some_and(|(withheld, _)| withheld == doorbell) { + self.withheld_doorbell = None; + } + } crate::g17::trace::publish(crate::g17::trace::COMPUTE, qid, ordinal, &packet); host.note_submission() })(); diff --git a/drivers/gpu/drm/asahi/g17/runtime.rs b/drivers/gpu/drm/asahi/g17/runtime.rs index 7f89a25ae6d35..625339afafeaa 100644 --- a/drivers/gpu/drm/asahi/g17/runtime.rs +++ b/drivers/gpu/drm/asahi/g17/runtime.rs @@ -1661,6 +1661,7 @@ impl super::Firmware { let port = &self.port; let retired = deferred.pending(); let mut freed = false; + let mut doorbells = [0u64; 4]; for mut queue in registry .compute .iter_mut() @@ -1677,6 +1678,15 @@ impl super::Firmware { &mut |event| deferred.push(event), )?; freed |= busy && queue.channel_free(); + // Withheld doorbells are rung once their command is next but one; one per + // priority class announces every queue's work slots on that ring. + if let Some(doorbell) = queue.due_doorbell() { + let class = ((doorbell >> 2) & 3) as usize; + doorbells[class] = doorbell; + } + } + for doorbell in doorbells.into_iter().filter(|doorbell| *doorbell != 0) { + self.primary.notify(doorbell)?; } // All compute observations in this batch are latched before a VM // failure can settle its render peers. Exact successes win first. From b1f5dc6df0ba5650c8424027a025db233c6544c4 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 25 Oct 2026 13:07:37 -0400 Subject: [PATCH 063/388] drm/asahi: drop G17 compute doorbells that another doorbell announced A withheld compute doorbell is rung once its command is next but one. When a doorbell of the same priority class was rung after the last withheld work slot was written, the firmware has already read that slot, so the withheld doorbell repeats an announcement it has handled. Count the compute work slots written per class, and note the count read before each compute doorbell is rung. A due withheld doorbell whose slots are covered by that count is dropped instead of rung. Same-boot A/B on Neo against the withholding alone, alternated per row, 3 rounds, unpinned, medians: sync.signal queue timeline 23.47 -> 22.70 us per hop (-3.3%) sync.signal queue binary 23.41 -> 23.02 us per hop (-1.7%) compute.light d1/d2/d64, render.light d1/d32: unchanged Doorbells per command in the hop went from 0.22 to 0.16. --- drivers/gpu/drm/asahi/g17/port.rs | 32 ++++++++++++++++++++++ drivers/gpu/drm/asahi/g17/queue/compute.rs | 29 ++++++++++++++------ drivers/gpu/drm/asahi/g17/runtime.rs | 16 +++++++++-- 3 files changed, 66 insertions(+), 11 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/port.rs b/drivers/gpu/drm/asahi/g17/port.rs index 8d129c49c640c..db043ef9d6f91 100644 --- a/drivers/gpu/drm/asahi/g17/port.rs +++ b/drivers/gpu/drm/asahi/g17/port.rs @@ -98,6 +98,11 @@ pub(super) struct Port { generation: AtomicU64, /// Device publication order shared by compute and render commands. publications: AtomicU64, + /// Compute work slots written per priority class. + compute_written: [AtomicU64; PRIORITIES], + /// `compute_written` as read before the latest compute doorbell of each class: every slot + /// counted there was in its ring when the firmware handled that doorbell. + compute_announced: [AtomicU64; PRIORITIES], /// Full firmware effort is applied; cleared before an idle clear rechecks `submissions`. effort_full: AtomicBool, submissions: AtomicU64, @@ -147,6 +152,8 @@ impl Port { render <- new_mutex!((), "g17::Port::render"), generation: AtomicU64::new(0), publications: AtomicU64::new(0), + compute_written: [const { AtomicU64::new(0) }; PRIORITIES], + compute_announced: [const { AtomicU64::new(0) }; PRIORITIES], effort_full: AtomicBool::new(false), submissions: AtomicU64::new(0), }), @@ -223,10 +230,35 @@ impl Port { unsafe { (*ring.producer).store(next, Ordering::Relaxed) }; fence(Ordering::SeqCst); reserved[claim.priority] = reserved[claim.priority].saturating_sub(1); + self.compute_written[claim.priority].fetch_add(1, Ordering::AcqRel); claim.armed = false; Ok(next) } + /// Compute work slots written at `priority` so far. + pub(super) fn compute_written(&self, priority: u8) -> u64 { + self.compute_written + .get(usize::from(priority)) + .map_or(u64::MAX, |written| written.load(Ordering::Acquire)) + } + + /// Compute work slots at `priority` that a doorbell has announced. + pub(super) fn compute_announced(&self, priority: u8) -> u64 { + self.compute_announced + .get(usize::from(priority)) + .map_or(0, |announced| announced.load(Ordering::Acquire)) + } + + /// Rings the compute doorbell `message` of `priority`, after noting which slots it + /// announces. + pub(super) fn notify_compute(&self, priority: u8, message: u64) -> Result { + let index = usize::from(priority); + let written = self.compute_written.get(index).ok_or(EINVAL)?.load(Ordering::Acquire); + self.notify(message)?; + self.compute_announced[index].fetch_max(written, Ordering::AcqRel); + Ok(()) + } + /// Taken around every render publication; see `render`. pub(super) fn render_lock(&self) -> MutexGuard<'_, ()> { self.render.lock() diff --git a/drivers/gpu/drm/asahi/g17/queue/compute.rs b/drivers/gpu/drm/asahi/g17/queue/compute.rs index 5359a732562c4..4b6bb8ce8c56f 100644 --- a/drivers/gpu/drm/asahi/g17/queue/compute.rs +++ b/drivers/gpu/drm/asahi/g17/queue/compute.rs @@ -594,15 +594,22 @@ pub(crate) struct Queue { source_readers: Arc, source_hint: crate::g17::sync_input::ReaderHint, /// The doorbell withheld from publications that had [`WITHHOLD_DEPTH`] - 1 or more - /// unretired commands of this queue ahead of them, and the publication of the first. - withheld_doorbell: Option<(u64, u64)>, + /// unretired commands of this queue ahead of them, the publication of the first, and the + /// compute work slots written at its priority once its slot was. + withheld_doorbell: Option<(u64, u64, u64)>, } impl Queue { /// Returns the withheld doorbell once at most one unretired command of this queue - /// precedes the first publication it covers. - pub(crate) fn due_doorbell(&mut self) -> Option { - let (doorbell, first) = self.withheld_doorbell?; + /// precedes the first publication it covers. A doorbell of the same class rung after the + /// last withheld work slot was written announced every one of them: nothing is left to + /// ring. `announced` gives the slots announced per class. + pub(crate) fn due_doorbell(&mut self, announced: impl Fn(u8) -> u64) -> Option { + let (doorbell, first, written) = self.withheld_doorbell?; + if announced(((doorbell >> 2) & 3) as u8) >= written { + self.withheld_doorbell = None; + return None; + } if self.active.iter().filter(|active| active.published < first).count() > 1 { return None; } @@ -1913,6 +1920,8 @@ pub(crate) trait Host { fn publish_outer(&mut self, priority: u8, slot: &crate::g17::fw::channels::WorkSlot) -> Result; fn notify(&mut self, message: u64) -> Result; + fn notify_compute(&mut self, priority: u8, message: u64) -> Result; + fn compute_written(&self, priority: u8) -> u64; fn note_submission(&mut self) -> Result; } @@ -2260,12 +2269,14 @@ impl Queue { fence(Ordering::SeqCst); let doorbell = (0x83 << 48) | 0x02 | u64::from(policy.priority()) << 2; if self.active.len >= WITHHOLD_DEPTH - && self.withheld_doorbell.is_none_or(|(withheld, _)| withheld == doorbell) + && self.withheld_doorbell.is_none_or(|(withheld, _, _)| withheld == doorbell) { - self.withheld_doorbell.get_or_insert((doorbell, published)); + let written = host.compute_written(policy.priority()); + let entry = self.withheld_doorbell.get_or_insert((doorbell, published, written)); + entry.2 = written; } else { - host.notify(doorbell)?; - if self.withheld_doorbell.is_some_and(|(withheld, _)| withheld == doorbell) { + host.notify_compute(policy.priority(), doorbell)?; + if self.withheld_doorbell.is_some_and(|(withheld, _, _)| withheld == doorbell) { self.withheld_doorbell = None; } } diff --git a/drivers/gpu/drm/asahi/g17/runtime.rs b/drivers/gpu/drm/asahi/g17/runtime.rs index 625339afafeaa..90d980bc427c2 100644 --- a/drivers/gpu/drm/asahi/g17/runtime.rs +++ b/drivers/gpu/drm/asahi/g17/runtime.rs @@ -1394,6 +1394,12 @@ impl compute::Host for ComputeHost<'_> { fn notify(&mut self, message: u64) -> Result { self.port.notify(message) } + fn notify_compute(&mut self, priority: u8, message: u64) -> Result { + self.port.notify_compute(priority, message) + } + fn compute_written(&self, priority: u8) -> u64 { + self.port.compute_written(priority) + } fn note_submission(&mut self) -> Result { self.effort.submit(); super::Firmware::apply_shared_effort(self.init, self.effort, self.port, super::power::FULL_EFFORT) @@ -1438,6 +1444,12 @@ impl compute::Host for DirectHost<'_> { fn notify(&mut self, message: u64) -> Result { self.port.notify(message) } + fn notify_compute(&mut self, priority: u8, message: u64) -> Result { + self.port.notify_compute(priority, message) + } + fn compute_written(&self, priority: u8) -> u64 { + self.port.compute_written(priority) + } fn note_submission(&mut self) -> Result { Ok(()) } @@ -1680,13 +1692,13 @@ impl super::Firmware { freed |= busy && queue.channel_free(); // Withheld doorbells are rung once their command is next but one; one per // priority class announces every queue's work slots on that ring. - if let Some(doorbell) = queue.due_doorbell() { + if let Some(doorbell) = queue.due_doorbell(|class| port.compute_announced(class)) { let class = ((doorbell >> 2) & 3) as usize; doorbells[class] = doorbell; } } for doorbell in doorbells.into_iter().filter(|doorbell| *doorbell != 0) { - self.primary.notify(doorbell)?; + self.port.notify_compute(((doorbell >> 2) & 3) as u8, doorbell)?; } // All compute observations in this batch are latched before a VM // failure can settle its render peers. Exact successes win first. From fe44455689f7f2641febb34698b31af53d360bc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Bal=C3=A1zsy?= Date: Sun, 25 Oct 2026 18:11:12 +0100 Subject: [PATCH 064/388] drm/apple: say how desktop 26.x uses the Type-C fabric MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The comment in dcp_is_typec_output() still described the private route the previous commit removed. Desktop 26.x now takes its Type-C route from the fabric and only drives the link through its own compact DPTX connect. Signed-off-by: FrantiÅ¡ek Balázsy --- drivers/gpu/drm/apple/dcp-fabric.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/apple/dcp-fabric.c b/drivers/gpu/drm/apple/dcp-fabric.c index 2ca975e28ffd4..29e106b9c74c0 100644 --- a/drivers/gpu/drm/apple/dcp-fabric.c +++ b/drivers/gpu/drm/apple/dcp-fabric.c @@ -103,7 +103,11 @@ static bool dcp_hdmi_borrowed(void *ctx); bool dcp_is_typec_output(struct apple_dcp *dcp) { - /* Desktop 26.x keeps its own Type-C route, outside this fabric. */ + /* + * Desktop 26.x takes its Type-C route from this fabric but drives the + * link through its own compact DPTX connect (dcp_desktop_dptx_connect()), + * not the Type-C output session below. + */ if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) return false; return dcp->active_typec_route || From d3a119240285042c231bd991a861309825a4929b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Bal=C3=A1zsy?= Date: Sun, 25 Oct 2026 18:41:09 +0100 Subject: [PATCH 065/388] drm/apple: order desktop PHY power against route release, free the pipeline with the sink MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review of the Type-C fabric series found four holes on the T6041 path. dcp_typec_phy_on() looked at the active route under hpd_mutex only, while the deactivate powers the PHY off and releases the crossbar under tb_lock. A connect racing a release could power the PHY after the crossbar was deselected and leave phy_powered set. Take tb_lock around the check and the power-on, and refuse a route whose crossbar is already released. The start-up gate in dcp_start_desktop() is again satisfied by a Type-C route, so a restart after an unplug (dcp->phy is NULL then) still brings the DPTX services up, and the start-up link is only connected when a fixed PHY or an active route exists: an idle pipeline has no powered PHY and no crossbar selection to connect on. When HPD drops while the port stays in DP alt mode, the route, its crossbar and the PHY were kept. Release them with the sink, as a mode change does, so a display on another port is not refused as busy; the route is claimed again when HPD returns. Check that a desktop callback record lies inside the shared buffer before its header is read, and access dcp->xbar with READ_ONCE()/WRITE_ONCE() as it is read by the DPTX worker without a lock. Signed-off-by: FrantiÅ¡ek Balázsy --- drivers/gpu/drm/apple/dcp-fabric.c | 39 +++++++++++++++++++++++++++--- drivers/gpu/drm/apple/dcp.c | 10 ++++++-- drivers/gpu/drm/apple/dptxep.c | 5 ++-- drivers/gpu/drm/apple/iomfb.c | 11 +++++++-- 4 files changed, 55 insertions(+), 10 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp-fabric.c b/drivers/gpu/drm/apple/dcp-fabric.c index 29e106b9c74c0..a01f9431acece 100644 --- a/drivers/gpu/drm/apple/dcp-fabric.c +++ b/drivers/gpu/drm/apple/dcp-fabric.c @@ -338,9 +338,23 @@ static int dcp_typec_route_phy_on(struct apple_dcp_typec_route *route) */ int dcp_typec_phy_on(struct apple_dcp *dcp) { - struct apple_dcp_typec_route *route = READ_ONCE(dcp->active_typec_route); + struct apple_dcp_typec_route *route; - return route ? dcp_typec_route_phy_on(route) : 0; + /* + * Ordered against dcp_typec_route_deactivate(), which powers the PHY + * off and releases the crossbar under tb_lock: a connect racing the + * release either powers the PHY first and has it taken down again, or + * finds the route released and powers nothing. A desktop connect does + * not take the fabric lock, so tb_lock is the common one. + */ + guard(mutex)(&dcp->tb_lock); + route = READ_ONCE(dcp->active_typec_route); + if (!route) + return 0; + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && + route->deactivate_xbar_done) + return -ENODEV; + return dcp_typec_route_phy_on(route); } static void dcp_typec_route_phy_off(struct apple_dcp_typec_route *route) @@ -421,7 +435,7 @@ static int dcp_typec_route_activate(struct apple_dcp_typec_route *route, * a link configuration (dptxport_compact_xbar_link()) through dcp->xbar. */ if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) - dcp->xbar = route->xbar; + WRITE_ONCE(dcp->xbar, route->xbar); dcp->dptx_phy = route->dptx_phy; dcp->connector_type = DRM_MODE_CONNECTOR_USB; WRITE_ONCE(dcp->ext_backlight, false); @@ -607,7 +621,7 @@ static int dcp_typec_route_deactivate(struct apple_dcp_typec_route *route) dcp->phy = dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP ? NULL : dcp->typec_routes[0].phy; if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) - dcp->xbar = NULL; + WRITE_ONCE(dcp->xbar, NULL); dcp->dptx_phy = dcp->typec_routes[0].dptx_phy; dcp->connector_type = DRM_MODE_CONNECTOR_USB; } @@ -1502,6 +1516,23 @@ static int dcp_typec_route_set(struct typec_mux_dev *mux, if (ret && !dcp_is_external_h17p(dcp)) return ret; } + /* + * Desktop 26.x has one Type-C pipeline and powers the PHY with + * the route. A sink that went away while the port stays in DP + * alt mode (a dock whose monitor was unplugged) must give back + * the pipeline, its crossbar and the PHY, or a display on + * another port is refused as busy. The route is taken again + * when HPD returns. + */ + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) { + port->preferred_route = port->owner; + port->hpd = false; + ret = dcp_typec_route_deactivate(port->owner); + if (ret) + return ret; + port->owner = NULL; + dcp_typec_pipeline_freed(); + } } /* did the pairing pass count this port's direct stream so far? */ diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 0563b07f67c1e..ce59933abdec6 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -3048,7 +3048,8 @@ static int dcp_start_desktop_external(struct apple_dcp *dcp, u32 map) if (ret) return ret; - if (!dcp->phy) { + /* An idle Type-C pipeline has no PHY until a route is claimed. */ + if (!dcp->phy && !dcp->nr_typec_routes) { dev_warn(dcp->dev, "no DP PHY and no Type-C route: no display link\n"); return 0; } @@ -3076,7 +3077,12 @@ static int dcp_start_desktop_external(struct apple_dcp *dcp, u32 map) if (ret) return ret; - if (!dptx_ret && dptx_service_available(&dcp->dptxport[0])) { + /* + * A Type-C route powers the PHY and selects the crossbar when it is + * claimed; until then there is no link to bring up. + */ + if (!dptx_ret && dptx_service_available(&dcp->dptxport[0]) && + (dcp->fixed_phy || dcp->active_typec_route)) { ret = dcp_dptx_connect(dcp, 0); if (ret) dev_info(dcp->dev, "no DPTX link at startup: %d\n", ret); diff --git a/drivers/gpu/drm/apple/dptxep.c b/drivers/gpu/drm/apple/dptxep.c index 79b26f7caeb44..0334f98c33189 100644 --- a/drivers/gpu/drm/apple/dptxep.c +++ b/drivers/gpu/drm/apple/dptxep.c @@ -486,11 +486,12 @@ static int dptxport_call_get_link_rate(struct apple_epic_service *service, static int dptxport_compact_xbar_link(struct apple_epic_service *service, bool up) { struct apple_dcp *dcp = service->ep->dcp; + struct mux_control *xbar = READ_ONCE(dcp->xbar); int ret; - if (!READ_ONCE(service->ep->compact) || !dcp->xbar) + if (!READ_ONCE(service->ep->compact) || !xbar) return 0; - ret = dcp_dpxbar_link(dcp->xbar, up); + ret = dcp_dpxbar_link(xbar, up); dev_dbg(dcp->dev, "DPTXPort: crossbar link %s: %d\n", up ? "up" : "down", ret); return ret; } diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index 924139aad4c2d..b8187ada77238 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -1284,14 +1284,21 @@ static void dcpep_handle_cb(struct apple_dcp *dcp, enum dcp_context_id context, return; } + /* The header is read below: check the record lies in the shared buffer first. */ + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && + (length < sizeof(*hdr) || offset >= 0x8000 || length > 0x8000 - offset)) { + dev_err(dev, "invalid IOMFB callback envelope\n"); + dcp->crashed = true; + return; + } + tag = dcp_parse_tag(hdr->tag); handled = tag >= 0 && tag < IOMFB_MAX_CB && dcp->cb_handlers && dcp->cb_handlers[tag]; if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && handled && - (length < sizeof(*hdr) || - (u64)sizeof(*hdr) + hdr->in_len + hdr->out_len > length || + ((u64)sizeof(*hdr) + hdr->in_len + hdr->out_len > length || !iomfb_check_callback_v26_6_desktop(tag, hdr->in_len, hdr->out_len))) { dev_err(dev, "short IOMFB callback %d (%u, %u, length %u)\n", tag, hdr->in_len, hdr->out_len, length); From 361d633777bbb122fa5080018c2ddfd111f5e2e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Bal=C3=A1zsy?= Date: Sun, 25 Oct 2026 18:41:09 +0100 Subject: [PATCH 066/388] selftests: apple-dcp: follow the desktop 26.6 fabric and callback checks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fabric extraction now reaches dcp_typec_route_phy_off() and the DCP_FIRMWARE_V_26_6_DESKTOP tests, and the dispatcher extraction the desktop callback check. Add the define and, as for the other desktop paths, a stub that asserts if a non-desktop scenario reaches the check; the PHY power-off is a no-op there as the real one returns for an unpowered PHY. Seven of the eight units that stopped compiling build and pass again; mux-release-stages needs -Wno-unused-but-set-global with clang 23. Signed-off-by: FrantiÅ¡ek Balázsy --- tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py | 4 ++++ tools/testing/selftests/drivers/gpu/apple-dcp/quiesce.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py index 6b088b1be6ccd..2a1d93d087c06 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py @@ -1830,6 +1830,7 @@ def dispatcher(root): typedef uint32_t u32; typedef uint64_t u64; #define DCP_FIRMWARE_H17P 1 +#define DCP_FIRMWARE_V_26_6_DESKTOP 100 #define DCP_IOMFB_METHODS_H17P_EXTERNAL 2 #define DCP_IOMFB_METHODS_H17G 3 #define DCP_MAX_CALL_DEPTH 8 @@ -1859,6 +1860,9 @@ def dispatcher(root): static struct dcp_channel *dcp_get_channel(struct apple_dcp *d, enum dcp_context_id c) { (void)c; return &d->channel; } static int dcp_parse_tag(char *p) { (void)p; return case_tag; } +/* A desktop 26.6 callback is not under test here: no scenario selects that firmware. */ +static bool __attribute__((unused)) iomfb_check_callback_v26_6_desktop(int tag, u32 in_len, u32 out_len) +{ (void)tag; (void)in_len; (void)out_len; assert(!"desktop 26.6 callback in an H17P scenario"); return false; } static bool of_machine_is_compatible(const char *n) { (void)n; return machine; } static bool of_device_is_compatible(struct device_node *n, const char *s) { (void)s; return n->external; } diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/quiesce.py b/tools/testing/selftests/drivers/gpu/apple-dcp/quiesce.py index 46ea2900dcade..34e17a0765efd 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/quiesce.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/quiesce.py @@ -80,6 +80,10 @@ def make_unit(source, parent=False, reference=None): static struct drm_device *dcp_typec_drm(void) { return NULL; } static void dcp_typec_plan(struct drm_device *d,struct apple_dcp_typec_port *p,unsigned n) { (void)d;(void)p;(void)n; } static bool dcp_typec_tunnel_held(struct apple_dcp *d) { (void)d;return false; } +/* The desktop 26.6 (T6041) Type-C path is not under test: nothing here is on that firmware. */ +#define DCP_FIRMWARE_V_26_6_DESKTOP 100 +static void __attribute__((unused)) dcp_typec_route_phy_off(struct apple_dcp_typec_route *r) +{ (void)r; /* never powered: the real one returns at once */ } static int dcp_typec_route_deactivate(struct apple_dcp_typec_route *r) { deactivate_calls++;r->selected=false;return 0; } static void dcp_typec_port_attach(struct apple_dcp_typec_port *p) { (void)p; } static int gpiod_get_value_cansleep(void *p) { (void)p;return 0; } From d38cce2d8be54bdbc01e90e1a92ef3dd65ac0640 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 25 Oct 2026 13:42:20 -0400 Subject: [PATCH 067/388] drm/asahi: announce withheld G17 compute doorbells whenever retirement stalls A withheld compute doorbell waited for the host to retire all but one of the commands ahead of it. Retirement can stall even though the firmware has finished those commands, for example while a completion record waits up to 50 ms for its stamp. It can also leave ordinary operation through a timeout, kill, quarantine, replay, recovery witness or exit. The firmware then sat idle on a command it had never been told about. Count only predecessors that have no completion record yet. Ring a withheld doorbell at once from the compute scan, and from the timeout and cancellation paths, whenever the queue is settling. Never withhold while it is settling. The firmware then knows every published command, as it does without withholding. --- drivers/gpu/drm/asahi/g17/queue/compute.rs | 42 ++++++++++++++++--- .../drm/asahi/g17/runtime/compute_control.rs | 14 ++++++- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/queue/compute.rs b/drivers/gpu/drm/asahi/g17/queue/compute.rs index 4b6bb8ce8c56f..9382d50d90b79 100644 --- a/drivers/gpu/drm/asahi/g17/queue/compute.rs +++ b/drivers/gpu/drm/asahi/g17/queue/compute.rs @@ -600,22 +600,53 @@ pub(crate) struct Queue { } impl Queue { - /// Returns the withheld doorbell once at most one unretired command of this queue - /// precedes the first publication it covers. A doorbell of the same class rung after the - /// last withheld work slot was written announced every one of them: nothing is left to - /// ring. `announced` gives the slots announced per class. + /// Returns the withheld doorbell once at most one command of this queue that precedes the + /// first publication it covers has no completion record yet. A doorbell of the same class + /// rung after the last withheld work slot was written announced every one of them: nothing + /// is left to ring. `announced` gives the slots announced per class. + /// + /// Retirement waiting for a stamp does not hold the doorbell back, and any departure from + /// ordinary retirement (timeout, kill, quarantine, replay, recovery or exit) rings it at + /// once: the firmware then sees every published command, as without withholding. pub(crate) fn due_doorbell(&mut self, announced: impl Fn(u8) -> u64) -> Option { let (doorbell, first, written) = self.withheld_doorbell?; if announced(((doorbell >> 2) & 3) as u8) >= written { self.withheld_doorbell = None; return None; } - if self.active.iter().filter(|active| active.published < first).count() > 1 { + let settling = self.settling(); + let incomplete = self + .active + .iter() + .filter(|active| active.published < first && active.observed.is_none()) + .count(); + if !settling && incomplete > 1 { return None; } self.withheld_doorbell = None; Some(doorbell) } + + /// The queue has left ordinary retirement: a timeout, kill, quarantine, replay, recovery + /// witness or exit is in progress. + fn settling(&self) -> bool { + self.quarantined + || self.released + || self.exit.is_some() + || self.replays.len != 0 + || self.replay_retirement + || self.awaiting_witness + || self.retire_pending + || self.retired_by_teardown + || self.spared_quarantine + || self.failure_status_pending + || self.terminal.is_some() + } + + /// Takes the withheld doorbell regardless of its predecessors. + pub(crate) fn take_withheld_doorbell(&mut self) -> Option { + self.withheld_doorbell.take().map(|(doorbell, _, _)| doorbell) + } /// The caller reserves a QID first, cancels it on construction failure, and retains every /// successfully installed queue until processor stop. Shared VM apertures precede this call. pub(crate) fn new( @@ -2269,6 +2300,7 @@ impl Queue { fence(Ordering::SeqCst); let doorbell = (0x83 << 48) | 0x02 | u64::from(policy.priority()) << 2; if self.active.len >= WITHHOLD_DEPTH + && !self.settling() && self.withheld_doorbell.is_none_or(|(withheld, _, _)| withheld == doorbell) { let written = host.compute_written(policy.priority()); diff --git a/drivers/gpu/drm/asahi/g17/runtime/compute_control.rs b/drivers/gpu/drm/asahi/g17/runtime/compute_control.rs index 4f519743aed53..42282aea62023 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/compute_control.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/compute_control.rs @@ -69,7 +69,14 @@ impl Backend { if !queue.owns_packet(packet) { return Ok(None); } - queue.timeout(&mut |event| deferred.push(event)) + let status = queue.timeout(&mut |event| deferred.push(event)); + // A timed-out queue leaves ordinary retirement: announce what it withheld. + let withheld = queue.take_withheld_doorbell(); + drop(queue); + if let Some(doorbell) = withheld { + firmware.port.notify_compute(((doorbell >> 2) & 3) as u8, doorbell)?; + } + status })(); if result.as_ref().is_ok_and(|status| status.is_some()) { // VM status and peer settlement precede signalling this timeout. The backend @@ -92,6 +99,11 @@ impl Backend { firmware.scan_compute(None, &mut deferred)?; if let Some(mut queue) = firmware.queues.compute_mut(self.owner) { queue.cancel(packet, &mut |event| deferred.push(event))?; + let withheld = queue.take_withheld_doorbell(); + drop(queue); + if let Some(doorbell) = withheld { + firmware.port.notify_compute(((doorbell >> 2) & 3) as u8, doorbell)?; + } } Ok(()) })(); From 853a8d6451b22bb9c9c223141d2caa3f83c607dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Bal=C3=A1zsy?= Date: Sun, 25 Oct 2026 19:39:47 +0100 Subject: [PATCH 068/388] drm/apple: connect desktop 26.x Type-C displays through the Type-C output session MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A desktop 26.x Type-C display took its route from the fabric but was connected by dcp_desktop_dptx_connect(), outside the session the other SoCs use: no attach and detach bookkeeping (cable flag, generation, firmware metadata, unplug count), no relink, no reconnect retry and none of the IRQ_HPD handling the fabric already dispatches to it. dcp_is_typec_output() is no longer false for the desktop firmware, and dcp_dptx_connect(), dcp_dptx_connect_oob() and dcp_dptx_disconnect_oob() take the common path for it. What compact DPTX does differently stays where it differs: - the connection validation call does not exist, so it is skipped; - the PHY is powered when the display connects, before the DPTX connect; - a link configuration that has not arrived when the wait ends is not an error, it comes once a sink arrives; - a disconnect marks the external pipe as shut down by the firmware. The fixed HDMI output (dcpext0) has no Type-C session and keeps dcp_desktop_dptx_connect(). Signed-off-by: FrantiÅ¡ek Balázsy --- drivers/gpu/drm/apple/dcp-fabric.c | 7 ------ drivers/gpu/drm/apple/dcp.c | 34 +++++++++++++++++++++++------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp-fabric.c b/drivers/gpu/drm/apple/dcp-fabric.c index a01f9431acece..37a789a5341fb 100644 --- a/drivers/gpu/drm/apple/dcp-fabric.c +++ b/drivers/gpu/drm/apple/dcp-fabric.c @@ -103,13 +103,6 @@ static bool dcp_hdmi_borrowed(void *ctx); bool dcp_is_typec_output(struct apple_dcp *dcp) { - /* - * Desktop 26.x takes its Type-C route from this fabric but drives the - * link through its own compact DPTX connect (dcp_desktop_dptx_connect()), - * not the Type-C output session below. - */ - if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) - return false; return dcp->active_typec_route || dcp->fixed_connector_type == DRM_MODE_CONNECTOR_USB; } diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index ce59933abdec6..8b156b0f585db 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -1342,6 +1342,7 @@ static int dcp_dptx_connect_raw(struct apple_dcp *dcp, u32 port, bool *retryable { /* T8122 dcpext: HPD only after the link wait, see dcp_ext_dptx_607() */ bool late_hpd = dcp_ext_dptx_607(dcp); + bool compact; unsigned long timeout; u64 generation; u8 dfp_port; @@ -1367,6 +1368,8 @@ static int dcp_dptx_connect_raw(struct apple_dcp *dcp, u32 port, bool *retryable dcp->connector_type, dcp->dptxport[port].connected); mutex_lock(&dcp->hpd_mutex); + compact = dptx_service_available(&dcp->dptxport[port]) && + READ_ONCE(dcp->dptxport[port].service->ep->compact); if (!dcp_connect_session_valid_locked(dcp, session, cable, recovery)) { ret = -ESTALE; goto out_unlock; @@ -1395,9 +1398,14 @@ static int dcp_dptx_connect_raw(struct apple_dcp *dcp, u32 port, bool *retryable reinit_completion(&dcp->dptxport[port].linkcfg_completion); WRITE_ONCE(dcp->dptxport[port].tile_hint, false); dcp->dptxport[port].atcphy = dcp->phy; + /* A desktop 26.x route powers its PHY when the display connects. */ + ret = dcp_typec_phy_on(dcp); + if (ret) + goto out_unlock; /* a tiled display's second half comes in on the port's dpin1 */ dfp_port = port && READ_ONCE(dcp->split.active) ? 2 : dcp->dptx_dfp_port; - if (!dcp_ext_dptx_607(dcp)) { + /* Compact DPTX (desktop 26.x) has no connection validation call. */ + if (!dcp_ext_dptx_607(dcp) && !compact) { ret = dptxport_validate_connection(dcp->dptxport[port].service, dfp_port, dcp->dptx_phy, dcp->dptx_die); @@ -1472,7 +1480,12 @@ static int dcp_dptx_connect_raw(struct apple_dcp *dcp, u32 port, bool *retryable /* A revoked wait must not disconnect or publish readiness for a new session. */ if (!dcp_connect_session_valid(dcp, session, cable, recovery)) return -ESTALE; - if (!ret && late_hpd) { + if (!ret && compact) { + /* Without a sink the link is configured once one arrives. */ + dev_dbg(dcp->dev, + "dcp_dptx_connect: no link configuration on port %u yet\n", port); + ret = 0; + } else if (!ret && late_hpd) { /* Without HPD this firmware does not train: assert it now. */ dev_dbg(dcp->dev, "dcp_dptx_connect: no link configuration on port %u yet, asserting HPD\n", @@ -2025,7 +2038,8 @@ int dcp_dptx_connect(struct apple_dcp *dcp, u32 port) struct dcp_fabric_session session = {}; int ret; - if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) + /* The fixed HDMI output of a desktop 26.x processor has no Type-C session. */ + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && !dcp_is_typec_output(dcp)) return dcp_desktop_dptx_connect(dcp, port); if (!dcp_is_external_h17p(dcp)) { @@ -2612,7 +2626,7 @@ int dcp_dptx_connect_oob(struct platform_device *pdev, u32 port) { struct apple_dcp *dcp = platform_get_drvdata(pdev); - if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && !dcp_is_typec_output(dcp)) return dcp_desktop_dptx_connect(dcp, port); if (dcp_is_external_h17p(dcp)) { @@ -2655,8 +2669,9 @@ int dcp_dptx_connect_oob(struct platform_device *pdev, u32 port) int dcp_dptx_disconnect_oob(struct platform_device *pdev, u32 port) { struct apple_dcp *dcp = platform_get_drvdata(pdev); + int ret; - if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && !dcp_is_typec_output(dcp)) return dcp_desktop_dptx_disconnect_oob(dcp, port); if (dcp_is_external_h17p(dcp)) @@ -2684,13 +2699,16 @@ int dcp_dptx_disconnect_oob(struct platform_device *pdev, u32 port) av_service_disconnect(dcp); if (dptx_service_available(&dcp->dptxport[port])) { - int ret = dptxport_set_hpd(dcp->dptxport[port].service, false); - + ret = dptxport_set_hpd(dcp->dptxport[port].service, false); if (ret) return ret; } - return dcp_dptx_disconnect(dcp, port); + ret = dcp_dptx_disconnect(dcp, port); + /* The firmware shuts a desktop 26.x external pipe down with its display. */ + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && !dcp->main_display) + dcp->desktop->ext_pipe_off = true; + return ret; } /* From 6dc79aa1b335048b7cfcf91e45bc67314598acaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Bal=C3=A1zsy?= Date: Sun, 25 Oct 2026 19:45:14 +0100 Subject: [PATCH 069/388] selftests: apple-dcp: the fast-replug scenario is not compact DPTX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dcp_dptx_connect_raw() now asks the DPTX endpoint whether it is compact and has the route power the PHY. This scenario's endpoint is the classic one and its PHY needs no route: report not compact and stub the PHY power-on. Signed-off-by: FrantiÅ¡ek Balázsy --- tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py index 2a1d93d087c06..ce5ac6e47d647 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py @@ -490,6 +490,8 @@ def bounce(root): m = re.search(r'^static int dcp_dptx_connect_raw\(', dcp, re.M) assert m, 'dcp_dptx_connect_raw' raw = block(dcp, m.start()) + # Compact DPTX is the desktop 26.x wire; this scenario's endpoint is not. + raw = raw.replace('READ_ONCE(dcp->dptxport[port].service->ep->compact)', 'false') def swap(old, new): nonlocal unit @@ -510,6 +512,8 @@ def swap(old, new): swap(old_raw, 'static int dcp_dptx_connect_raw(struct apple_dcp *, u32, bool *' + (', const struct dcp_fabric_session *, bool, bool' if sessions else '') + ');') swap('static int dptxport_set_hpd(void *p, bool on)\n{ (void)p; assert(!on); return hpd_error; }', + '/* The route powers a desktop 26.x PHY at connect: nothing to do for this scenario. */\n' + 'static int __attribute__((unused)) dcp_typec_phy_on(struct apple_dcp *d) { (void)d; return 0; }\n' 'static bool fw_hpd, fw_display, link_trains;\n' 'static int hpd_drops, connects, release_error_once, assert_error;\n' '/* A request that times out may still be carried out; a refused one is not. */\n' From 26f5eb68dbce039de470b789d754d2ec3541c15e Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 06:37:44 +1100 Subject: [PATCH 070/388] drm/asahi: admit the alternate T6040 loaded 25G83 image Keep a list of admitted images on each versioned firmware Build and pass that list to fwid::identify(). Version selection uses the first matching Build, so another Build for macOS 26.6.2 would leave its image unreachable. Add the second normalized T6040 identity measured over two J616s boots. The boots differ only in their entropy payloads. Compared with the qualified 25G83 image, the remaining differences are the field at 0x23d and the iBoot-filled patchbay/tunables area. The executable code outside those areas and DATA are unchanged. This follows the J713 precedent in #442, while keeping whole-TEXT hashing and exact identity checks. Factor the existing lookup so KUnit can exercise the real T6040 and J713 lists with digest measurements, without embedding firmware. Cover both T6040 identities, both J713 identities, unknown digests and size/offset mismatches. Keep the entropy-normalization tests. Link: https://github.com/aurora-silicon/linux/pull/442 Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/asahi/fwid.rs | 98 ++++++++++++++++++++++--- drivers/gpu/drm/asahi/g17/fw/variant.rs | 4 +- drivers/gpu/drm/asahi/g17/mod.rs | 4 +- drivers/gpu/drm/asahi/hw/t6040.rs | 47 ++++++++---- 4 files changed, 127 insertions(+), 26 deletions(-) diff --git a/drivers/gpu/drm/asahi/fwid.rs b/drivers/gpu/drm/asahi/fwid.rs index 36a6fa45581be..bb1fd3c36f623 100644 --- a/drivers/gpu/drm/asahi/fwid.rs +++ b/drivers/gpu/drm/asahi/fwid.rs @@ -103,6 +103,20 @@ fn find_entropy(image: &[u8]) -> Option<[u32; 2]> { Some(found) } +/// Finds an exact identity, measuring each candidate at its own entropy offsets. +fn lookup( + builds: &'static [FirmwareBuild], + text_size: u64, + data_size: u64, + mut digest: impl FnMut([u32; 2]) -> Option<[u8; 32]>, +) -> Option<&'static FirmwareBuild> { + builds.iter().find(|build| { + build.text_size == text_size + && build.data_size == data_size + && digest(build.entropy) == Some(build.text_sha256) + }) +} + /// Identifies the firmware build loaded at `text` (DATA `data_size` bytes) among `builds`, /// before any firmware-visible write. pub(crate) fn identify( @@ -118,19 +132,20 @@ pub(crate) fn identify( dev_err!(dev, "GPU firmware TEXT changed while it was measured\n"); return Err(EAGAIN); } - for build in builds { - if build.text_size == size as u64 - && build.data_size == data_size - && digest_at(&mut copy, build.entropy) == Some(build.text_sha256) - { - dev_info!(dev, "GPU firmware build: {}\n", build.name); - return Ok(build); - } + if let Some(build) = lookup(builds, size as u64, data_size, |at| { + digest_at(&mut copy, at) + }) { + dev_info!(dev, "GPU firmware build: {}\n", build.name); + return Ok(build); } let Some((entropy, digest)) = find_entropy(©).and_then(|at| Some((at, digest_at(&mut copy, at)?))) else { - dev_err!(dev, "unsupported GPU firmware build: TEXT {:#x} has no entropy records\n", size); + dev_err!( + dev, + "unsupported GPU firmware build: TEXT {:#x} has no entropy records\n", + size + ); return Err(EINVAL); }; dev_err!( @@ -185,4 +200,69 @@ mod tests { assert_eq!(image, original); assert_eq!(find_entropy(&image), None); } + + fn measured( + builds: &'static [FirmwareBuild], + data_size: u64, + entropy: [u32; 2], + digest: [u8; 32], + ) -> Option<&'static FirmwareBuild> { + lookup(builds, 0x60000, data_size, |at| { + (at == entropy).then_some(digest) + }) + } + + #[test] + fn t6040_accepts_both_loaded_images() { + let crate::g17::fw::variant::Firmware::Identified(builds) = + crate::hw::t6040::CONFIG.firmware + else { + panic!("T6040 must identify its loaded firmware"); + }; + let build = builds.iter().find(|b| b.os_version == [26, 6, 2]).unwrap(); + let digests = [ + [ + 0x03, 0xdf, 0x75, 0x48, 0x4a, 0x7f, 0xdd, 0x6e, 0x4b, 0x47, 0x7f, 0x95, 0x6c, 0xaa, + 0x5b, 0x44, 0x06, 0xdf, 0xff, 0x4d, 0x19, 0x17, 0x28, 0x62, 0x7f, 0x53, 0x10, 0xe0, + 0x63, 0x87, 0x6d, 0xcd, + ], + [ + 0x19, 0x0c, 0xef, 0x19, 0x46, 0xa2, 0x3d, 0xfb, 0xde, 0x18, 0x05, 0x92, 0x62, 0x96, + 0x31, 0xc3, 0xb2, 0x8d, 0x05, 0x09, 0xa1, 0x7e, 0xb7, 0x61, 0x08, 0xb7, 0xc9, 0xa5, + 0x2a, 0xcc, 0xd9, 0xac, + ], + ]; + for (i, digest) in digests.into_iter().enumerate() { + let found = measured(build.images, 0x138000, [0x59839, 0x5991c], digest).unwrap(); + assert!(core::ptr::eq(found, &build.images[i])); + assert!(measured(build.images, 0x13c000, [0x59839, 0x5991c], digest).is_none()); + assert!(measured(build.images, 0x138000, [0x5c8df, 0x5c9c2], digest).is_none()); + assert!(lookup(build.images, 0x60001, 0x138000, |_| Some(digest)).is_none()); + } + assert!(measured(build.images, 0x138000, [0x59839, 0x5991c], [0; 32]).is_none()); + } + + #[cfg(CONFIG_DRM_ASAHI_G16)] + #[test] + fn j713_accepts_both_loaded_images() { + let builds = crate::g16::hw::t8132::CONFIG.firmware; + let digests = [ + [ + 0x5f, 0x65, 0xf6, 0x14, 0x58, 0x72, 0x54, 0x70, 0xb4, 0xb0, 0xc0, 0x20, 0x28, 0x47, + 0x7b, 0xf2, 0xea, 0x57, 0x3a, 0xfb, 0x4e, 0x0c, 0x49, 0x6e, 0xf9, 0x95, 0xe9, 0x6a, + 0xff, 0xb0, 0xd8, 0x6c, + ], + [ + 0x3b, 0x36, 0x8e, 0xe4, 0x2d, 0xcc, 0xc6, 0x26, 0xe8, 0x49, 0x67, 0xff, 0x40, 0x87, + 0x64, 0x71, 0x0a, 0x64, 0xbf, 0xf1, 0x59, 0x4f, 0x3c, 0xfd, 0x2c, 0x6e, 0x4d, 0x34, + 0xd0, 0x65, 0x24, 0x8b, + ], + ]; + for (i, digest) in digests.into_iter().enumerate() { + let found = measured(builds, 0x13c000, [0x5c8df, 0x5c9c2], digest).unwrap(); + assert!(core::ptr::eq(found, &builds[i])); + assert!(measured(builds, 0x138000, [0x5c8df, 0x5c9c2], digest).is_none()); + } + assert!(measured(builds, 0x13c000, [0x5c8df, 0x5c9c2], [0; 32]).is_none()); + } } diff --git a/drivers/gpu/drm/asahi/g17/fw/variant.rs b/drivers/gpu/drm/asahi/g17/fw/variant.rs index 59f1bb4714fc0..d2d6d2404dfc8 100644 --- a/drivers/gpu/drm/asahi/g17/fw/variant.rs +++ b/drivers/gpu/drm/asahi/g17/fw/variant.rs @@ -71,8 +71,8 @@ pub(crate) struct Build { pub(crate) os_version: [u32; 3], /// Name, for the log. pub(crate) name: &'static str, - /// The loaded image, measured from the `fw-text` region. - pub(crate) image: crate::fwid::FirmwareBuild, + /// The admitted loaded images, measured from the `fw-text` region. + pub(crate) images: &'static [crate::fwid::FirmwareBuild], /// Layouts and rules of the build. pub(crate) variant: &'static Variant, } diff --git a/drivers/gpu/drm/asahi/g17/mod.rs b/drivers/gpu/drm/asahi/g17/mod.rs index 897a982b3728a..ae78efaad1a95 100644 --- a/drivers/gpu/drm/asahi/g17/mod.rs +++ b/drivers/gpu/drm/asahi/g17/mod.rs @@ -1117,7 +1117,7 @@ impl Gpu { Err(ENODEV) } - /// Checks that the loaded TEXT (the `fw-text` region) is `build`'s image, with the DATA size + /// Checks that the loaded TEXT (the `fw-text` region) is one of `build`'s images, with the DATA size /// of the `fw-data` region. The version property only says which build to expect. fn identify_image(dev: &AsahiDrmDevice, build: &'static fw::variant::Build) -> Result { let node = dev.as_ref().as_ref().of_node().ok_or(EINVAL)?; @@ -1129,7 +1129,7 @@ impl Gpu { dev.as_ref().as_ref(), start..end, data.size(), - core::slice::from_ref(&build.image), + build.images, ) .inspect_err(|e| dev_err!(dev.as_ref(), "GPU firmware image not admitted: {:?}\n", e))?; Ok(()) diff --git a/drivers/gpu/drm/asahi/hw/t6040.rs b/drivers/gpu/drm/asahi/hw/t6040.rs index 8df2cff4f6541..0007fdf132949 100644 --- a/drivers/gpu/drm/asahi/hw/t6040.rs +++ b/drivers/gpu/drm/asahi/hw/t6040.rs @@ -62,17 +62,30 @@ pub(crate) const CONFIG: Config = Config { firmware: fw::variant::Firmware::Identified(&[fw::variant::Build { os_version: [26, 6, 2], name: "macOS 26.6.2 (25G83, iBoot mBoot-18000.161.10)", - image: crate::fwid::FirmwareBuild { - name: "Apple GFX 25G83 for T6040 (normalized loaded image)", - text_size: 0x60000, - data_size: 0x138000, - entropy: [0x59839, 0x5991c], - text_sha256: [ - 0x03, 0xdf, 0x75, 0x48, 0x4a, 0x7f, 0xdd, 0x6e, 0x4b, 0x47, 0x7f, 0x95, 0x6c, 0xaa, - 0x5b, 0x44, 0x06, 0xdf, 0xff, 0x4d, 0x19, 0x17, 0x28, 0x62, 0x7f, 0x53, 0x10, 0xe0, - 0x63, 0x87, 0x6d, 0xcd, - ], - }, + images: &[ + crate::fwid::FirmwareBuild { + name: "Apple GFX 25G83 for T6040 (normalized loaded image)", + text_size: 0x60000, + data_size: 0x138000, + entropy: [0x59839, 0x5991c], + text_sha256: [ + 0x03, 0xdf, 0x75, 0x48, 0x4a, 0x7f, 0xdd, 0x6e, 0x4b, 0x47, 0x7f, 0x95, 0x6c, + 0xaa, 0x5b, 0x44, 0x06, 0xdf, 0xff, 0x4d, 0x19, 0x17, 0x28, 0x62, 0x7f, 0x53, + 0x10, 0xe0, 0x63, 0x87, 0x6d, 0xcd, + ], + }, + crate::fwid::FirmwareBuild { + name: "Apple GFX 25G83 for T6040 (normalized loaded image, alternate J616s fill)", + text_size: 0x60000, + data_size: 0x138000, + entropy: [0x59839, 0x5991c], + text_sha256: [ + 0x19, 0x0c, 0xef, 0x19, 0x46, 0xa2, 0x3d, 0xfb, 0xde, 0x18, 0x05, 0x92, 0x62, + 0x96, 0x31, 0xc3, 0xb2, 0x8d, 0x05, 0x09, 0xa1, 0x7e, 0xb7, 0x61, 0x08, 0xb7, + 0xc9, 0xa5, 0x2a, 0xcc, 0xd9, 0xac, + ], + }, + ], variant: &fw::variant::V25G83, }]), // Measured on the 20-core M4 Pro; other core counts are refused at probe. @@ -122,7 +135,11 @@ const fn instances(window: RegisterWindow, instances: u32, phys_stride: u64) -> /// controllers (ADT /arm-io/mcc), slot 20 the four AFR blocks. pub(crate) const REGISTER_WINDOWS: [RegisterWindow; 25] = [ window(0, 0x5_0241_4000, 0x20_0000, 0x4000, 0, 2), - instances(window(3, 0x2_201c_4000, 0x20_8000, 0x1_8000, 0, 2), 4, 0x200_0000), + instances( + window(3, 0x2_201c_4000, 0x20_8000, 0x1_8000, 0, 2), + 4, + 0x200_0000, + ), window(4, 0x5_0244_c000, 0x26_c000, 0x4000, 0, 0), window(9, 0x5_003d_0000, 0x27_4000, 0x4000, 0, 2), window(10, 0x5_003c_0000, 0x27_c000, 0x4000, 0, 0), @@ -131,7 +148,11 @@ pub(crate) const REGISTER_WINDOWS: [RegisterWindow; 25] = [ window(14, 0x5_0040_0000, 0x29_4000, 0x1_0000, 0, 0), window(15, 0x5_0cdc_0000, 0x2a_8000, 0x4000, 0, 0), window(17, 0x2_8800_0000, 0x2b_0000, 0x2_1400, 0, 2), - instances(window(20, 0x2_8b00_0000, 0x2d_8000, 0x4_0000, 0, 2), 4, 0x20_0000), + instances( + window(20, 0x2_8b00_0000, 0x2d_8000, 0x4_0000, 0, 2), + 4, + 0x20_0000, + ), window(22, 0x2_8900_0000, 0x3d_c000, 0x8000, 0, 2), window(26, 0x2_88d0_4000, 0x3e_8000, 0x8000, 0xd0_4000, 2), window(28, 0x2_88d5_8000, 0x3f_4000, 0x8000, 0xd5_8000, 2), From da83e551d4032b2df121a9b5ad98554fb3f074c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Bal=C3=A1zsy?= Date: Sun, 25 Oct 2026 21:09:56 +0100 Subject: [PATCH 071/388] drm/apple: answer the desktop 26.6 piodma unmap and descriptor release callbacks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Setting 50, 59.94 or 48 Hz on the J616s internal panel logged "received unknown callback D202 (24, 0)" and timed out in set_digital_out_mode (reported on #446). A mode change makes the desktop 26.6 firmware retire the piodma buffers it mapped for the change with unmap_piodma (D202), then their descriptors with release_mem_desc (D454). The desktop callback table had neither, so the first stayed unanswered and every later modeset timed out. D202 is not the record the older firmware sends: the buffer id and the id again, then the DVA unaligned at offset 12, then one word. Reading it as struct dcp_unmap_buf_resp makes the DVA check fail and leaves the piodma translation standing. Decode it and unmap that. D454 carries a 32-bit descriptor id and takes a 32-bit result back (the older firmware returned a byte); run the existing release handler and return its result as 32 bits. D454 alone, with the piodma buffers still mapped, made the firmware crash ("RTKit: co-processor has crashed"); with D202 unmapping first, both together run clean. Signed-off-by: FrantiÅ¡ek Balázsy --- drivers/gpu/drm/apple/iomfb_v26_6_desktop.c | 45 +++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c b/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c index 98a3504a9a5f0..05417d2c2b2cb 100644 --- a/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c +++ b/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c @@ -91,6 +91,48 @@ static bool trampoline_passthrough_dict_v26_6(struct apple_dcp *dcp, int tag, return true; } +/* + * unmap_piodma (D202): the buffer id, the id again, the DVA (unaligned) and a + * trailing word. The older firmware sends the DVA after a virtual address. + */ +struct dcp_unmap_buf_req_v26_6 { + __le64 buffer; + __le32 id; + __le64 dva; + __le32 unk; +} __packed; +static_assert(sizeof(struct dcp_unmap_buf_req_v26_6) == 24); + +static bool trampoline_unmap_piodma_v26_6(struct apple_dcp *dcp, int tag, + void *out, void *in) +{ + const struct dcp_unmap_buf_req_v26_6 *wire = in; + struct dcp_unmap_buf_resp request = { + .buffer = get_unaligned_le64(&wire->buffer), + .dva = get_unaligned_le64(&wire->dva), + }; + + trace_iomfb_callback(dcp, tag, "dcpep_cb_unmap_piodma"); + dcpep_unmap_piodma(dcp, &request); + + return true; +} + +/* + * release_mem_desc (D454): the firmware sends the 32-bit descriptor id and + * wants a 32-bit result, where the older firmware took a byte. + */ +static bool trampoline_release_mem_desc_v26_6(struct apple_dcp *dcp, int tag, + void *out, void *in) +{ + u32 id = get_unaligned_le32(in); + + trace_iomfb_callback(dcp, tag, "dcpep_cb_release_mem_desc"); + put_unaligned_le32(dcpep_cb_release_mem_desc(dcp, &id), out); + + return true; +} + static const iomfb_cb_handler cb_handlers[IOMFB_MAX_CB] = { [0] = trampoline_true, /* did_boot_signal */ [1] = trampoline_true, /* did_power_on_signal */ @@ -120,6 +162,8 @@ static const iomfb_cb_handler cb_handlers[IOMFB_MAX_CB] = { [129] = trampoline_prop_end, [134] = trampoline_zero, [201] = trampoline_map_piodma, + /* a mode change retires its piodma buffers, then their descriptors */ + [202] = trampoline_unmap_piodma_v26_6, [206] = iomfbep_cb_match_pmu_service_2, [207] = iomfbep_cb_match_backlight_service, [208] = trampoline_nop, /* update_backlight_factor_prop */ @@ -134,6 +178,7 @@ static const iomfb_cb_handler cb_handlers[IOMFB_MAX_CB] = { [414] = trampoline_sr_set_property_int, [415] = trampoline_true, /* sr_set_property_bool */ [451] = trampoline_allocate_buffer, + [454] = trampoline_release_mem_desc_v26_6, [552] = trampoline_true, /* set_property_dict_0 */ [561] = trampoline_true, /* set_property_dict */ [563] = trampoline_true, /* set_property_int */ From 2d8286101c8631939cb13ce1b7b14c9041856ca6 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 25 Oct 2026 16:45:52 -0400 Subject: [PATCH 072/388] drm/asahi: build the G17 driver without warnings Kernels built with CONFIG_WERROR fail on the G17 changes. trace.c defines the eight tracepoint entry points that only Rust calls, so nothing in C declares them (-Wmissing-prototypes). The Rust side also leaves code without users: the compute host's generic doorbell, a recovery poll constant, the old capacity-wait helper and an unused import, and, without G16, the CPU timestamp mapping that only the G16 timestamp path uses. Declare the entry points in trace_api.h, included by trace.c, remove the unused items, and build the CPU timestamp mapping only with G16. Reported-by: RyanTheTide --- drivers/gpu/drm/asahi/g17/queue/compute.rs | 1 - drivers/gpu/drm/asahi/g17/runtime.rs | 13 ------------ drivers/gpu/drm/asahi/g17/trace.c | 2 ++ drivers/gpu/drm/asahi/g17/trace.rs | 2 +- drivers/gpu/drm/asahi/g17/trace_api.h | 24 ++++++++++++++++++++++ drivers/gpu/drm/asahi/mmu.rs | 3 +++ drivers/gpu/drm/asahi/timestamp.rs | 1 + 7 files changed, 31 insertions(+), 15 deletions(-) create mode 100644 drivers/gpu/drm/asahi/g17/trace_api.h diff --git a/drivers/gpu/drm/asahi/g17/queue/compute.rs b/drivers/gpu/drm/asahi/g17/queue/compute.rs index 9382d50d90b79..9c4c5c92d049d 100644 --- a/drivers/gpu/drm/asahi/g17/queue/compute.rs +++ b/drivers/gpu/drm/asahi/g17/queue/compute.rs @@ -1950,7 +1950,6 @@ pub(crate) trait Host { fn publish_qid(&mut self, id: kick::Id) -> Result; fn publish_outer(&mut self, priority: u8, slot: &crate::g17::fw::channels::WorkSlot) -> Result; - fn notify(&mut self, message: u64) -> Result; fn notify_compute(&mut self, priority: u8, message: u64) -> Result; fn compute_written(&self, priority: u8) -> u64; fn note_submission(&mut self) -> Result; diff --git a/drivers/gpu/drm/asahi/g17/runtime.rs b/drivers/gpu/drm/asahi/g17/runtime.rs index 90d980bc427c2..1600c55eaf47b 100644 --- a/drivers/gpu/drm/asahi/g17/runtime.rs +++ b/drivers/gpu/drm/asahi/g17/runtime.rs @@ -31,7 +31,6 @@ use kernel::{prelude::*, sync::Arc}; const QIDS: usize = 128; const FIRST_COMPUTE_QID: u8 = 4; -const RECOVERY_SETTLE_POLLS: usize = 200; const WORK_RING_WAIT_NS: u64 = super::job::TIMEOUT_MS as u64 * 1_000_000; /// Pending admission keeps exactly retired compute backing available for reuse. @@ -1391,9 +1390,6 @@ impl compute::Host for ComputeHost<'_> { fn publish_outer(&mut self, _priority: u8, slot: &super::fw::channels::WorkSlot) -> Result { self.port.publish_compute(self.outer.take().ok_or(EIO)?, slot).map(|_| ()) } - fn notify(&mut self, message: u64) -> Result { - self.port.notify(message) - } fn notify_compute(&mut self, priority: u8, message: u64) -> Result { self.port.notify_compute(priority, message) } @@ -1441,9 +1437,6 @@ impl compute::Host for DirectHost<'_> { fn publish_outer(&mut self, _priority: u8, slot: &super::fw::channels::WorkSlot) -> Result { self.port.publish_compute(self.outer.take().ok_or(EIO)?, slot).map(|_| ()) } - fn notify(&mut self, message: u64) -> Result { - self.port.notify(message) - } fn notify_compute(&mut self, priority: u8, message: u64) -> Result { self.port.notify_compute(priority, message) } @@ -2396,12 +2389,6 @@ impl Registry { } impl Backend { - fn wait_compute_capacity(&self, admission: &Admission<'_>, reclaim_backing: bool) -> Result { - self.reclaim_capacity(admission, reclaim_backing)?; - kernel::time::delay::fsleep(kernel::time::Delta::from_millis(1)); - admission.check() - } - /// Compute admission without a channel: give back what pressure allows, then block until a /// channel is released after `observed` (sampled before the failed check). fn wait_channels(&self, observed: u64, admission: &Admission<'_>, reclaim_backing: bool) -> Result { diff --git a/drivers/gpu/drm/asahi/g17/trace.c b/drivers/gpu/drm/asahi/g17/trace.c index ae977c13aab3b..23666be389b96 100644 --- a/drivers/gpu/drm/asahi/g17/trace.c +++ b/drivers/gpu/drm/asahi/g17/trace.c @@ -2,6 +2,8 @@ #include #include +#include "trace_api.h" + #define CREATE_TRACE_POINTS #include "trace.h" diff --git a/drivers/gpu/drm/asahi/g17/trace.rs b/drivers/gpu/drm/asahi/g17/trace.rs index 57db5abaccef8..b76b46425d2ce 100644 --- a/drivers/gpu/drm/asahi/g17/trace.rs +++ b/drivers/gpu/drm/asahi/g17/trace.rs @@ -3,7 +3,7 @@ //! Command lifecycle tracepoints (tracefs `events/asahi_g17`). Each call is a //! static-key test in C while its event is disabled. -use kernel::{bindings, dma_fence::RawDmaFence, prelude::*, sync::Arc}; +use kernel::{bindings, dma_fence::RawDmaFence, sync::Arc}; unsafe extern "C" { fn asahi_g17_trace_publish_enabled() -> bool; diff --git a/drivers/gpu/drm/asahi/g17/trace_api.h b/drivers/gpu/drm/asahi/g17/trace_api.h new file mode 100644 index 0000000000000..d09f529a163f0 --- /dev/null +++ b/drivers/gpu/drm/asahi/g17/trace_api.h @@ -0,0 +1,24 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ +/* + * Entry points of the G17 command lifecycle tracepoints, called from Rust + * (g17/trace.rs). + */ +#ifndef _ASAHI_G17_TRACE_API_H +#define _ASAHI_G17_TRACE_API_H + +#include + +struct dma_fence; + +bool asahi_g17_trace_publish_enabled(void); +void asahi_g17_trace_publish(u8 engine, u8 qid, u32 ordinal, u32 ctx, u64 seq, + struct dma_fence *fence); +void asahi_g17_trace_complete(u8 engine, u8 qid, u32 ordinal, u32 ctx, u64 seq, + u64 gpu_start, u64 gpu_end); +void asahi_g17_trace_bind(u8 engine, u8 qid, u32 ctx, u8 kind); +bool asahi_g17_trace_doorbell_enabled(void); +void asahi_g17_trace_doorbell(u8 engine, u32 power); +void asahi_g17_trace_message(u8 kind, u64 value); +void asahi_g17_trace_event(u8 phase); + +#endif /* _ASAHI_G17_TRACE_API_H */ diff --git a/drivers/gpu/drm/asahi/mmu.rs b/drivers/gpu/drm/asahi/mmu.rs index f5b079e9b5382..d67513d6bde4f 100644 --- a/drivers/gpu/drm/asahi/mmu.rs +++ b/drivers/gpu/drm/asahi/mmu.rs @@ -1255,11 +1255,13 @@ impl TimestampVmap { /// Prepared CPU timestamp access with its own vmap; destruction happens after completion /// signalling. +#[cfg(CONFIG_DRM_ASAHI_G16)] pub(crate) struct TimestampMapping { mapping: TimestampVmap, offset: usize, } +#[cfg(CONFIG_DRM_ASAHI_G16)] impl TimestampMapping { /// Prepare one bounded CPU timestamp destination on retained GEM backing. pub(crate) fn new(gem: &gem::Object, offset: usize) -> Result { @@ -1312,6 +1314,7 @@ impl KernelMapping { /// use a complete atomic store; byte stores support unaligned destinations without /// creating mutable aliases. An unaligned complete timestamp is not atomic. /// Retain CPU access before this destination's completion fence is published. + #[cfg(CONFIG_DRM_ASAHI_G16)] pub(crate) fn timestamp_mapping(&self, offset: usize) -> Result { let gem = self.0._gem.as_ref().ok_or(EINVAL)?; TimestampMapping::new(gem, self.timestamp_offset(offset)?) diff --git a/drivers/gpu/drm/asahi/timestamp.rs b/drivers/gpu/drm/asahi/timestamp.rs index 0d365a8d930bb..9f13436b4caf5 100644 --- a/drivers/gpu/drm/asahi/timestamp.rs +++ b/drivers/gpu/drm/asahi/timestamp.rs @@ -66,6 +66,7 @@ impl Buffer { } } + #[cfg(CONFIG_DRM_ASAHI_G16)] pub(crate) fn timestamp_mapping(&self, offset: usize) -> Result { match self { Self::Firmware(buffer) => buffer.mapping.timestamp_mapping(offset), From 9a0ed807e4d3691a5ea46fce2a2424295f8af6ea Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 25 Oct 2026 16:45:52 -0400 Subject: [PATCH 073/388] drm/asahi: renew G17 job deadlines while the GPU keeps retiring work The scheduler fails a published job two seconds after it started unless the job is a ray-tracing or acceleration-structure build. Under load that timer measures how the GPU is shared rather than whether the job makes progress. Beside four other clients, a 1.5 s compute dispatch took up to 5 s, and lower-priority work waited behind higher-priority work for seconds, while the GPU kept retiring other commands at least every 1.2 s. Those jobs failed with a lost device although nothing had hung. Renew a published job's deadline while any command on the GPU retired in the last five seconds, at most fourteen times (thirty seconds in all). A GPU that retires nothing for five seconds has stopped, and its jobs fail as before. --- drivers/gpu/drm/asahi/g17/job.rs | 9 ++++ drivers/gpu/drm/asahi/g17/recovery.rs | 2 +- drivers/gpu/drm/asahi/g17/recovery/safety.rs | 11 +++++ drivers/gpu/drm/asahi/g17/timeout.rs | 44 ++++++++++++++++++++ 4 files changed, 65 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/asahi/g17/job.rs b/drivers/gpu/drm/asahi/g17/job.rs index 27e54613965cd..8175c992b8a62 100644 --- a/drivers/gpu/drm/asahi/g17/job.rs +++ b/drivers/gpu/drm/asahi/g17/job.rs @@ -307,6 +307,7 @@ struct Timeout { backend: Arc, victim_renewals: u8, execution_renewals: u8, + shared_renewals: u8, packet: Arc, } @@ -331,6 +332,7 @@ impl sched::JobImpl for Job { backend: self.backend.clone(), victim_renewals: 0, execution_renewals: 0, + shared_renewals: 0, packet: self.packet.clone(), } } @@ -344,6 +346,13 @@ impl sched::JobImpl for Job { ) { return true; } + // Other work retiring means the GPU is shared, not hung. + if super::timeout::renew_shared( + job.packet.is_published(), super::recovery::since_retired(super::now_ns()), + &mut job.shared_renewals, + ) { + return true; + } let deferred = job.backend.defer_timeout(&job.packet, job.victim_renewals); if deferred { job.victim_renewals = job.victim_renewals.saturating_add(1); diff --git a/drivers/gpu/drm/asahi/g17/recovery.rs b/drivers/gpu/drm/asahi/g17/recovery.rs index 84a01d975c36d..36f1ceac9f821 100644 --- a/drivers/gpu/drm/asahi/g17/recovery.rs +++ b/drivers/gpu/drm/asahi/g17/recovery.rs @@ -10,7 +10,7 @@ mod attribution; mod slot; mod safety; -pub(crate) use safety::note_retired; +pub(crate) use safety::{note_retired, since_retired}; pub(crate) use attribution::Sources; use attribution::Attribution; diff --git a/drivers/gpu/drm/asahi/g17/recovery/safety.rs b/drivers/gpu/drm/asahi/g17/recovery/safety.rs index b97fc26914329..082416f0cb9a6 100644 --- a/drivers/gpu/drm/asahi/g17/recovery/safety.rs +++ b/drivers/gpu/drm/asahi/g17/recovery/safety.rs @@ -10,16 +10,27 @@ const LIMIT: u8 = 8; /// Commands retired successfully. A watchdog storm means the GPU stopped making progress; /// timeouts of overloaded clients while other work keeps retiring are not one. static RETIRED: AtomicU64 = AtomicU64::new(0); +/// [`crate::g17::now_ns`] of the latest successful retirement, zero before the first. +static LAST_RETIRED_NS: AtomicU64 = AtomicU64::new(0); /// Called for every command that retires without error. pub(crate) fn note_retired() { RETIRED.fetch_add(1, Ordering::Relaxed); + LAST_RETIRED_NS.store(crate::g17::now_ns(), Ordering::Relaxed); } pub(super) fn retired() -> u64 { RETIRED.load(Ordering::Relaxed) } +/// Nanoseconds since any command last retired successfully, or `None` before the first. +pub(crate) fn since_retired(now: u64) -> Option { + match LAST_RETIRED_NS.load(Ordering::Relaxed) { + 0 => None, + last => Some(now.saturating_sub(last)), + } +} + pub(super) struct Storm { start: Option, count: u8, diff --git a/drivers/gpu/drm/asahi/g17/timeout.rs b/drivers/gpu/drm/asahi/g17/timeout.rs index d951bd164d6e9..7b4ef4137b795 100644 --- a/drivers/gpu/drm/asahi/g17/timeout.rs +++ b/drivers/gpu/drm/asahi/g17/timeout.rs @@ -53,6 +53,32 @@ pub(crate) fn renew_execution(published: bool, enabled: bool, renewals: &mut u8) true } +/// Polls a published job may be renewed while the GPU keeps retiring work. A job +/// shares the GPU's cores with every other runnable queue, so its wall time grows +/// with the other clients' work: beside four other clients a 1.5 s dispatch took up +/// to 5 s, and lower-priority work waits behind higher-priority work. It has not +/// hung while the GPU keeps completing commands. Fourteen renewals bound the wait at +/// thirty seconds, after which a job that still has not finished fails as before. +const SHARED_RENEWALS: u8 = 14; + +/// The GPU counts as live while some command retired this recently. Under heavy +/// sharing the longest gap between retirements measured 1.2 s; a GPU that retires +/// nothing for this long has stopped. +pub(crate) const LIVENESS_NS: u64 = 5_000_000_000; + +/// Renews a published job's deadline while some command retired within +/// [`LIVENESS_NS`] (`since_retired`), at most [`SHARED_RENEWALS`] times. +pub(crate) fn renew_shared(published: bool, since_retired: Option, renewals: &mut u8) -> bool { + if !published + || !since_retired.is_some_and(|since| since < LIVENESS_NS) + || *renewals >= SHARED_RENEWALS + { + return false; + } + *renewals += 1; + true +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) struct Head { /// The owning VM's [`crate::mmu::Vm::id`], one per VM status; the watchdog lines print it. @@ -215,6 +241,24 @@ mod tests { assert_eq!(POLL_MS * (1 + u32::from(ray) + u32::from(MAX_RENEWALS)), 26_000); } #[test] + fn a_live_gpu_renews_a_published_job_for_thirty_seconds() { + let mut renewals = 0; + for poll in 1..=14u8 { + assert!(renew_shared(true, Some(4_900_000_000), &mut renewals)); + assert_eq!(renewals, poll); + } + assert!(!renew_shared(true, Some(0), &mut renewals)); + assert_eq!(POLL_MS * (u32::from(renewals) + 1), 30_000); + } + #[test] + fn a_stopped_gpu_or_unpublished_job_keeps_the_deadline() { + let mut renewals = 0; + assert!(!renew_shared(true, Some(LIVENESS_NS), &mut renewals)); + assert!(!renew_shared(true, None, &mut renewals)); + assert!(!renew_shared(false, Some(0), &mut renewals)); + assert_eq!(renewals, 0); + } + #[test] fn exhausted_or_invalid_credit_never_wraps() { for value in 4..=u8::MAX { let mut renewals = value; From afbfbc0b89ca4779b13cefff5bed0b4036751b2c Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 25 Oct 2026 16:45:52 -0400 Subject: [PATCH 074/388] drm/asahi: kill closed G17 contexts only while no other work runs A client whose work failed closes its queues, and 250 ms later the driver asks the firmware to kill the closed context's remaining commands. While other contexts' commands are running, that kill makes the firmware restart the GPU and fail everything in flight, and a few repeats crash it. On an otherwise idle GPU the same kill is handled cleanly. Publish the kill only while no other context has a render pair or compute queue in flight, or once the GPU has retired nothing for five seconds, the watchdog's liveness window. Until then the closed context's queues stay quarantined and retained, and its commands retire like any others once the GPU reaches them, as commands with ray traversal scratch already do. Deferred kills are rechecked on every service pass. --- .../gpu/drm/asahi/g17/runtime/compute_exit.rs | 26 ++++++++++++++++++- .../drm/asahi/g17/runtime/render/teardown.rs | 8 ++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/asahi/g17/runtime/compute_exit.rs b/drivers/gpu/drm/asahi/g17/runtime/compute_exit.rs index 69bc7550fc4e9..bf9d023038a51 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/compute_exit.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/compute_exit.rs @@ -9,6 +9,7 @@ use crate::g17::{ fw::channels::{ContextKill, ControlRecord}, queue::compute::exit, recovery::Memory as _, + timeout::LIVENESS_NS, Firmware, MSG_CONTROL_NOTIFY, }; use core::sync::atomic::{fence, Ordering}; @@ -82,7 +83,7 @@ impl Firmware { }) else { return Ok(()); }; - if self.render_control_backpressured() { + if self.render_control_backpressured() || self.kill_unsafe(context, now) { return Ok(()); } let (owner, target) = { @@ -149,6 +150,29 @@ impl Firmware { Ok(()) } + /// A kill while other contexts' commands run makes the firmware restart the + /// GPU, and repeated ones crash it; on an otherwise idle GPU it is safe. A + /// closed context's commands left alone retire like any others once the GPU + /// reaches them, so its queues stay quarantined and retained until then. Only + /// a GPU that retired nothing for [`LIVENESS_NS`] has stopped, and then the + /// kill proceeds. + fn kill_unsafe(&self, context: &Arc, now: u64) -> bool { + let stalled = crate::g17::recovery::since_retired(now).is_none_or(|since| since >= LIVENESS_NS); + if stalled { + return false; + } + self.queues.render.foreign_in_flight(context) + || self + .queues + .compute + .iter() + .flatten() + .filter_map(|entry| entry.queue.as_deref()) + .any(|queue| { + queue.in_flight() && !queue.context().is_some_and(|bound| Arc::ptr_eq(bound, context)) + }) + } + pub(in crate::g17) fn compute_exit_polling(&self, now: u64) -> bool { if self .queues diff --git a/drivers/gpu/drm/asahi/g17/runtime/render/teardown.rs b/drivers/gpu/drm/asahi/g17/runtime/render/teardown.rs index eb04f80361955..e54b1f4638f58 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render/teardown.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render/teardown.rs @@ -46,6 +46,14 @@ impl RenderState { Ok(true) } + /// Some render pair runs another context's commands. + pub(in crate::g17::runtime) fn foreign_in_flight(&self, context: &Arc) -> bool { + self.entries.iter().flatten().any(|entry| { + !entry.context.as_ref().is_some_and(|owner| Arc::ptr_eq(owner, context)) + && entry.pair.as_ref().is_some_and(|pair| pair.in_flight()) + }) + } + pub(in crate::g17::runtime) fn teardown_pools_released(&self, context: &Arc) -> bool { context.render_pools_released(|id, control| self.pools.released(id, control)) && self From 31279abf0648c00e454b181f01d65b277a32fb90 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 25 Oct 2026 18:22:31 -0400 Subject: [PATCH 075/388] drm/asahi: fail G17 render groups a recovery dropped out of order at once After a firmware restart, the firmware retires every render group it had started on a pair without running it: the groups the recovery table lists, and those whose tiler had run while their fragment stage was still pending. Groups it had not started run as usual. The host republishes dropped groups only when every retained group was dropped, so that none runs after a live successor. Once a later group has run, the dropped ones never qualify, and in-order retirement cannot pass the unexecuted front either. The pair then waited for the render watchdog: about 2 s later before deadline renewal, up to 30 s with it while other work keeps retiring. Its owner then saw ETIMEDOUT and a poisoned VM, even when the client was innocent and none of its work had started. In a stress run with forced recoveries, 10 of 11 recoveries left such a pair. Replay was also wrong for a dropped group that a compute command waited on: the wait ends when firmware retires the group, so the consumer may read output the group never wrote while the replay runs it afterwards. A client in the same stress run read back the previous iteration's image this way without any error. When firmware has retired every group of the pair but a dropped group cannot run again in order (a later group already ran, the dropped group had started, or a compute command waited on it), fail the whole set at once, after the same double observation as a replay. The groups that ran fail too: they consumed output the dropped ones never wrote. The work is innocent, so it fails with ENODATA, which its owner sees, without poisoning the VM. Guilty and blamed sets are left to the timeout as before, and a group that ran ahead of every dropped one is still retired in order. --- drivers/gpu/drm/asahi/g17/queue/render.rs | 79 ++++++++++++++++--- .../asahi/g17/queue/render/dependencies.rs | 26 +++--- drivers/gpu/drm/asahi/g17/replay.rs | 67 +++++++++++++++- .../asahi/g17/runtime/render/dependencies.rs | 3 + 4 files changed, 149 insertions(+), 26 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/queue/render.rs b/drivers/gpu/drm/asahi/g17/queue/render.rs index 9469d526f49c8..696a1486eb0ec 100644 --- a/drivers/gpu/drm/asahi/g17/queue/render.rs +++ b/drivers/gpu/drm/asahi/g17/queue/render.rs @@ -1333,10 +1333,9 @@ impl Pair { let blamed = active.packet.completion.blamed(); let exhausted = active.packet.completion.replay_count() >= crate::g17::completion::MAX_REPLAYS; - // decide() stops at the first group that is not abandoned, an error included. - let abandoned = innocent - && !blamed - && (|| -> Result { + // decide() stops at the first group that is still live, an error included. + let (abandoned, ran, lost) = if innocent && !blamed { + (|| -> Result<(bool, bool, bool)> { let mut observer = Observer { memory, qids, @@ -1347,21 +1346,33 @@ impl Pair { let stamps = observer.stamps()?; let (_, state) = observer.host.epoch()?; // A torn sample is no proof; a later poll or the timeout looks again. - // Interrupted work is run again only if it never started. - Ok(matches!( - retirement::abandoned(&active.ticket, snapshot, stamps, state, false), - Ok(true) - ) && (!active.packet.completion.interrupted() || snapshot.untouched())) + let witness = |executed| { + matches!( + retirement::abandoned(&active.ticket, snapshot, stamps, state, executed), + Ok(true) + ) + }; + let unexecuted = witness(false); + // Interrupted work is run again only if it never started, and work that + // another queue waited on never: the wait ended when firmware retired it. + let lost = (active.packet.completion.interrupted() && !snapshot.untouched()) + || active.packet.completion.has_firmware_consumer(); + Ok((unexecuted && !lost, !unexecuted && witness(true), unexecuted && lost)) })() .unwrap_or_else(|error: Error| { failure = Some(error); - false - }); + (false, false, false) + }) + } else { + (false, false, false) + }; Group { innocent, blamed, abandoned, exhausted, + ran, + lost, } }); let decision = replay::decide(groups); @@ -1376,7 +1387,7 @@ impl Pair { front.abandoned_since = None; return Ok(false); } - Decision::Replay => {} + Decision::Replay | Decision::Fail => {} } if grace { let Some(since) = front.abandoned_since else { @@ -1387,6 +1398,9 @@ impl Pair { return Ok(false); } } + if decision == Decision::Fail { + return self.fail_dropped(host, defer); + } // decide() lets only the front be exhausted. An exhausted front fails, and its // successors, which consume its output, fail with it rather than run without it. let mut failing = self.active.front().is_some_and(|front| { @@ -1445,6 +1459,45 @@ impl Pair { Ok(true) } + /// Firmware retired every group of the pair after a restart, but some were dropped + /// unexecuted and cannot run again in order (see `replay::decide`). Every group fails now, + /// the ones that ran too: they consumed output the dropped ones never wrote. The work is + /// innocent, so it fails without poisoning its VM, with ENODATA, which its owner sees, + /// instead of waiting for the render timeout to fail it with ETIMEDOUT. + fn fail_dropped( + &mut self, + host: &mut impl Host, + defer: &mut impl FnMut(Deferred) -> Result, + ) -> Result { + pr_warn!( + "G17: render slot {}: a GPU recovery dropped groups that cannot run again in order; failing {} groups\n", + self.slot, + self.active.len() + ); + let qos = self.qos_owners(); + let buffer = self.buffer_id(); + let pool = self.free_list.id(); + while let Some(front) = self.active.front_mut() { + if !front.flist_completed { + host.complete_render( + (!front.qos_completed).then_some(qos), + pool, + front.generation, + buffer, + )?; + front.flist_completed = true; + front.qos_completed = true; + } + let ordinal = front.ticket.ordinal; + let completion = front.packet.completion.clone(); + completion.classify(recovery::Class::Spared); + defer(Deferred::Retired(completion, Err(ENODATA)))?; + self.memory.retire(ordinal)?; + self.active.pop().ok_or(EIO)?; + } + Ok(true) + } + /// A front group a restart interrupted after its tiler started, which firmware /// then retired without writing its fragment end, may have run partly. It is /// not replayed (blending or storage writes could apply twice) and fails now, @@ -1943,7 +1996,7 @@ impl Pair { pub(crate) fn own_dependencies(&self, owner: u64, packet: &Arc) -> Result { let mut dependencies = RenderDependencies::EMPTY; let mut frontier = crate::g17::dependency::Frontier::new(); - self.collect_dependencies(owner, packet.completion.status(), packet.order.wait_through[0], &mut frontier)?; + self.collect_dependencies(owner, packet.completion.status(), packet.order.wait_through[0], &mut frontier, false)?; for dependency in frontier.as_slice() { *dependencies.render.get_mut(dependencies.render_count).ok_or(EOVERFLOW)? = *dependency; dependencies.render_count += 1; diff --git a/drivers/gpu/drm/asahi/g17/queue/render/dependencies.rs b/drivers/gpu/drm/asahi/g17/queue/render/dependencies.rs index 53a0fa6e449ed..ab96b48de3e35 100644 --- a/drivers/gpu/drm/asahi/g17/queue/render/dependencies.rs +++ b/drivers/gpu/drm/asahi/g17/queue/render/dependencies.rs @@ -31,6 +31,7 @@ fn include( prefix: Option, packet: &Arc, word: u64, + compute: bool, ) -> Result { if !Arc::ptr_eq(packet.completion.status(), status) { return Ok(()); @@ -38,12 +39,15 @@ fn include( if !job::Order::contains(prefix, packet.order.sequence) { return Ok(()); } - frontier.include( - prefix, - packet.order.sequence, - job::fence_status(&packet.completion.scheduler_fence()), - word, - ) + let status = job::fence_status(&packet.completion.scheduler_fence()); + frontier.include(prefix, packet.order.sequence, status, word)?; + // A compute consumer's firmware wait ends once firmware retires this group, even unexecuted + // after a restart, so it may then read output the group never wrote. Render consumers of the + // same owner follow the group on its pair, and a replay keeps that order. + if compute && status == 0 { + packet.completion.note_firmware_consumer(); + } + Ok(()) } impl Dependencies { @@ -60,12 +64,13 @@ impl Dependencies { status: &Arc, prefix: Option, frontier: &mut Frontier, + compute: bool, ) -> Result { if self.owner != Some(owner) { return Ok(()); } for entry in self.entries.iter().flatten() { - include(frontier, status, prefix, &entry.packet, entry.word)?; + include(frontier, status, prefix, &entry.packet, entry.word, compute)?; } Ok(()) } @@ -80,7 +85,7 @@ impl Dependencies { return Ok(()); } for entry in self.entries.iter().flatten() { - include(frontier, status, prefix, &entry.packet, entry.ta_word)?; + include(frontier, status, prefix, &entry.packet, entry.ta_word, false)?; } Ok(()) } @@ -112,6 +117,7 @@ impl Pair { status: &Arc, prefix: Option, frontier: &mut Frontier, + compute: bool, ) -> Result { if self.owner != Some(owner) { return Ok(()); @@ -120,7 +126,7 @@ impl Pair { for entry in self.active.iter() { let word = KickDependency::new(qid, entry.kicks[1]).map_or(u64::MAX, KickDependency::word); - include(frontier, status, prefix, &entry.packet, word)?; + include(frontier, status, prefix, &entry.packet, word, compute)?; } Ok(()) } @@ -138,7 +144,7 @@ impl Pair { for entry in self.active.iter() { let word = KickDependency::new(qid, entry.kicks[0]).map_or(u64::MAX, KickDependency::word); - include(frontier, status, prefix, &entry.packet, word)?; + include(frontier, status, prefix, &entry.packet, word, false)?; } Ok(()) } diff --git a/drivers/gpu/drm/asahi/g17/replay.rs b/drivers/gpu/drm/asahi/g17/replay.rs index 94daa5ec0dfeb..7668871376bad 100644 --- a/drivers/gpu/drm/asahi/g17/replay.rs +++ b/drivers/gpu/drm/asahi/g17/replay.rs @@ -16,6 +16,12 @@ pub(crate) struct Group { pub(crate) abandoned: bool, /// Already republished the maximum number of times. pub(crate) exhausted: bool, + /// Retired by firmware after executing in full. + pub(crate) ran: bool, + /// Retired by firmware unexecuted, but never to run again: a recovery interrupted it after + /// its tiler started, so it may have run partly, or a command of another queue waited on it, + /// and that wait ended when firmware retired the group, before its output existed. + pub(crate) lost: bool, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -25,26 +31,54 @@ pub(crate) enum Decision { Wait, /// Never replay this set; the render timeout settles it. Refuse, + /// Firmware has retired every group, but a dropped group cannot run again in order: a later + /// group already ran, or the dropped group had started or had a consumer. Fail the set now. + Fail, } /// Groups are read in order and the first one that decides stops the walk, so a caller can /// observe the abandonment lazily. An exhausted front is replayed: the replay fails it, and its /// successors, which consume its output, with it. An exhausted successor would break the order. +/// +/// After a restart the firmware retires the groups it had started on a pair without running +/// them, while groups it had not yet started run as usual. Once such a later group has run, a +/// replay of the dropped ones would run them after it; a group that had started cannot run +/// twice; and a group another queue waited on may already have been read before it ran. None of +/// these is replayed: the whole set fails at once instead of waiting for the render timeout. A +/// group that ran ahead of every dropped one is left to in-order retirement. pub(crate) fn decide(groups: impl Iterator) -> Decision { let mut front_exhausted = None; let mut successor_exhausted = false; + let mut dropped = false; + let mut unordered = false; for group in groups { if !group.innocent || group.blamed { return Decision::Refuse; } + if group.ran { + if !dropped { + return Decision::Wait; + } + unordered = true; + continue; + } + if group.lost { + dropped = true; + unordered = true; + continue; + } if !group.abandoned { return Decision::Wait; } + dropped = true; match front_exhausted { None => front_exhausted = Some(group.exhausted), Some(_) => successor_exhausted |= group.exhausted, } } + if unordered { + return Decision::Fail; + } match front_exhausted { Some(true) => Decision::Replay, Some(false) if !successor_exhausted => Decision::Replay, @@ -56,10 +90,13 @@ pub(crate) fn decide(groups: impl Iterator) -> Decision { mod tests { use super::*; fn group(innocent: bool, abandoned: bool, exhausted: bool) -> Group { - Group { innocent, blamed: false, abandoned, exhausted } + Group { innocent, blamed: false, abandoned, exhausted, ran: false, lost: false } } - const OK: Group = Group { innocent: true, blamed: false, abandoned: true, exhausted: false }; - const BLAMED: Group = Group { innocent: true, blamed: true, abandoned: true, exhausted: false }; + const OK: Group = + Group { innocent: true, blamed: false, abandoned: true, exhausted: false, ran: false, lost: false }; + const BLAMED: Group = Group { blamed: true, ..OK }; + const RAN: Group = Group { abandoned: false, ran: true, ..OK }; + const LOST: Group = Group { abandoned: false, lost: true, ..OK }; #[test] fn every_group_abandoned_and_innocent_is_replayed() { assert_eq!(decide([OK].into_iter()), Decision::Replay); @@ -106,4 +143,28 @@ mod tests { Decision::Replay ); } + #[test] + fn a_group_that_ran_after_dropped_ones_fails_the_set() { + // Seen after restarts: the started groups were dropped, the queued ones ran. + assert_eq!(decide([OK, OK, RAN, RAN].into_iter()), Decision::Fail); + assert_eq!(decide([OK, RAN, OK].into_iter()), Decision::Fail); + // Also with an exhausted front, which a replay would only fail later. + assert_eq!(decide([group(true, true, true), RAN].into_iter()), Decision::Fail); + } + #[test] + fn a_group_that_ran_first_is_left_to_retirement() { + assert_eq!(decide([RAN].into_iter()), Decision::Wait); + assert_eq!(decide([RAN, OK].into_iter()), Decision::Wait); + } + #[test] + fn a_started_dropped_group_fails_the_set() { + assert_eq!(decide([OK, LOST].into_iter()), Decision::Fail); + assert_eq!(decide([LOST, OK].into_iter()), Decision::Fail); + } + #[test] + fn failing_still_waits_for_live_groups_and_refuses_guilty_ones() { + assert_eq!(decide([OK, RAN, group(true, false, false)].into_iter()), Decision::Wait); + assert_eq!(decide([OK, RAN, group(false, true, false)].into_iter()), Decision::Refuse); + assert_eq!(decide([LOST, BLAMED].into_iter()), Decision::Refuse); + } } diff --git a/drivers/gpu/drm/asahi/g17/runtime/render/dependencies.rs b/drivers/gpu/drm/asahi/g17/runtime/render/dependencies.rs index 2b14b4548e0db..e150a10e2c9bc 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render/dependencies.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render/dependencies.rs @@ -12,6 +12,7 @@ impl Registry { packet: &Arc, ) -> Result { let mut frontier = Frontier::new(); + let compute = packet.engine() == crate::g17::job::Engine::Compute; for entry in self.render.entries.iter().flatten() { if let Some(pair) = entry.pair.as_ref() { pair.collect_dependencies( @@ -19,6 +20,7 @@ impl Registry { packet.completion.status(), packet.order.wait_through[0], &mut frontier, + compute, )?; } else if let Some(dependencies) = entry.borrowed_dependencies.as_ref() { dependencies.collect( @@ -26,6 +28,7 @@ impl Registry { packet.completion.status(), packet.order.wait_through[0], &mut frontier, + compute, )?; } } From 7012a59ed129baf969d83a3aa4d90f2a6cb410f2 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 25 Oct 2026 18:22:31 -0400 Subject: [PATCH 076/388] drm/asahi: announce withheld G17 compute doorbells after a recovery A compute doorbell is withheld while two or more unretired commands of its queue precede it, and any departure from ordinary retirement is meant to ring it at once. A recovery that touches no command of the queue left it withheld until those predecessors retired. Once the firmware has resumed from a restart, ring every withheld doorbell, one per priority class, before any replay is published. --- drivers/gpu/drm/asahi/g17/recovery.rs | 4 ++++ drivers/gpu/drm/asahi/g17/runtime/events.rs | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/drivers/gpu/drm/asahi/g17/recovery.rs b/drivers/gpu/drm/asahi/g17/recovery.rs index 36f1ceac9f821..3bb9d20190b57 100644 --- a/drivers/gpu/drm/asahi/g17/recovery.rs +++ b/drivers/gpu/drm/asahi/g17/recovery.rs @@ -107,6 +107,9 @@ pub(super) trait Queues { fn settle_cancelled_renders(&mut self, qids: u128); fn rescan_compute(&mut self) -> Result; fn release_retired_compute(&mut self); + /// Ring every compute doorbell still withheld: a restart is a departure from ordinary + /// retirement, after which the firmware must see every published command. + fn announce_withheld(&mut self); } /// Live shared-memory operations. The handshake word uses aligned 32-bit accesses; transitions @@ -296,6 +299,7 @@ pub(super) fn service( } let scan_error = host.rescan_compute().err(); host.release_retired_compute(); + host.announce_withheld(); Ok(ServiceResult { scan_error, target_error }) } diff --git a/drivers/gpu/drm/asahi/g17/runtime/events.rs b/drivers/gpu/drm/asahi/g17/runtime/events.rs index e13ec3e35e13c..4cf2dd0fafb97 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/events.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/events.rs @@ -236,6 +236,26 @@ impl recovery::Queues for Service<'_> { let result = self.firmware.release_compute_witnesses(self.deferred); self.settlement_error = self.settlement_error.or(result.err()); } + fn announce_withheld(&mut self) { + let mut doorbells = [0u64; 4]; + for mut queue in self + .firmware + .queues + .compute + .iter_mut() + .flatten() + .filter_map(|entry| entry.queue.as_deref_mut()) + { + if let Some(doorbell) = queue.take_withheld_doorbell() { + doorbells[((doorbell >> 2) & 3) as usize] = doorbell; + } + } + // One doorbell per priority class announces every queue's work slots on that ring. + for doorbell in doorbells.into_iter().filter(|doorbell| *doorbell != 0) { + let result = self.firmware.port.notify_compute(((doorbell >> 2) & 3) as u8, doorbell); + self.settlement_error = self.settlement_error.or(result.err()); + } + } } impl Firmware { From a5b034dc1a7822590471190f0d11e7c254c25634 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 09:25:33 +1100 Subject: [PATCH 077/388] selftests: apple-dcp: keep host errno and make namespaces native The Type-C PM adapter left linux/errno.h in the extracted unit. That picked up the AFK shim's Linux errno numbers after the native errno.h, which fails strict compilation on Darwin. Translate the include at the host boundary instead. Give the power-reply fixture a local EREMOTEIO when libc does not define it; its status never crosses a host API. The errno namespace gaps were present in the fixtures added in a7a7e2f37796. Their later replug and dropped-swap derivatives retain the same gap. Keep all ownership, ordering, retry and reply assertions unchanged. Use an explicitly selected MAKE command, or prefer gmake when installed, for the real Kconfig producer. This avoids accidentally invoking the stock macOS GNU Make 3.81 against Kbuild, which requires GNU Make 4. Document the producer dependency without skipping the Kconfig checks. Fixes: a7a7e2f37796 ("drm/apple: Compose the qualified J700 external display path") Signed-off-by: Ryan Murray --- tools/testing/selftests/drivers/gpu/apple-dcp/README | 8 +++++++- tools/testing/selftests/drivers/gpu/apple-dcp/common.py | 2 ++ .../testing/selftests/drivers/gpu/apple-dcp/lifecycle.py | 4 ++++ tools/testing/selftests/drivers/gpu/apple-dcp/pm.py | 5 +++-- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/README b/tools/testing/selftests/drivers/gpu/apple-dcp/README index c49b2d48949af..ffd0ecfcb852c 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/README +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/README @@ -12,7 +12,13 @@ Or keep translation units, commands, failure logs and source hashes: An installed copy needs --source /path/to/kernel. --group edid, rate, phy or lifecycle or boot selects one group. HOSTCC selects the host compiler (default clang). Python 3 and a compiler supporting AddressSanitizer/UndefinedBehaviorSanitizer -are required. No target access, firmware command, captured fixture or kernel +are required. The usb-kconfig producer also needs GNU Make 4 or newer. The +runner uses MAKE when set, otherwise gmake if available, then make. On macOS, +install GNU Make or select an existing producer with KCONFIG_CONF. The host +fixtures use native errno definitions; Linux-only reply statuses stay local +to the synthetic callback test. + +No target access, firmware command, captured fixture or kernel configuration is required. One TAP entry point uses shared production extraction and compiler handling. diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/common.py b/tools/testing/selftests/drivers/gpu/apple-dcp/common.py index cdf20256dfa4f..918a9d91fa30f 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/common.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/common.py @@ -47,6 +47,8 @@ def source_defines(source): def tipd_pm_header(root): """CD321x PM state machine for host units, with a host clamp() for minmax.h.""" header = (root / 'drivers/usb/typec/tipd/cd321x-pm.h').read_text() + # Use the host errno namespace, not the AFK Linux-numbered shim. + header = header.replace('#include ', '#include ') # Host integer types; a repeated identical typedef is valid C11. header = header.replace('#include ', '#include \ntypedef uint32_t u32;') diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py index d7485a6681747..21a962fb10cb3 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py @@ -1770,6 +1770,10 @@ def power_reply(root): #include #include #include +/* Darwin has no EREMOTEIO; this synthetic status is never passed to libc. */ +#ifndef EREMOTEIO +#define EREMOTEIO 121 +#endif typedef uint32_t u32; typedef uint64_t u64; #define WRITE_ONCE(p,v) ((p)=(v)) #define container_of(p,t,m) ((t *)((char *)(p)-offsetof(t,m))) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py b/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py index 75fe10221fcc9..d5b3890d30861 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py @@ -2,7 +2,7 @@ # Copyright 2026 Ryan Murray """Actual PM/HCD/mux/probe boundaries using the shared PHY extraction fixture.""" from pathlib import Path -import os, re, subprocess +import os, re, shlex, shutil, subprocess import phy from common import block, tipd_pm_header, atc_function @@ -681,7 +681,8 @@ def kconfig(root, output): 'config PHY_APPLE_ATC\n\ttristate "PHY"\n\t' + dependency + '\n') compiler = Path(os.environ.get('KCONFIG_CONF', root / 'scripts/kconfig/conf')) if not compiler.is_file(): - subprocess.run(['make', '-C', str(root), 'O=' + str(directory / 'producer'), + make = shlex.split(os.environ.get('MAKE', shutil.which('gmake') or 'make')) + subprocess.run(make + ['-C', str(root), 'O=' + str(directory / 'producer'), 'ARCH=arm64', 'LLVM=1', 'allnoconfig'], check=True) compiler = directory / 'producer/scripts/kconfig/conf' for usb, phy_setting in [('n', 'y'), ('y', 'y'), ('m', 'y'), ('m', 'm')]: From 5269f2bdc0854bc277b83b89c9ce72f962e9cfb4 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 09:27:27 +1100 Subject: [PATCH 078/388] selftests: apple-dcp: initialize the lifetime endpoint command list AFK quiesce now cancels commands before destroying the endpoint queues. The AV lifetime fixtures construct endpoints without afk_init(), so their zeroed compact_cmds list is not a valid empty list. The devres-order case faults in the real cancellation path before its ordering assertions finish. Initialize that list at both fixture endpoint allocation sites, as the real endpoint initializer does. Keep the real teardown, blocked owner work and all release-order assertions in place. Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/av-lifetime-test.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/drivers/gpu/drm/apple/av-lifetime-test.c b/drivers/gpu/drm/apple/av-lifetime-test.c index 0232dfb670bbd..193358ce2bfba 100644 --- a/drivers/gpu/drm/apple/av-lifetime-test.c +++ b/drivers/gpu/drm/apple/av-lifetime-test.c @@ -155,6 +155,7 @@ static struct av_life_test *av_life_setup(struct kunit *test, work_func_t owner_ t->dcp->dev = t->dev; dev_set_drvdata(t->dev, t->dcp); spin_lock_init(&t->ep->lock); + INIT_LIST_HEAD(&t->ep->compact_cmds); init_rwsem(&t->asrv->srv_rwsem); INIT_WORK(&t->asrv->start_av_service_wq, owner_fn); INIT_WORK(&t->asrv->terminate_wq, owner_fn); @@ -484,6 +485,7 @@ static struct av_life_test *av_life_group_setup(struct kunit *test, t->ep = devm_kzalloc(t->dev, sizeof(*t->ep), GFP_KERNEL); KUNIT_ASSERT_NOT_NULL(test, t->ep); spin_lock_init(&t->ep->lock); + INIT_LIST_HEAD(&t->ep->compact_cmds); t->ep->dcp = t->dcp; t->wq = alloc_ordered_workqueue("av-lifetime-devres", 0); KUNIT_ASSERT_NOT_ERR_OR_NULL(test, t->wq); From 88aebfe0c656676c530faa8f85df8d7b69a7ddb0 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 25 Oct 2026 11:18:56 -0400 Subject: [PATCH 079/388] irqchip/apple-aic: Report the effective affinity of AICv2+ IRQs AICv2 and later have no per-IRQ CPU target. The controller hands each hardware IRQ to one of the CPUs that accept IRQs, and each CPU says whether it does in its SIQ_CFG system register: a CPU whose routing field (bits 1:0) reads 3 never receives one. On T8140 the boot CPU reads 0 and every secondary reads 3, so every hardware IRQ is taken on CPU 0, even while CPU 0 holds IRQs masked for tens of milliseconds. Setting a secondary to 1 or 2 makes the controller deliver to it as well. The AICv2 irqchip has no irq_set_affinity, so the effective affinity of its IRQs stays empty and their threaded handlers may run on any CPU. The scheduler often places them away from the CPU taking the hard IRQ, which adds a cross-CPU wakeup to every interrupt. On SoCs where this routing has been verified on hardware, currently T8140, and only at EL2, since a hypervisor or secure monitor may trap SIQ_CFG, map AICv2+ IRQs to a second irqchip. It records at CPU bring-up which CPUs accept IRQs and reports them as each IRQ's effective affinity, so that genirq binds threaded handlers to the CPUs the hard IRQ arrives on. The requested mask cannot change delivery; it is accepted and kept only for reporting. On T8140 every AIC IRQ thread, PCIe MSI ones included, therefore runs on CPU 0, and smp_affinity writes do not move it. Every other SoC keeps the existing irqchip unchanged. The accepting set grows as CPUs start and existing IRQs keep theirs, which is enough while only one CPU accepts IRQs. A SoC with more than one accepting CPU would also need the masks refreshed when such a CPU comes online. On T8140 the G17 GPU completion thread used to run on CPU 5 most of the time and now stays on CPU 0. Over two boots of each kernel, compute submission at depth 2 goes from 47-49 us to 37 us per submit, and render submission at depth 1 from a CPU 0 submitter from 128 us to 108 us. Other submission rows are unchanged. --- drivers/irqchip/irq-apple-aic.c | 77 ++++++++++++++++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) diff --git a/drivers/irqchip/irq-apple-aic.c b/drivers/irqchip/irq-apple-aic.c index 3214501a10635..1125008130ef4 100644 --- a/drivers/irqchip/irq-apple-aic.c +++ b/drivers/irqchip/irq-apple-aic.c @@ -203,6 +203,17 @@ #define SYS_IMP_APL_UPMSR_EL1 sys_reg(3, 7, 15, 6, 4) #define UPMSR_IACT BIT(0) +/* + * System interrupt configuration. AICv2 and later have no per-IRQ CPU target: + * the controller delivers each IRQ to one of the CPUs that accept IRQs, and + * this per-CPU field says whether a CPU does. A CPU whose field reads + * SIQ_CFG_ROUTE_NONE never receives an AIC IRQ. Only used on SoCs where this + * has been verified on hardware (aic_info.siq_cfg_routing). + */ +#define SYS_IMP_APL_SIQ_CFG_EL1 sys_reg(3, 4, 15, 10, 4) +#define SIQ_CFG_ROUTE GENMASK(1, 0) +#define SIQ_CFG_ROUTE_NONE 3 + /* MPIDR fields */ #define MPIDR_CPU(x) MPIDR_AFFINITY_LEVEL(x, 0) #define MPIDR_CLUSTER(x) MPIDR_AFFINITY_LEVEL(x, 1) @@ -261,6 +272,15 @@ struct aic_info { bool fast_ipi; bool local_fast_ipi; bool vm_timer_fiq_readonly; + /* + * SIQ_CFG routing verified on hardware: at EL2, AICv2+ IRQs report the + * CPUs that accept them as their effective affinity. The set grows as + * CPUs start and IRQs keep the set they were given, which is enough + * while only one CPU accepts IRQs. A SoC listed here with more than one + * accepting CPU would also need those masks refreshed when such a CPU + * comes online. + */ + bool siq_cfg_routing; }; static const struct aic_info aic1_info __initconst = { @@ -307,6 +327,14 @@ static const struct aic_info aic3_info __initconst = { .local_fast_ipi = true, }; +static const struct aic_info aic3_t8140_info __initconst = { + .version = 3, + .irq_cfg = AIC3_IRQ_CFG, + .fast_ipi = true, + .local_fast_ipi = true, + .siq_cfg_routing = true, +}; + static const struct aic_info aic3_t8142_info __initconst = { .version = 3, .irq_cfg = AIC3_IRQ_CFG, @@ -316,6 +344,10 @@ static const struct aic_info aic3_t8142_info __initconst = { }; static const struct of_device_id aic_info_match[] = { + { + .compatible = "apple,t8140-aic3", + .data = &aic3_t8140_info, + }, { .compatible = "apple,t8142-aic3", .data = &aic3_t8142_info, @@ -363,6 +395,9 @@ static DEFINE_PER_CPU(uint32_t, aic_fiq_unmasked); static struct aic_irq_chip *aic_irqc; +/* CPUs that accept hardware IRQs, read from SIQ_CFG where siq_cfg_routing is set */ +static struct cpumask aic_irq_cpus; + static void aic_handle_ipi(struct pt_regs *regs); static u32 aic_ic_read(struct aic_irq_chip *ic, u32 reg) @@ -510,6 +545,32 @@ static struct irq_chip aic2_chip = { .flags = IRQCHIP_SKIP_SET_WAKE, }; +/* + * AICv2 and later cannot be steered: the controller delivers each IRQ to one of + * the CPUs that accept IRQs, whatever the requested mask. Where SIQ_CFG says + * which CPUs those are, report them, so that threaded handlers run where the + * hard IRQ is taken. The requested mask is accepted and kept only for + * reporting. + */ +static int aic2_routed_irq_set_affinity(struct irq_data *d, + const struct cpumask *mask_val, bool force) +{ + irq_data_update_effective_affinity(d, cpumask_empty(&aic_irq_cpus) ? + cpu_possible_mask : &aic_irq_cpus); + return IRQ_SET_MASK_OK; +} + +/* AIC2 on SoCs whose SIQ_CFG routing is read (aic_info.siq_cfg_routing, at EL2) */ +static struct irq_chip aic2_routed_chip = { + .name = "AIC2", + .irq_mask = aic_irq_mask, + .irq_unmask = aic_irq_unmask, + .irq_eoi = aic_irq_eoi, + .irq_set_affinity = aic2_routed_irq_set_affinity, + .irq_set_type = aic_irq_set_type, + .flags = IRQCHIP_SKIP_SET_WAKE, +}; + /* * FIQ irqchip */ @@ -684,7 +745,8 @@ static int aic_irq_domain_map(struct irq_domain *id, unsigned int irq, struct irq_chip *chip = &aic_chip; if (ic->info.version == 2 || ic->info.version == 3) - chip = &aic2_chip; + chip = ic->info.siq_cfg_routing && is_kernel_in_hyp_mode() ? + &aic2_routed_chip : &aic2_chip; if (type == AIC_EVENT_TYPE_IRQ) { irq_domain_set_info(id, irq, hw, chip, id->host_data, @@ -958,6 +1020,19 @@ static int aic_init_cpu(unsigned int cpu) } } + /* + * Record whether the controller delivers IRQs to this CPU. SIQ_CFG is + * read only on SoCs where its routing has been verified on hardware, + * and only at EL2, since a hypervisor or secure monitor may trap it. + */ + if (aic_irqc->info.siq_cfg_routing && is_kernel_in_hyp_mode()) { + if (FIELD_GET(SIQ_CFG_ROUTE, read_sysreg_s(SYS_IMP_APL_SIQ_CFG_EL1)) == + SIQ_CFG_ROUTE_NONE) + cpumask_clear_cpu(cpu, &aic_irq_cpus); + else + cpumask_set_cpu(cpu, &aic_irq_cpus); + } + /* Initialize the local mask state */ __this_cpu_write(aic_fiq_unmasked, 0); From b11a5e9e8cd83d4ac3ac40bc620de254b095c75d Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 25 Oct 2026 03:17:29 -0400 Subject: [PATCH 080/388] cpufreq: apple: verify T8140 transitions on the next request Every T8140 frequency request spun until the cluster reported the new state, about 50 us per transition, and the same verification kept schedutil off the fast-switch path. Each change therefore woke the governor kthread, which runs above every normal task and then busy-waited on whichever CPU it landed on. Under light GPU work the P-cluster changes state about a thousand times a second: each GPU completion runs the mailbox interrupt thread, a real-time task that raises the cluster to its top state, and the cluster drops back once the thread sleeps. The spin cost 7% of a P-core and landed between a completion and the next submission. On the fast-switch path, check each request when the next one is issued instead. That request still waits for BUSY to clear and still stops all requests if the accepted state is not the one last written, so an unexpected owner is caught as before, one request later. With the wait gone the driver can switch from the scheduler path, and the governor thread is no longer used. Requests through ->target_index, from suspend and the static governors, still wait for their own transition. The fast-switch callback now resolves its table index from the frequency it is given instead of reading policy->cached_resolved_idx, which a concurrent limits update can rewrite between programming the cluster and reporting the frequency. Measured on Neo, same boot, alternating with the previous module (gpubench, us per submission, two rounds): compute, depth 2 58.8-59.7 -> 46.4-47.3 compute, depth 1 107.8-108.4 -> 102.6-102.7 render, depth 2 47.5-47.8 -> 45.8-45.9 render, depth 32 and compute, depth 64: unchanged --- drivers/cpufreq/apple-soc-cpufreq.c | 48 +++++++++++++++++++++++------ 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/drivers/cpufreq/apple-soc-cpufreq.c b/drivers/cpufreq/apple-soc-cpufreq.c index ba18f042e64b6..6425f95cbaedb 100644 --- a/drivers/cpufreq/apple-soc-cpufreq.c +++ b/drivers/cpufreq/apple-soc-cpufreq.c @@ -110,7 +110,10 @@ * running, the hrtimer only checks every 100 ms, and every second once the * warning has been given. A nohz_full CPU can run a task with its tick * stopped, and so without sampling, so a cluster with nohz_full CPUs - * samples from the hrtimer all the time. + * samples from the hrtimer all the time. Where schedutil switches frequency + * from the scheduler (fast switch), a lower limit reaches the cluster at its + * next scheduler frequency update rather than at once, which a nohz_full CPU + * running with its tick stopped may not have for a long time. * * A sample reads the effort through the PMP report driver; only when the * limit it maps to changes is a work item queued to update the QoS request, @@ -187,6 +190,7 @@ static int apple_fast_die_hp_state; struct apple_soc_cpufreq_info { bool has_ps2; bool verify_transition; + bool lazy_verify; /* Fail closed when the DT describes no fast die-temperature limit. */ bool needs_thermal_policy; bool needs_thermal_owner; @@ -243,12 +247,17 @@ static const struct apple_soc_cpufreq_info soc_t8112_info = { /* * T8140 uses 5-bit PS1 state IDs starting at 1. The command register reads - * back the last accepted request, not a measured frequency; every transition - * is verified, see apple_soc_cpufreq_set_target(). Voltage, PLL and firmware - * throttling configuration are left as the bootloader set them. + * back the last accepted request, not a measured frequency; each request is + * verified, see apple_soc_cpufreq_request(). Requests from the scheduler's + * fast-switch path are verified when the next one is issued rather than + * waited for, so schedutil can switch from the scheduler; requests through + * ->target_index (suspend, the static governors) are still waited for. + * Voltage, PLL and firmware throttling configuration are left as the + * bootloader set them. */ static const struct apple_soc_cpufreq_info soc_t8140_info = { .verify_transition = true, + .lazy_verify = true, .needs_thermal_policy = true, .min_pstate = 1, .max_pstate = 31, @@ -378,8 +387,14 @@ static unsigned int apple_soc_cpufreq_get_rate(unsigned int cpu) return 0; } -static int apple_soc_cpufreq_set_target(struct cpufreq_policy *policy, - unsigned int index) +/* + * Requests the frequency table entry @index. With @wait, a SoC that verifies + * transitions waits for this one to be accepted; without it, the next request + * checks it: it waits for BUSY to clear and requires the accepted state to + * match. + */ +static int apple_soc_cpufreq_request(struct cpufreq_policy *policy, + unsigned int index, bool wait) { struct apple_cpu_priv *priv = policy->driver_data; unsigned int pstate = policy->freq_table[index].driver_data; @@ -424,7 +439,7 @@ static int apple_soc_cpufreq_set_target(struct cpufreq_policy *policy, writeq_relaxed(reg, priv->reg_base + APPLE_DVFS_CMD); - if (priv->info->verify_transition && + if (priv->info->verify_transition && wait && readq_poll_timeout_atomic(priv->reg_base + APPLE_DVFS_CMD, reg, !(reg & APPLE_DVFS_CMD_BUSY) && FIELD_GET(APPLE_DVFS_CMD_PS1, reg) == pstate, @@ -441,13 +456,26 @@ static int apple_soc_cpufreq_set_target(struct cpufreq_policy *policy, return 0; } +static int apple_soc_cpufreq_set_target(struct cpufreq_policy *policy, + unsigned int index) +{ + return apple_soc_cpufreq_request(policy, index, true); +} + static unsigned int apple_soc_cpufreq_fast_switch(struct cpufreq_policy *policy, unsigned int target_freq) { - if (apple_soc_cpufreq_set_target(policy, policy->cached_resolved_idx) < 0) + struct apple_cpu_priv *priv = policy->driver_data; + /* + * Resolve the index here: policy->cached_resolved_idx can be rewritten + * by a concurrent limits update. target_freq is already clamped. + */ + int idx = cpufreq_table_find_index_l(policy, target_freq, false); + + if (idx < 0 || apple_soc_cpufreq_request(policy, idx, !priv->info->lazy_verify) < 0) return 0; - return policy->freq_table[policy->cached_resolved_idx].frequency; + return policy->freq_table[idx].frequency; } static int apple_soc_cpufreq_find_cluster(struct cpufreq_policy *policy, @@ -1014,7 +1042,7 @@ static int apple_soc_cpufreq_init(struct cpufreq_policy *policy) policy->cpuinfo.transition_latency = transition_latency; policy->dvfs_possible_from_any_cpu = true; - policy->fast_switch_possible = !info->verify_transition; + policy->fast_switch_possible = !info->verify_transition || info->lazy_verify; policy->suspend_freq = freq_table[0].frequency; return 0; From 8e257af33a6c4036ee56bc8a628e733dba2bd525 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 25 Oct 2026 19:17:20 -0400 Subject: [PATCH 081/388] drm/asahi: move the G17 cumulative-signals feature to bit 29 Feature bit 17 is also used for render resources by the M5 userspace line, whose features take bits 17 to 28. Advertise cumulative signals as bit 29, which no line uses, so one Mesa can serve both kernels. A kernel and Mesa that disagree on the bit fall back to ordinary per-point signals, so either mix still works. --- drivers/gpu/drm/asahi/g17/sync_output.rs | 2 +- include/uapi/drm/asahi_drm.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/sync_output.rs b/drivers/gpu/drm/asahi/g17/sync_output.rs index 50585befb3b71..928ebe2319394 100644 --- a/drivers/gpu/drm/asahi/g17/sync_output.rs +++ b/drivers/gpu/drm/asahi/g17/sync_output.rs @@ -6,7 +6,7 @@ use kernel::{bindings, dma_fence::{Fence, RawDmaFence}, prelude::*}; // Wire values also documented in include/uapi/drm/asahi_drm.h. Keeping these // local permits building this module against an older kernel's Rust bindings. pub(crate) const SYNC_CUMULATIVE: u32 = 2; -pub(crate) const FEATURE_CUMULATIVE_SIGNALS: u64 = 1 << 17; +pub(crate) const FEATURE_CUMULATIVE_SIGNALS: u64 = 1 << 29; unsafe extern "C" { fn asahi_g17_signals_new(syncs: *const SignalSpec, count: usize, anchor: usize, diff --git a/include/uapi/drm/asahi_drm.h b/include/uapi/drm/asahi_drm.h index 836739ffbbebf..66d5188199383 100644 --- a/include/uapi/drm/asahi_drm.h +++ b/include/uapi/drm/asahi_drm.h @@ -374,7 +374,7 @@ enum drm_asahi_feature { * DRM_ASAHI_SYNC_TIMELINE_SYNCOBJ_CUMULATIVE output. All outputs * include the earlier points of that timeline without adding GPU waits. */ - DRM_ASAHI_FEATURE_CUMULATIVE_SIGNALS = (1UL) << 17, + DRM_ASAHI_FEATURE_CUMULATIVE_SIGNALS = (1UL) << 29, }; #define DRM_ASAHI_PARAM_GROUP_VM_STATUS 1 From 51aafb566d64906eb1c655f9932b3c006e62f088 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 10:36:46 +1100 Subject: [PATCH 082/388] arm64: dts: apple: repair the J616s PCI Wi-Fi schema description Name the BCM4388 PCI function while retaining the family fallback used by the driver for country and MAC properties. Allow that pair in the binding and omit the redundant root-port compatible. This is independent of GPU enablement, split from the J616s bring-up to keep that review focused. Assisted-by: LLM Signed-off-by: Ryan Murray --- .../devicetree/bindings/net/wireless/brcm,bcm4329-fmac.yaml | 1 + arch/arm64/boot/dts/apple/t6040-j616s.dts | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Documentation/devicetree/bindings/net/wireless/brcm,bcm4329-fmac.yaml b/Documentation/devicetree/bindings/net/wireless/brcm,bcm4329-fmac.yaml index 81fd3e37452a6..8f935a2d2b320 100644 --- a/Documentation/devicetree/bindings/net/wireless/brcm,bcm4329-fmac.yaml +++ b/Documentation/devicetree/bindings/net/wireless/brcm,bcm4329-fmac.yaml @@ -46,6 +46,7 @@ properties: - cypress,cyw4373-fmac - cypress,cyw43012-fmac - infineon,cyw43439-fmac + - pci14e4,4434 # BCM4388 - const: brcm,bcm4329-fmac - enum: - brcm,bcm4329-fmac diff --git a/arch/arm64/boot/dts/apple/t6040-j616s.dts b/arch/arm64/boot/dts/apple/t6040-j616s.dts index a40a3fa1e1dc0..c62fee61be44a 100644 --- a/arch/arm64/boot/dts/apple/t6040-j616s.dts +++ b/arch/arm64/boot/dts/apple/t6040-j616s.dts @@ -569,8 +569,7 @@ device_type = "pci"; port00: pci@0,0 { - compatible = "pciclass,0604"; - reg = <0 0 0 0 0>; + reg = <0 0 0 0 0>; ranges; interrupt-map = <0 0 0 1 &port00 0 0 0 0 0 0 0 2 &port00 0 0 0 1 0 0 0 3 &port00 0 0 0 2 0 0 0 4 &port00 0 0 0 3>; interrupt-map-mask = <0 0 0 7>; @@ -592,7 +591,7 @@ }; wifi@0,0 { - compatible = "brcm,bcm4329-fmac"; + compatible = "pci14e4,4434", "brcm,bcm4329-fmac"; reg = <0 0 0 0 0>; brcm,ccode-map-trivial; brcm,board-type = "apple,hostomel"; From bbcf21a8b416f71a6eedfbb383b38b12b89b617a Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 10:40:12 +1100 Subject: [PATCH 083/388] arm64: dts: apple: keep the root-port property indentation Preserve the existing indent after removing the redundant compatible. Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/boot/dts/apple/t6040-j616s.dts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arch/arm64/boot/dts/apple/t6040-j616s.dts b/arch/arm64/boot/dts/apple/t6040-j616s.dts index c62fee61be44a..65ee34b8b362f 100644 --- a/arch/arm64/boot/dts/apple/t6040-j616s.dts +++ b/arch/arm64/boot/dts/apple/t6040-j616s.dts @@ -569,7 +569,7 @@ device_type = "pci"; port00: pci@0,0 { - reg = <0 0 0 0 0>; + reg = <0 0 0 0 0>; ranges; interrupt-map = <0 0 0 1 &port00 0 0 0 0 0 0 0 2 &port00 0 0 0 1 0 0 0 3 &port00 0 0 0 2 0 0 0 4 &port00 0 0 0 3>; interrupt-map-mask = <0 0 0 7>; From e2a2d8ecbb24146ff61e1649ff503f234e846b30 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 25 Oct 2026 20:15:30 -0400 Subject: [PATCH 084/388] cpufreq: apple: verify T8140 fast-switch requests from a timer The previous change verified a fast-switch request only when the next one was issued. That request still spun on BUSY in the scheduler for up to the transition timeout, the last request of a burst was never verified, and a request dropped on BUSY was never retried: schedutil does not ask again for the frequency it last asked for. Verify from a hard-irq hrtimer instead, serialised with the scheduler's requests and ->target_index by a per-policy raw spinlock: - A request that finds the cluster idle checks that the accepted state is the one last written, and is written at once. The timer checks the last request written the same way within 1 ms, with 0.5 ms of slack so that it can share an interrupt, so every request is verified, the last of a burst included. - A request that finds the cluster busy is kept as pending; it is neither spun on nor dropped. The timer writes it once the transition has finished, from about when it should have, and verifies it in turn. - A transition still busy past the transition timeout, or an accepted state other than the one written, latches transition_failed as before. The timer reports it, outside the scheduler. - fast_switch returns the requested frequency once a request is committed. ->target_index cancels the timer, drops any pending request and keeps its blocking verification; ->exit cancels the timer. Only lazy_verify SoCs (T8140) use the timer. Measured on T8140 against the previous change, in the same boot, alternating, three rounds (us per submit, medians): compute.light queue d1 102.67 -> 102.38 compute.light queue d2 46.70 -> 46.61 render.light queue d2 45.81 -> 45.76 with 5-6% more timer interrupts over those rows. Under mixed GPU and CPU load the policies made about 570 transitions/s with no DVFS errors; all-core load throttled to the same frequencies; the static governors and a pm_test=devices suspend/resume cycle ran without errors. A diagnostic build, not part of this change, showed the timer alone finding a refused write when no further request was issued, and a stuck BUSY on both policies. --- drivers/cpufreq/apple-soc-cpufreq.c | 283 +++++++++++++++++++++++++--- 1 file changed, 262 insertions(+), 21 deletions(-) diff --git a/drivers/cpufreq/apple-soc-cpufreq.c b/drivers/cpufreq/apple-soc-cpufreq.c index 6425f95cbaedb..a634be0b6f6a7 100644 --- a/drivers/cpufreq/apple-soc-cpufreq.c +++ b/drivers/cpufreq/apple-soc-cpufreq.c @@ -72,6 +72,17 @@ #define APPLE_DVFS_TRANSITION_TIMEOUT 400 +/* + * Lazy verification: the timer checks the last request written this long + * after it, with this much slack so that it can share an interrupt with other + * timers. A request kept pending behind a busy transition is retried about + * when that transition should finish, and then at the short interval. + */ +#define APPLE_DVFS_VERIFY_LAZY_NS (1 * NSEC_PER_MSEC) +#define APPLE_DVFS_VERIFY_SLACK_NS (500 * NSEC_PER_USEC) +#define APPLE_DVFS_TRANSITION_NS (50 * NSEC_PER_USEC) +#define APPLE_DVFS_RETRY_NS (4 * NSEC_PER_USEC) + /* * Fast die-temperature limit * @@ -214,6 +225,22 @@ struct apple_cpu_priv { unsigned int expected_pstate; struct freq_qos_request unmanaged_cap; bool thermal_owned; + /* + * Lazy verification, see apple_soc_cpufreq_lazy_request(): the lock + * serialises the command register, expected_pstate and + * transition_failed between the scheduler's requests, the timer that + * verifies them and ->target_index. + */ + raw_spinlock_t lock; + struct hrtimer verify_timer; + /* When the last request was written, and when it times out. */ + ktime_t write_time; + ktime_t verify_deadline; + /* P-state requested while a transition was busy, or -1. */ + int pending_pstate; + /* A failure the verify timer has yet to report, and its command. */ + bool failure_unreported; + u64 failure_command; }; static struct cpufreq_driver apple_soc_cpufreq_driver; @@ -248,12 +275,11 @@ static const struct apple_soc_cpufreq_info soc_t8112_info = { /* * T8140 uses 5-bit PS1 state IDs starting at 1. The command register reads * back the last accepted request, not a measured frequency; each request is - * verified, see apple_soc_cpufreq_request(). Requests from the scheduler's - * fast-switch path are verified when the next one is issued rather than - * waited for, so schedutil can switch from the scheduler; requests through - * ->target_index (suspend, the static governors) are still waited for. - * Voltage, PLL and firmware throttling configuration are left as the - * bootloader set them. + * verified. Requests from the scheduler's fast-switch path are verified by a + * timer rather than waited for, so schedutil can switch from the scheduler, + * see apple_soc_cpufreq_lazy_request(); requests through ->target_index + * (suspend, the static governors) are still waited for. Voltage, PLL and + * firmware throttling configuration are left as the bootloader set them. */ static const struct apple_soc_cpufreq_info soc_t8140_info = { .verify_transition = true, @@ -387,14 +413,25 @@ static unsigned int apple_soc_cpufreq_get_rate(unsigned int cpu) return 0; } +/* Writes a request for @pstate into the command register value @reg. */ +static void apple_soc_cpufreq_write(struct apple_cpu_priv *priv, u64 reg, + unsigned int pstate) +{ + reg &= ~priv->info->ps1_mask; + reg |= pstate << priv->info->ps1_shift; + if (priv->info->has_ps2) + FIELD_MODIFY(APPLE_DVFS_CMD_PS2, ®, pstate); + reg |= APPLE_DVFS_CMD_SET; + + writeq_relaxed(reg, priv->reg_base + APPLE_DVFS_CMD); +} + /* - * Requests the frequency table entry @index. With @wait, a SoC that verifies - * transitions waits for this one to be accepted; without it, the next request - * checks it: it waits for BUSY to clear and requires the accepted state to - * match. + * Requests the frequency table entry @index and, on a SoC that verifies + * transitions, waits for the cluster to accept it. */ static int apple_soc_cpufreq_request(struct cpufreq_policy *policy, - unsigned int index, bool wait) + unsigned int index) { struct apple_cpu_priv *priv = policy->driver_data; unsigned int pstate = policy->freq_table[index].driver_data; @@ -431,15 +468,9 @@ static int apple_soc_cpufreq_request(struct cpufreq_policy *policy, return 0; } - reg &= ~priv->info->ps1_mask; - reg |= pstate << priv->info->ps1_shift; - if (priv->info->has_ps2) - FIELD_MODIFY(APPLE_DVFS_CMD_PS2, ®, pstate); - reg |= APPLE_DVFS_CMD_SET; - - writeq_relaxed(reg, priv->reg_base + APPLE_DVFS_CMD); + apple_soc_cpufreq_write(priv, reg, pstate); - if (priv->info->verify_transition && wait && + if (priv->info->verify_transition && readq_poll_timeout_atomic(priv->reg_base + APPLE_DVFS_CMD, reg, !(reg & APPLE_DVFS_CMD_BUSY) && FIELD_GET(APPLE_DVFS_CMD_PS1, reg) == pstate, @@ -456,10 +487,210 @@ static int apple_soc_cpufreq_request(struct cpufreq_policy *policy, return 0; } +/* + * Lazy verification (lazy_verify SoCs) + * + * A transition takes about 50 us. The scheduler's fast-switch path must not + * wait for one, nor drop a request: schedutil records the frequency it asked + * for and does not ask again for the same one. + * + * A request finding the cluster idle requires the accepted state to be the + * one last written, so verifying the previous request, and is written at + * once. The verify timer checks the last request written in the same way + * within APPLE_DVFS_VERIFY_LAZY_NS plus slack, so the last request of a burst + * is verified like every other; a transition still busy then is followed + * until it finishes or its timeout has passed. + * + * A request finding the cluster busy is kept as pending, replacing an older + * pending one. The timer writes it as soon as the transition has finished, + * after the same check, and then verifies it as above. + * + * A failure stops all further requests, as for a request that is waited for, + * and the timer reports it. + * + * Called with priv->lock held. + */ +static void apple_soc_cpufreq_fail(struct apple_cpu_priv *priv, u64 reg) +{ + priv->transition_failed = true; + priv->pending_pstate = -1; + /* The scheduler may hold a runqueue lock: the timer reports it. */ + priv->failure_unreported = true; + priv->failure_command = reg; +} + +/* Reports a failure recorded by apple_soc_cpufreq_fail(), once. */ +static void apple_soc_cpufreq_report(struct apple_cpu_priv *priv, u64 reg, + unsigned int expected) +{ + if (reg & APPLE_DVFS_CMD_BUSY) + dev_err(priv->cpu_dev, + "DVFS busy timeout, command=%#llx; stopping requests\n", reg); + else + dev_err(priv->cpu_dev, + "unexpected DVFS state %u (expected %u); stopping requests\n", + (unsigned int)FIELD_GET(APPLE_DVFS_CMD_PS1, reg), expected); +} + +/* + * Writes @pstate if it differs from the accepted state, given the idle + * command register @reg, and returns whether a transition is now in flight. + * Called with priv->lock held. + */ +static bool apple_soc_cpufreq_lazy_write(struct apple_cpu_priv *priv, u64 reg, + unsigned int pstate) +{ + if (FIELD_GET(APPLE_DVFS_CMD_PS1, reg) != priv->expected_pstate) { + apple_soc_cpufreq_fail(priv, reg); + return false; + } + if (pstate == priv->expected_pstate) + return false; + + apple_soc_cpufreq_write(priv, reg, pstate); + priv->expected_pstate = pstate; + priv->write_time = ktime_get(); + priv->verify_deadline = ktime_add_us(priv->write_time, + apple_soc_cpufreq_timeout(priv->info)); + return true; +} + +/* Called with priv->lock held. */ +static void apple_soc_cpufreq_verify_later(struct apple_cpu_priv *priv) +{ + hrtimer_start_range_ns(&priv->verify_timer, + ns_to_ktime(APPLE_DVFS_VERIFY_LAZY_NS), + APPLE_DVFS_VERIFY_SLACK_NS, HRTIMER_MODE_REL_HARD); +} + +static enum hrtimer_restart apple_soc_cpufreq_verify(struct hrtimer *timer) +{ + struct apple_cpu_priv *priv = container_of(timer, struct apple_cpu_priv, + verify_timer); + unsigned int expected; + bool report = false; + unsigned long flags; + u64 reg; + + raw_spin_lock_irqsave(&priv->lock, flags); + if (priv->transition_failed) + goto out; + + /* + * Re-armed with hrtimer_start(), never forwarded: a request may have + * queued the timer again while this callback waited for the lock. + */ + reg = readq_relaxed(priv->reg_base + APPLE_DVFS_CMD); + if (reg & APPLE_DVFS_CMD_BUSY) { + ktime_t now = ktime_get(); + + if (ktime_after(now, priv->verify_deadline)) + apple_soc_cpufreq_fail(priv, reg); + else if (priv->pending_pstate >= 0) + hrtimer_start(timer, ns_to_ktime(APPLE_DVFS_RETRY_NS), + HRTIMER_MODE_REL_HARD); + else + hrtimer_start(timer, priv->verify_deadline, + HRTIMER_MODE_ABS_HARD); + } else if (priv->pending_pstate >= 0) { + unsigned int pstate = priv->pending_pstate; + + priv->pending_pstate = -1; + if (apple_soc_cpufreq_lazy_write(priv, reg, pstate)) + apple_soc_cpufreq_verify_later(priv); + } else if (FIELD_GET(APPLE_DVFS_CMD_PS1, reg) != priv->expected_pstate) { + apple_soc_cpufreq_fail(priv, reg); + } +out: + if (priv->failure_unreported) { + priv->failure_unreported = false; + report = true; + reg = priv->failure_command; + expected = priv->expected_pstate; + } + raw_spin_unlock_irqrestore(&priv->lock, flags); + if (report) + apple_soc_cpufreq_report(priv, reg, expected); + return HRTIMER_NORESTART; +} + +/* + * Requests the frequency table entry @index from the scheduler's fast-switch + * path without waiting, see "Lazy verification" above. + */ +static int apple_soc_cpufreq_lazy_request(struct cpufreq_policy *policy, + unsigned int index) +{ + struct apple_cpu_priv *priv = policy->driver_data; + unsigned int pstate = policy->freq_table[index].driver_data; + unsigned long flags; + int ret = 0; + u64 reg; + + if (pstate < priv->info->min_pstate || pstate > priv->info->max_pstate) + return -EINVAL; + + raw_spin_lock_irqsave(&priv->lock, flags); + if (priv->transition_failed) { + ret = -EIO; + goto out; + } + + reg = readq_relaxed(priv->reg_base + APPLE_DVFS_CMD); + if (reg & APPLE_DVFS_CMD_BUSY) { + /* Retry about when the transition should have finished. */ + ktime_t now = ktime_get(); + ktime_t at = ktime_add_ns(priv->write_time, APPLE_DVFS_TRANSITION_NS); + + if (ktime_before(at, ktime_add_ns(now, APPLE_DVFS_RETRY_NS))) + at = ktime_add_ns(now, APPLE_DVFS_RETRY_NS); + priv->pending_pstate = pstate; + if (!hrtimer_active(&priv->verify_timer) && + ktime_after(now, priv->verify_deadline)) + priv->verify_deadline = + ktime_add_us(now, apple_soc_cpufreq_timeout(priv->info)); + if (!hrtimer_is_queued(&priv->verify_timer) || + ktime_after(hrtimer_get_softexpires(&priv->verify_timer), at)) + hrtimer_start(&priv->verify_timer, at, HRTIMER_MODE_ABS_HARD); + goto out; + } + + /* An idle cluster has accepted, or refused, the previous request. */ + priv->pending_pstate = -1; + if (apple_soc_cpufreq_lazy_write(priv, reg, pstate)) { + if (!hrtimer_is_queued(&priv->verify_timer)) + apple_soc_cpufreq_verify_later(priv); + } else if (priv->transition_failed) { + /* Let the timer report it, outside the scheduler. */ + hrtimer_start(&priv->verify_timer, 0, HRTIMER_MODE_REL_HARD); + ret = -EIO; + } +out: + raw_spin_unlock_irqrestore(&priv->lock, flags); + return ret; +} + static int apple_soc_cpufreq_set_target(struct cpufreq_policy *policy, unsigned int index) { - return apple_soc_cpufreq_request(policy, index, true); + struct apple_cpu_priv *priv = policy->driver_data; + unsigned long flags; + int ret; + + if (!priv->info->lazy_verify) + return apple_soc_cpufreq_request(policy, index); + + /* + * Waited-for requests (suspend, the static governors) replace any + * pending one; the lock keeps a late fast-switch request out until + * this one is accepted. + */ + hrtimer_cancel(&priv->verify_timer); + raw_spin_lock_irqsave(&priv->lock, flags); + priv->pending_pstate = -1; + ret = apple_soc_cpufreq_request(policy, index); + raw_spin_unlock_irqrestore(&priv->lock, flags); + return ret; } static unsigned int apple_soc_cpufreq_fast_switch(struct cpufreq_policy *policy, @@ -472,9 +703,13 @@ static unsigned int apple_soc_cpufreq_fast_switch(struct cpufreq_policy *policy, */ int idx = cpufreq_table_find_index_l(policy, target_freq, false); - if (idx < 0 || apple_soc_cpufreq_request(policy, idx, !priv->info->lazy_verify) < 0) + if (idx < 0) + return 0; + if (priv->info->lazy_verify ? apple_soc_cpufreq_lazy_request(policy, idx) : + apple_soc_cpufreq_request(policy, idx)) return 0; + /* A lazy request is committed: the cluster applies it within microseconds. */ return policy->freq_table[idx].frequency; } @@ -1012,6 +1247,10 @@ static int apple_soc_cpufreq_init(struct cpufreq_policy *policy) priv->cpu_dev = cpu_dev; priv->reg_base = reg_base; priv->info = info; + raw_spin_lock_init(&priv->lock); + hrtimer_setup(&priv->verify_timer, apple_soc_cpufreq_verify, CLOCK_MONOTONIC, + HRTIMER_MODE_REL_HARD); + priv->pending_pstate = -1; policy->driver_data = priv; policy->freq_table = freq_table; @@ -1068,6 +1307,8 @@ static void apple_soc_cpufreq_exit(struct cpufreq_policy *policy) { struct apple_cpu_priv *priv = policy->driver_data; + /* The governor has stopped: no request re-arms the verify timer. */ + hrtimer_cancel(&priv->verify_timer); /* Drain firmware-limit updates before releasing the OPP tables. */ apple_fast_die_exit(priv); From 76d4e880149e5592396001f728f7803efb616377 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 25 Oct 2026 20:29:35 -0400 Subject: [PATCH 085/388] drm/asahi: build the G17 compute QID budget test against the variant constructor The test that grows compute QIDs with free identities still called Registry::new() with the clusters and descriptor flags only, from before the constructor took the deflake setting and the firmware variant, so a KUnit build of the driver failed. Construct the registry as the other tests do, for one cluster on the 26A428 firmware. --- drivers/gpu/drm/asahi/g17/runtime.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/gpu/drm/asahi/g17/runtime.rs b/drivers/gpu/drm/asahi/g17/runtime.rs index 1600c55eaf47b..c5675cbec5f79 100644 --- a/drivers/gpu/drm/asahi/g17/runtime.rs +++ b/drivers/gpu/drm/asahi/g17/runtime.rs @@ -2547,7 +2547,7 @@ mod shared_capacity_tests { #[test] fn compute_grows_with_free_qids_and_narrows_queue_admission() { - let mut registry = KBox::init(Registry::new(1, [0; 2]), GFP_KERNEL).unwrap(); + let mut registry = KBox::init(Registry::new(1, Deflake::ONE_CLUSTER, [0; 2], &V26A428), GFP_KERNEL).unwrap(); let mut slots = KVec::with_capacity(20, GFP_KERNEL).unwrap(); for _ in 0..20 { slots.push(registry.admission.reserve_queue().unwrap(), GFP_KERNEL).unwrap(); From d0da7e79dc9c99020d109e95d18256cd5b3d6bc2 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 25 Oct 2026 20:35:37 -0400 Subject: [PATCH 086/388] drm/asahi: check the G17 QID floor test against the open-queue compute budget The test that a firmware QID floor admits only the render pairs it leaves reserved every compute channel before opening any logical queue. Compute QIDs are now sized by the open logical queues, so on 25G83's floor that first reservation was refused and the test panicked. Open the 42 logical queues the floor allows without the up-front compute reservations, and check as before that the next one is refused when it is created. --- drivers/gpu/drm/asahi/g17/runtime.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/runtime.rs b/drivers/gpu/drm/asahi/g17/runtime.rs index c5675cbec5f79..e04295d576606 100644 --- a/drivers/gpu/drm/asahi/g17/runtime.rs +++ b/drivers/gpu/drm/asahi/g17/runtime.rs @@ -2508,9 +2508,6 @@ mod shared_capacity_tests { GFP_KERNEL, ) .unwrap(); - for owner in 1..=super::super::kick::COMPUTE_CHANNELS as u64 { - registry.reserve_compute(owner).unwrap(); - } let mut slots = KVec::with_capacity(limit as usize, GFP_KERNEL).unwrap(); let mut leases = KVec::with_capacity(limit as usize, GFP_KERNEL).unwrap(); for owner in 1..=u64::from(limit) { From bf2c191bb3ccb937bec9d2a5cfeaf66c4317786d Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 00:23:11 +1100 Subject: [PATCH 087/388] drm/apple: share panel fades and gate T6041 adaptive sync Use the shared 250 ms nits ramp on desktop 26.6 panels and account brightness only after the matching accepted present completes. Keep the loader level until a request, support zero nits, and retain the restore target across blanking and power changes. Validate internal refresh ranges without losing their fixed timings, retain fractional rates and sync polarity from TimingElements, and keep desktop presentation deadlines for fixed ProMotion. Add opt-in adaptive sync with fixed-refresh retry and watchdog recovery, leaving the established fixed-refresh default intact. Exercise ramps, real mode parsing, ranges and adaptive-sync callback failures in the host runner, and extend the backlight and mode KUnit suites. Assisted-by: LLM Signed-off-by: Ryan Murray --- Documentation/gpu/apple-t6041-panel.rst | 29 ++ drivers/gpu/drm/apple/dcp-internal.h | 11 + drivers/gpu/drm/apple/dcp-refresh.h | 36 +++ drivers/gpu/drm/apple/dcp.c | 17 +- drivers/gpu/drm/apple/dcp_backlight.c | 126 ++++++-- drivers/gpu/drm/apple/dcp_backlight.h | 49 +++ drivers/gpu/drm/apple/dcp_backlight_test.c | 36 +++ drivers/gpu/drm/apple/iomfb.c | 28 +- drivers/gpu/drm/apple/iomfb_template.c | 147 ++++++--- drivers/gpu/drm/apple/parser.c | 47 ++- drivers/gpu/drm/apple/parser_mode_test.c | 28 ++ .../selftests/drivers/gpu/apple-dcp/README | 11 + .../drivers/gpu/apple-dcp/display.py | 278 ++++++++++++++++++ .../selftests/drivers/gpu/apple-dcp/rate.py | 2 + .../selftests/drivers/gpu/apple-dcp/run.py | 7 +- 15 files changed, 760 insertions(+), 92 deletions(-) create mode 100644 Documentation/gpu/apple-t6041-panel.rst create mode 100644 drivers/gpu/drm/apple/dcp-refresh.h create mode 100644 tools/testing/selftests/drivers/gpu/apple-dcp/display.py diff --git a/Documentation/gpu/apple-t6041-panel.rst b/Documentation/gpu/apple-t6041-panel.rst new file mode 100644 index 0000000000000..ba1b9de0f4094 --- /dev/null +++ b/Documentation/gpu/apple-t6041-panel.rst @@ -0,0 +1,29 @@ +T6041 internal panel +=================== + +Desktop 26.6 firmware uses its own IOMFB instance. Panel brightness is in +integer nits from zero to the panel ceiling, with the same 250 ms fade used +by the H17P panel. The backlight interface reports the requested target and +``actual_brightness`` follows accepted, completed presents. A registration +default does not change the brightness inherited from the boot loader. +Ambient-light policy can use the normal backlight interface, just as a slider +does. Requests made while blanked or suspended preserve the restore target. + +Fixed refresh modes keep the timing and color IDs supplied by DCP. The driver +does not synthesize lower refresh modes from a 120 Hz mode. Fractional rates +remain in 16.16 Hz until the DRM pixel clock is calculated. A ProMotion mode +needs desktop presentation deadlines even with adaptive sync disabled. + +Adaptive sync is experimental and disabled by default. To qualify the internal +panel, boot with ``appledrm.t6041_vrr=1``. This enables the connector capability +only for the internal main display and a valid adaptive-sync range. The +existing internal ProMotion convention supplies 24 Hz to the mode rate when +the firmware omits both range fields; an explicit partial or invalid range +leaves the timing usable at fixed refresh. + +A rejected adaptive-sync parameter or mode selection gets one retry with +adaptive sync disabled. An adaptive swap that stops completing invokes the +swap watchdog and re-applies the active CRTC at fixed refresh. Either failure +latches adaptive sync off until reboot. A transport that stops replying cannot +be recovered by issuing another command on its busy channel; it still requires +a reboot. Use the previous kernel entry if a panel fails qualification. diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index 5950b5883d28f..2f6e107fa43e8 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -228,6 +228,9 @@ enum dcp_test_point { * for such a DCP only. */ struct dcp_desktop_state { + /* Protected by backlight.lock, independent of framebuffer retirement. */ + struct dcp_backlight_swap backlight; + /* Queued swap; too large for the shared swap union. */ struct dcp_swap_submit_req_v26_6_desktop swap; /* Opaque client identity the firmware's IOMFB calls carry. */ @@ -238,6 +241,11 @@ struct dcp_desktop_state { * element before the real one is selected. */ bool mode_live; + /* An opt-in adaptive-sync failure stays fixed until the next boot. */ + bool vrr_failed; + bool vrr_requested; + u32 present_id; + bool present_pending; /* * GAMMA_LUT: the firmware's own table, read before the first change; * the table being sent; the LUT it is composed from, if any. @@ -287,6 +295,8 @@ struct dcp_desktop_state { struct dcpext_scanout; struct dcp_hdmi; +bool dcp_desktop_vrr_supported(struct apple_dcp *dcp); + struct apple_dcp { /* Generation-specific wire/transaction state, sharing the common KMS owner. */ struct iomfb_m4 *v26_6; @@ -669,6 +679,7 @@ struct apple_dcp { void (*kick)(struct apple_dcp *dcp); /* Steps a running fade; fade_start is its start in jiffies. */ struct delayed_work fade_work; + struct delayed_work retry_work; unsigned long fade_start; /* Rate limit for firmware re-send requests. */ unsigned long resent_at; diff --git a/drivers/gpu/drm/apple/dcp-refresh.h b/drivers/gpu/drm/apple/dcp-refresh.h new file mode 100644 index 0000000000000..eea5058cc3645 --- /dev/null +++ b/drivers/gpu/drm/apple/dcp-refresh.h @@ -0,0 +1,36 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ +/* Copyright 2026 Ryan Murray */ +#ifndef __APPLE_DCP_REFRESH_H__ +#define __APPLE_DCP_REFRESH_H__ + +#include + +/* All rates are unsigned 16.16 Hz, including fractional fixed timings. */ +static inline bool dcp_refresh_range(u32 rate, u32 minimum, u32 maximum, + bool internal, u32 *low, u32 *high) +{ + if (!minimum && !maximum && internal && + rate >= (120U << 16) - (120U << 16) / 500 && + rate <= (120U << 16)) { + minimum = 24U << 16; + maximum = rate; + } + /* A partial, inverted or out-of-mode range leaves a fixed timing. */ + if (minimum < (1U << 16) || minimum >= maximum || + maximum > rate || maximum > (65535U << 16)) + return false; + *low = minimum; + *high = maximum; + return true; +} + +/* + * Desktop 26.6 uses its own presentation clock. A deadline long past asks + * for the next permitted refresh. ARM counter values can stall the swap. + */ +static inline u64 dcp_desktop_present_deadline(bool have_surface, bool required) +{ + return have_surface && required ? 120 : 0; +} + +#endif diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 8a2e2c5bcbf10..03815b0f86e6b 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -909,9 +909,22 @@ VISIBLE_IF_KUNIT void dcp_swap_watchdog(struct work_struct *work) container_of(to_delayed_work(work), struct apple_dcp, swap_watchdog_wq); + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && + (!READ_ONCE(dcp->desktop->present_pending) || + time_before(jiffies, dcp->swap_watchdog_deadline))) + return; + if (!iomfb_retire_dropped_present_h17p(dcp)) return; + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && + READ_ONCE(dcp->vrr_enabled)) { + WRITE_ONCE(dcp->desktop->vrr_failed, true); + WRITE_ONCE(dcp->vrr_enabled, false); + dcp_backlight_desktop_finish(dcp, + READ_ONCE(dcp->desktop->present_id), true, false); + dev_warn(dcp->dev, "adaptive swap timed out, restoring fixed refresh\n"); + } dev_warn(dcp->dev, "swap not completed, retraining the display\n"); dcp_mode_invalidate(&dcp->mode_state); dcp_drm_crtc_vblank(dcp->crtc); @@ -922,7 +935,9 @@ VISIBLE_IF_KUNIT void dcp_swap_watchdog(struct work_struct *work) void dcp_swap_watchdog_arm(struct apple_dcp *dcp) { - if (dcp_is_typec_output(dcp)) { + if (dcp_is_typec_output(dcp) || + (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && + READ_ONCE(dcp->vrr_enabled))) { dcp->swap_watchdog_deadline = jiffies + msecs_to_jiffies(DCP_SWAP_WATCHDOG_MS); mod_delayed_work(system_dfl_wq, &dcp->swap_watchdog_wq, diff --git a/drivers/gpu/drm/apple/dcp_backlight.c b/drivers/gpu/drm/apple/dcp_backlight.c index e9bc471654cde..d32f52f11890b 100644 --- a/drivers/gpu/drm/apple/dcp_backlight.c +++ b/drivers/gpu/drm/apple/dcp_backlight.c @@ -104,6 +104,18 @@ bool dcp_backlight_active(struct apple_dcp *dcp) READ_ONCE(dcp->backlight.state.ready); } +static bool dcp_backlight_desktop(struct apple_dcp *dcp) +{ + return dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && + READ_ONCE(dcp->backlight.state.ready); +} + +static void dcp_backlight_desktop_kick(struct apple_dcp *dcp) +{ + if (!READ_ONCE(dcp->quiescing) && !READ_ONCE(dcp->crashed)) + schedule_work(&dcp->bl_update_wq); +} + int dcp_backlight_takeover(struct apple_dcp *dcp, u32 millinits) { u32 maximum, nits; @@ -130,7 +142,7 @@ static int dcp_get_brightness(struct backlight_device *bd) if (READ_ONCE(dcp->quiescing)) return -ENODEV; - if (dcp_backlight_active(dcp)) { + if (dcp_backlight_active(dcp) || dcp_backlight_desktop(dcp)) { spin_lock_irqsave(&dcp->backlight.lock, flags); actual = dcp->backlight.state.actual; spin_unlock_irqrestore(&dcp->backlight.lock, flags); @@ -227,9 +239,19 @@ static void dcp_backlight_fade_work(struct work_struct *work) msecs_to_jiffies(DCP_BL_FADE_STEP_MS)); } +static void dcp_backlight_retry_work(struct work_struct *work) +{ + struct apple_dcp *dcp = container_of(to_delayed_work(work), + struct apple_dcp, backlight.retry_work); + + if (!READ_ONCE(dcp->quiescing) && !READ_ONCE(dcp->crashed)) + dcp_backlight_kick(dcp); +} + void dcp_backlight_stop(struct apple_dcp *dcp) { cancel_delayed_work_sync(&dcp->backlight.fade_work); + cancel_delayed_work_sync(&dcp->backlight.retry_work); } static void dcp_backlight_release(void *data) @@ -246,6 +268,7 @@ int dcp_backlight_init(struct apple_dcp *dcp, struct device *dev) { spin_lock_init(&dcp->backlight.lock); INIT_DELAYED_WORK(&dcp->backlight.fade_work, dcp_backlight_fade_work); + INIT_DELAYED_WORK(&dcp->backlight.retry_work, dcp_backlight_retry_work); return devm_add_action_or_reset(dev, dcp_backlight_release, dcp); } @@ -350,6 +373,64 @@ unsigned int dcp_backlight_retry_delay(struct apple_dcp *dcp) return delay; } +/* Power changes jump; a target chosen while off is restored on enable. */ +void dcp_backlight_desktop_power(struct apple_dcp *dcp, bool on) +{ + unsigned long flags; + + if (!dcp_backlight_desktop(dcp)) + return; + if (!on) + dcp_backlight_stop(dcp); + spin_lock_irqsave(&dcp->backlight.lock, flags); + dcp->backlight.state.dpms_off = !on; + if (dcp->backlight.state.controlled) + dcp_bl_jump(&dcp->backlight.state); + spin_unlock_irqrestore(&dcp->backlight.lock, flags); +} + +/* Reserve only after swap_start succeeds, using its actual firmware ID. */ +bool dcp_backlight_desktop_prepare(struct apple_dcp *dcp, u32 id, + bool have_surface, u32 *nits) +{ + unsigned long flags; + int ret; + + if (!dcp_backlight_desktop(dcp)) + return false; + spin_lock_irqsave(&dcp->backlight.lock, flags); + ret = dcp_bl_swap_prepare(&dcp->backlight.state, + &dcp->desktop->backlight, id, have_surface); + if (!ret) + *nits = dcp->desktop->backlight.present.nits; + spin_unlock_irqrestore(&dcp->backlight.lock, flags); + return !ret; +} + +void dcp_backlight_desktop_finish(struct apple_dcp *dcp, u32 id, + bool reply, bool accepted) +{ + unsigned long flags; + unsigned int delay; + bool finished, pending; + + if (!dcp_backlight_desktop(dcp)) + return; + spin_lock_irqsave(&dcp->backlight.lock, flags); + finished = dcp_bl_swap_finish(&dcp->backlight.state, + &dcp->desktop->backlight, id, reply, accepted); + delay = dcp_bl_retry_delay(&dcp->backlight.state); + pending = dcp->backlight.state.dirty; + spin_unlock_irqrestore(&dcp->backlight.lock, flags); + if (!finished || READ_ONCE(dcp->quiescing) || READ_ONCE(dcp->crashed)) + return; + if (delay) + mod_delayed_work(system_dfl_wq, &dcp->backlight.retry_work, + msecs_to_jiffies(delay)); + else if (pending && (!reply || accepted)) + dcp_backlight_kick(dcp); +} + #define SCALE_FACTOR (1 << 10) static u32 interpolate(int val, int min, int max, u32 *tbl, size_t tbl_size) @@ -416,7 +497,8 @@ static int drm_crtc_set_brightness(struct apple_dcp *dcp) if (ret) goto out; - if (!dcp->brightness.update) + if (dcp_backlight_desktop(dcp) ? !dcp_backlight_pending(dcp) : + !dcp->brightness.update) goto out; crtc_state = drm_atomic_get_crtc_state(commit, crtc); @@ -425,6 +507,10 @@ static int drm_crtc_set_brightness(struct apple_dcp *dcp) goto out; } + /* A request made while DPMS is off is restored on the enable swap. */ + if (dcp_backlight_desktop(dcp) && !crtc_state->active) + goto out; + /* Re-present the current scanout when brightness is the only change. */ ret = drm_atomic_add_affected_planes(commit, crtc); if (ret) @@ -471,7 +557,8 @@ int dcp_backlight_update(struct apple_dcp *dcp) /* Wait 1 vblank cycle in the hope an atomic swap has already updated * the brightness */ - msleep((1001 + 23) / 24); // 42ms for 23.976 fps + if (!dcp_backlight_desktop(dcp)) + msleep((1001 + 23) / 24); // 42ms for 23.976 fps return drm_crtc_set_brightness(dcp); } @@ -486,7 +573,7 @@ static int dcp_set_brightness(struct backlight_device *bd) if (READ_ONCE(dcp->quiescing)) return -ENODEV; - if (dcp_backlight_active(dcp)) { + if (dcp_backlight_active(dcp) || dcp_backlight_desktop(dcp)) { struct dcp_backlight_state *state = &dcp->backlight.state; bool fade; u32 old; @@ -510,24 +597,11 @@ static int dcp_set_brightness(struct backlight_device *bd) return ret; } - /* - * Desktop 26.x swaps carry nits. A level of 0 must not turn the - * panel off (DPMS does that); only a blanked panel still gets 0. - */ - if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && !brightness && - !bd->props.brightness) - brightness = min(bd->props.max_brightness, 2); - ret = drm_modeset_lock_single_interruptible(&dcp->crtc->base.mutex); if (ret) return ret; - if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) { - dcp->brightness.nits = brightness; - dcp->brightness.requested = true; - } else { - dcp->brightness.dac = calculate_dac(dcp, brightness); - } + dcp->brightness.dac = calculate_dac(dcp, brightness); dcp->brightness.update = true; drm_modeset_unlock(&dcp->crtc->base.mutex); @@ -591,7 +665,21 @@ int dcp_backlight_register(struct apple_dcp *dcp) if (!props.brightness) props.brightness = min_t(u32, DCP_DESKTOP_DEFAULT_NITS, props.max_brightness); - dcp->brightness.nits = props.brightness; + spin_lock_irqsave(&dcp->backlight.lock, flags); + if (!dcp->backlight.state.ready) { + int ret = dcp_bl_init(&dcp->backlight.state, + props.max_brightness, false, 0, + true, props.brightness); + + if (ret) { + spin_unlock_irqrestore(&dcp->backlight.lock, flags); + return ret; + } + dcp->backlight.state.fade = true; + dcp->backlight.kick = dcp_backlight_desktop_kick; + } + props.brightness = dcp->backlight.state.target; + spin_unlock_irqrestore(&dcp->backlight.lock, flags); } bl_dev = devm_backlight_device_register(dev, "apple-panel-bl", dev, dcp, diff --git a/drivers/gpu/drm/apple/dcp_backlight.h b/drivers/gpu/drm/apple/dcp_backlight.h index 92ac07b2e9d9d..a1ac47f166678 100644 --- a/drivers/gpu/drm/apple/dcp_backlight.h +++ b/drivers/gpu/drm/apple/dcp_backlight.h @@ -272,8 +272,57 @@ static inline unsigned int dcp_bl_retry_delay(const struct dcp_backlight_state * return 100U << (state->retries - 1); } +/* A desktop present can complete before its submit reply arrives. */ +struct dcp_backlight_swap { + struct dcp_backlight_present present; + u32 id; + bool pending; + bool accepted; + bool completed; +}; + +static inline int dcp_bl_swap_prepare(struct dcp_backlight_state *state, + struct dcp_backlight_swap *swap, + u32 id, bool have_surface) +{ + struct dcp_backlight_present present; + int ret = dcp_bl_prepare(state, have_surface, &present); + + if (ret) + return ret; + *swap = (struct dcp_backlight_swap) { + .present = present, + .id = id, + .pending = true, + }; + return 0; +} + +static inline bool dcp_bl_swap_finish(struct dcp_backlight_state *state, + struct dcp_backlight_swap *swap, + u32 id, bool reply, bool accepted) +{ + if (!swap->pending || swap->id != id) + return false; + if (reply) + swap->accepted = accepted; + else + swap->completed = true; + if ((!reply || accepted) && !(swap->accepted && swap->completed)) + return false; + swap->pending = false; + return dcp_bl_complete(state, swap->present.sequence, + swap->accepted && swap->completed); +} + struct apple_dcp; +void dcp_backlight_desktop_power(struct apple_dcp *dcp, bool on); +bool dcp_backlight_desktop_prepare(struct apple_dcp *dcp, u32 id, + bool have_surface, u32 *nits); +void dcp_backlight_desktop_finish(struct apple_dcp *dcp, u32 id, + bool reply, bool accepted); + /* * kick only queues work; it must not submit a top-level RPC from the caller. * The central IOMFB queue owns surface references, retries, and wire encoding. diff --git a/drivers/gpu/drm/apple/dcp_backlight_test.c b/drivers/gpu/drm/apple/dcp_backlight_test.c index 70379571a5dd3..61cf775c683fc 100644 --- a/drivers/gpu/drm/apple/dcp_backlight_test.c +++ b/drivers/gpu/drm/apple/dcp_backlight_test.c @@ -501,6 +501,41 @@ static void backlight_fade_blank_test(struct kunit *test) KUNIT_EXPECT_EQ(test, dcp_bl_effective(&state), 400U); } +static void backlight_desktop_swap_test(struct kunit *test) +{ + struct dcp_backlight_state state; + struct dcp_backlight_swap swap = {}; + + KUNIT_ASSERT_EQ(test, dcp_bl_init(&state, 600, true, 300, false, 0), 0); + state.fade = true; + KUNIT_ASSERT_EQ(test, dcp_bl_request(&state, 600, false, false), 0); + KUNIT_EXPECT_TRUE(test, dcp_bl_fade_step(&state, 125, 250)); + KUNIT_EXPECT_EQ(test, state.level, 450U); + KUNIT_ASSERT_EQ(test, dcp_bl_swap_prepare(&state, &swap, 42, true), 0); + /* A completion alone must not publish an unaccepted panel level. */ + KUNIT_EXPECT_FALSE(test, dcp_bl_swap_finish(&state, &swap, 42, false, true)); + KUNIT_EXPECT_EQ(test, state.actual, 300U); + KUNIT_EXPECT_FALSE(test, dcp_bl_swap_finish(&state, &swap, 41, true, true)); + KUNIT_EXPECT_TRUE(test, dcp_bl_swap_finish(&state, &swap, 42, true, true)); + KUNIT_EXPECT_EQ(test, state.actual, 450U); + KUNIT_EXPECT_FALSE(test, dcp_bl_swap_finish(&state, &swap, 42, true, false)); + KUNIT_EXPECT_FALSE(test, dcp_bl_fade_step(&state, 250, 250)); + KUNIT_ASSERT_EQ(test, dcp_bl_swap_prepare(&state, &swap, 43, true), 0); + KUNIT_EXPECT_TRUE(test, dcp_bl_swap_finish(&state, &swap, 43, true, false)); + KUNIT_EXPECT_EQ(test, state.actual, 450U); + KUNIT_EXPECT_EQ(test, dcp_bl_retry_delay(&state), 100U); + KUNIT_ASSERT_EQ(test, dcp_bl_swap_prepare(&state, &swap, 44, true), 0); + KUNIT_EXPECT_FALSE(test, dcp_bl_swap_finish(&state, &swap, 44, true, true)); + KUNIT_EXPECT_TRUE(test, dcp_bl_swap_finish(&state, &swap, 44, false, true)); + KUNIT_EXPECT_EQ(test, state.actual, 600U); + /* A lit zero is a valid request, with the same fade as the Neo. */ + KUNIT_ASSERT_EQ(test, dcp_bl_request(&state, 0, false, false), 0); + KUNIT_EXPECT_TRUE(test, dcp_bl_fade_step(&state, 125, 250)); + KUNIT_EXPECT_EQ(test, state.level, 300U); + KUNIT_EXPECT_FALSE(test, dcp_bl_fade_step(&state, 250, 250)); + KUNIT_EXPECT_EQ(test, state.level, 0U); +} + static struct kunit_case backlight_cases[] = { KUNIT_CASE(backlight_millinits_takeover_test), KUNIT_CASE(backlight_takeover_test), @@ -515,6 +550,7 @@ static struct kunit_case backlight_cases[] = { KUNIT_CASE(backlight_fallback_test), KUNIT_CASE(backlight_fallback_same_level_test), KUNIT_CASE(backlight_seed_known_test), + KUNIT_CASE(backlight_desktop_swap_test), KUNIT_CASE(backlight_fade_test), KUNIT_CASE(backlight_fade_restart_test), KUNIT_CASE(backlight_fade_blank_test), diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index c0bef660c25ec..2c93f24d5a006 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -90,15 +90,23 @@ static inline u64 dcpep_ack(enum dcp_context_id id) return dcpep_msg(id, 0, 0) | IOMFB_MSG_ACK; } -/* - * Adaptive sync through IOMFBParameter 14 black-screens the J616s panel on - * the desktop 26.6 firmware: the firmware accepts the VRR mode, then no swap - * completes. Do not offer it there. - */ +/* Keep the established fixed-refresh default until panel qualification. */ +static bool t6041_vrr; +module_param(t6041_vrr, bool, 0444); +MODULE_PARM_DESC(t6041_vrr, "Enable experimental adaptive sync on the T6041 internal panel"); + +bool dcp_desktop_vrr_supported(struct apple_dcp *dcp) +{ + return t6041_vrr && dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && + dcp_has_panel(dcp) && dcp->main_display && + !READ_ONCE(dcp->desktop->vrr_failed); +} + static bool dcp_mode_vrr_supported(struct apple_dcp *dcp, const struct dcp_display_mode *mode) { - return dcp->fw_compat != DCP_FIRMWARE_V_26_6_DESKTOP || !mode->vrr; + return dcp->fw_compat != DCP_FIRMWARE_V_26_6_DESKTOP || + dcp_desktop_vrr_supported(dcp); } /* @@ -1950,7 +1958,7 @@ int dcp_get_modes(struct drm_connector *connector) if (!dcp_mode_vrr_supported(dcp, &modes[i])) continue; if (modes[i].vrr) { - u16 lo = modes[i].min_vrr >> 16; + u16 lo = DIV_ROUND_UP(modes[i].min_vrr, 1U << 16); u16 hi = modes[i].max_vrr >> 16; if (!min_vfreq || lo < min_vfreq) @@ -1998,7 +2006,7 @@ static int dcp_desktop_modeset(struct apple_dcp *dcp, if (d->trial_modeset_ok) { d->trial_modeset_ok = false; - if (d->mode_live && lookup_mode(dcp, &crtc_state->mode, &matched) && + if (!crtc_state->vrr_enabled && d->mode_live && lookup_mode(dcp, &crtc_state->mode, &matched) && matched.timing_mode_id == d->trial_timing_mode_id && matched.color_mode_id == d->trial_color_mode_id) { dev_dbg(dcp->dev, "keeping the takeover mode " DRM_MODE_FMT "\n", @@ -2258,7 +2266,9 @@ int dcp_crtc_atomic_modeset(struct drm_crtc *crtc, return 0; modeset = drm_atomic_crtc_needs_modeset(crtc_state) || - !READ_ONCE(dcp->mode_state.valid); + !READ_ONCE(dcp->mode_state.valid) || + (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && + crtc_state->vrr_enabled != dcp->desktop->vrr_requested); if (!modeset) return 0; diff --git a/drivers/gpu/drm/apple/iomfb_template.c b/drivers/gpu/drm/apple/iomfb_template.c index 246498a8e843c..3d9465ee5561e 100644 --- a/drivers/gpu/drm/apple/iomfb_template.c +++ b/drivers/gpu/drm/apple/iomfb_template.c @@ -32,6 +32,7 @@ #include "iomfb.h" #include "iomfb_internal.h" #include "parser.h" +#include "dcp-refresh.h" #include "dcp-property.h" #include "trace.h" #include "version_utils.h" @@ -458,6 +459,11 @@ static void dcpep_cb_swap_complete(struct apple_dcp *dcp, } if (dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17G) retire = iomfb_present_complete_h17p(dcp); +#endif +#if DCP_FW_DESKTOP_26(DCP_FW_VER) + if (resp->swap_id == READ_ONCE(dcp->desktop->present_id)) + WRITE_ONCE(dcp->desktop->present_pending, false); + dcp_backlight_desktop_finish(dcp, resp->swap_id, false, true); #endif trace_iomfb_swap_complete(dcp, resp->swap_id); dcp->last_swap_id = resp->swap_id; @@ -1820,6 +1826,12 @@ static void dcp_swap_cleared(struct apple_dcp *dcp, void *data, void *cookie) if (ret) status = ~0U; +#if DCP_FW_DESKTOP_26(DCP_FW_VER) + if (status && swap_id == READ_ONCE(dcp->desktop->present_id)) + WRITE_ONCE(dcp->desktop->present_pending, false); + dcp_backlight_desktop_finish(dcp, swap_id, true, !status); +#endif + if (status) { /* An aborted present has already been reported. */ if (ret != -ECANCELED) @@ -2033,6 +2045,8 @@ void DCP_FW_NAME(iomfb_poweron)(struct apple_dcp *dcp) ret = wait_for_completion_timeout(&cookie->done, msecs_to_jiffies(10000)); #if DCP_FW_DESKTOP_26(DCP_FW_VER) dcp->desktop->poweron_result = ret > 0 ? 0 : ret == 0 ? 1 : 2; + if (ret > 0) + dcp_backlight_desktop_power(dcp, true); #endif if (ret == 0) { @@ -2154,6 +2168,9 @@ void DCP_FW_NAME(iomfb_poweroff)(struct apple_dcp *dcp) if (dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17G) dcp_prepare_clear_swap(dcp, swap); #else +#if DCP_FW_DESKTOP_26(DCP_FW_VER) + dcp_backlight_desktop_power(dcp, false); +#endif dcp_prepare_clear_swap(dcp, swap); #endif @@ -2566,6 +2583,12 @@ static void dcp_swapped(struct apple_dcp *dcp, void *data, void *cookie) if (ret) status = ~0U; +#if DCP_FW_DESKTOP_26(DCP_FW_VER) + if (status && swap_id == READ_ONCE(dcp->desktop->present_id)) + WRITE_ONCE(dcp->desktop->present_pending, false); + dcp_backlight_desktop_finish(dcp, swap_id, true, !status); +#endif + if (status) { /* An aborted present has already been reported. */ if (ret != -ECANCELED) @@ -2601,6 +2624,20 @@ static void dcp_swap_started(struct apple_dcp *dcp, void *data, void *cookie) if (dcp_present_retires(dcp)) dcp_arm_retained_framebuffers(dcp, resp->swap_id); +#if DCP_FW_DESKTOP_26(DCP_FW_VER) + WRITE_ONCE(dcp->desktop->present_id, resp->swap_id); + WRITE_ONCE(dcp->desktop->present_pending, true); + if (dcp_has_panel(dcp)) { + u32 nits; + bool have_surface = false; + + for (unsigned int i = 0; i < SWAP_SURFACES; i++) + have_surface |= !DCP_FW_SWAP(dcp).surf_null[i]; + if (dcp_backlight_desktop_prepare(dcp, resp->swap_id, + have_surface, &nits)) + dcp_swap_set_brightness(&DCP_FW_SWAP(dcp).swap, nits); + } +#endif trace_iomfb_swap_submit(dcp, resp->swap_id); dcp_h17p_prepare_swap(dcp); dcp_swap_submit(dcp, false, &DCP_FW_SWAP(dcp), dcp_swapped, NULL); @@ -2808,6 +2845,9 @@ struct dcp_modeset_cookie { struct dcp_set_digital_out_mode_req actual; struct dcp_set_digital_out_mode_req bootstrap; bool use_bootstrap; + bool vrr; + bool program_sync; + u32 min_vrr; }; /* @@ -2834,6 +2874,30 @@ static void dcp_get_mode_info(struct apple_dcp *dcp, sizeof(struct dcp_get_mode_info_resp_v26_6), &req, cb, cookie); } +static void dcp_modeset_select(struct apple_dcp *dcp, void *out, void *cookie); +static void dcp_modeset_sync(struct apple_dcp *dcp, void *cookie); + +static void dcp_modeset_fixed_fallback(struct apple_dcp *dcp, + struct dcp_modeset_cookie *modeset) +{ + dev_warn(dcp->dev, "adaptive sync failed, restoring fixed refresh\n"); + WRITE_ONCE(dcp->desktop->vrr_failed, true); + modeset->vrr = false; + dcp_modeset_sync(dcp, modeset); +} + +static void dcp_modeset_actual_done(struct apple_dcp *dcp, void *data, + void *cookie) +{ + struct dcp_modeset_cookie *modeset = cookie; + + if (data && get_unaligned_le32(data) && modeset->vrr) { + dcp_modeset_fixed_fallback(dcp, modeset); + return; + } + complete_set_digital_out_mode(dcp, data, cookie); +} + static void dcp_modeset_after_actual_info(struct apple_dcp *dcp, void *data, void *cookie) { @@ -2844,7 +2908,7 @@ static void dcp_modeset_after_actual_info(struct apple_dcp *dcp, void *data, return; } dcp_set_digital_out_mode(dcp, false, &modeset->actual, - complete_set_digital_out_mode, cookie); + dcp_modeset_actual_done, cookie); } static void dcp_modeset_after_bootstrap(struct apple_dcp *dcp, void *data, @@ -2886,6 +2950,36 @@ static void dcp_modeset_select(struct apple_dcp *dcp, void *out, void *cookie) dcp_modeset_after_actual_info, cookie); } +static void dcp_modeset_sync_done(struct apple_dcp *dcp, void *data, + void *cookie) +{ + struct dcp_modeset_cookie *modeset = cookie; + + if (!data || get_unaligned_le32(data)) { + if (data && modeset->vrr) { + dcp_modeset_fixed_fallback(dcp, modeset); + return; + } + complete_set_digital_out_mode(dcp, data, cookie); + return; + } + dcp_modeset_select(dcp, NULL, cookie); +} + +static void dcp_modeset_sync(struct apple_dcp *dcp, void *cookie) +{ + struct dcp_modeset_cookie *modeset = cookie; + struct dcp_set_parameter_dcp param; + + /* Match the desktop parameter record, including its unused padding. */ + memset(¶m, 0xaa, sizeof(param)); + param.param = IOMFBPARAM_ADAPTIVE_SYNC; + memset(param.value, 0, 3 * sizeof(u64)); + param.value[0] = modeset->vrr ? modeset->min_vrr : 0; + param.count = 3; + dcp_set_parameter_dcp(dcp, false, ¶m, dcp_modeset_sync_done, cookie); +} + int DCP_FW_NAME(iomfb_modeset)(struct apple_dcp *dcp, struct drm_crtc_state *crtc_state) { @@ -2927,11 +3021,9 @@ int DCP_FW_NAME(iomfb_modeset)(struct apple_dcp *dcp, .timing_mode_id = mode->timing_mode_id }; - /* - * Suspected VRR modes need swap timestamps, see iomfb_flush(). - * Adaptive sync itself stays off, see dcp_mode_vrr_supported(). - */ + /* ProMotion needs desktop timestamps in fixed-120 mode too. */ dcp->use_timestamps = mode->vrr; + dcp->desktop->vrr_requested = crtc_state->vrr_enabled; /* * The firmware changes a fixed panel's mode through the swap geometry, @@ -2949,6 +3041,12 @@ int DCP_FW_NAME(iomfb_modeset)(struct apple_dcp *dcp, return -ENOMEM; cookie = &modeset->wait; modeset->actual = dcp->mode; + modeset->program_sync = dcp_has_panel(dcp) && dcp->main_display && + (dcp_desktop_vrr_supported(dcp) || dcp->vrr_enabled || + READ_ONCE(dcp->desktop->vrr_failed)); + modeset->vrr = modeset->program_sync && mode->vrr && + crtc_state->vrr_enabled && dcp_desktop_vrr_supported(dcp); + modeset->min_vrr = mode->min_vrr; cookie->status = ~0U; init_completion(&cookie->done); @@ -2971,7 +3069,10 @@ int DCP_FW_NAME(iomfb_modeset)(struct apple_dcp *dcp, modeset->bootstrap.timing_mode_id); } - dcp_modeset_select(dcp, NULL, cookie); + if (modeset->program_sync) + dcp_modeset_sync(dcp, cookie); + else + dcp_modeset_select(dcp, NULL, cookie); /* * The DCP firmware has an internal timeout of ~8 seconds for @@ -3001,6 +3102,7 @@ int DCP_FW_NAME(iomfb_modeset)(struct apple_dcp *dcp, jiffies_to_msecs(ret)); ret = 0; } + dcp->vrr_enabled = !ret && modeset->vrr; kref_put(&cookie->refcount, release_wait_cookie); dcp->desktop->mode_live = !ret; @@ -3262,20 +3364,9 @@ void DCP_FW_NAME(iomfb_flush)(struct apple_dcp *dcp, struct drm_crtc *crtc, stru } #if DCP_FW_DESKTOP_26(DCP_FW_VER) - if (has_surface && dcp->use_timestamps) { - /* - * Timestamp types 1, 2 and 7 hold the time the swap should be - * shown, in the firmware's clock. A target long past shows it - * at the next refresh: 120 Hz in the fixed mode, as soon as - * the panel allows with adaptive sync. The architectural - * counter must not go here: after a VRR mode set the firmware - * then never shows the swap, and it holds a swap whose target - * it reads as more than about a frame ahead. - */ - req->swap.timestamp[0] = 120; - req->swap.timestamp[1] = 120; - req->swap.timestamp[6] = 120; - } + /* Types 1, 2 and 7 carry the same desktop presentation deadline. */ + req->swap.timestamp[0] = req->swap.timestamp[1] = req->swap.timestamp[6] = + dcp_desktop_present_deadline(has_surface, dcp->use_timestamps); #else if (has_surface && (dcp->use_timestamps || dcp->vrr_enabled)) { u64 submit_timestamp = dcp->swap_submit_timestamp; @@ -3310,20 +3401,8 @@ void DCP_FW_NAME(iomfb_flush)(struct apple_dcp *dcp, struct drm_crtc *crtc, stru dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17G; #endif #if DCP_FW_DESKTOP_26(DCP_FW_VER) - /* - * The firmware publishes no initial level, so nits stays at the - * registration default until userspace writes the backlight. Sending - * that from the forced post-poweron update can turn the panel off: - * keep the level the boot loader set until one is requested. - */ - if (update_brightness && !dcp->brightness.requested) { - dcp->brightness.update = false; - update_brightness = false; - } - if (update_brightness) { - dcp_swap_set_brightness(&req->swap, dcp->brightness.nits); - dcp->brightness.update = false; - } + /* The shared ramp is encoded after swap_start, at submission time. */ + dcp->brightness.update = false; #else if (update_brightness) { #if DCP_FW_LAYOUT_14_7(DCP_FW_VER) diff --git a/drivers/gpu/drm/apple/parser.c b/drivers/gpu/drm/apple/parser.c index cecad5a1fb866..612232bd394df 100644 --- a/drivers/gpu/drm/apple/parser.c +++ b/drivers/gpu/drm/apple/parser.c @@ -14,6 +14,7 @@ #endif #include "parser.h" +#include "dcp-refresh.h" #include "trace.h" #define DCP_PARSE_HEADER 0xd3 @@ -723,7 +724,8 @@ static int parse_mode(struct dcp_parse_ctx *handle, struct timing_color_restrictions limits = {}; struct dcp_color_mode unsafe_rgb8 = { .score = -1 }; bool have_horiz = false, have_vert = false, have_virtual = false; - bool duplicate_scope = false; + bool duplicate_scope = false, vrr_ambiguous = false; + unsigned int vrr_seen = 0; s64 min_vrr = 0, max_vrr = 0; u64 clock; s64 id = -1; @@ -773,16 +775,20 @@ static int parse_mode(struct dcp_parse_ctx *handle, else if (!strcmp(key, "HorizontalAttributes")) { duplicate_scope |= have_horiz; have_horiz = true; - ret = parse_dimension(it.handle, &horiz, external_sdr); + ret = parse_dimension(it.handle, &horiz, external_sdr || internal); } else if (!strcmp(key, "VerticalAttributes")) { duplicate_scope |= have_vert; have_vert = true; - ret = parse_dimension(it.handle, &vert, external_sdr); - } else if (!strcmp(key, "MinimumVariableRefreshRate")) + ret = parse_dimension(it.handle, &vert, external_sdr || internal); + } else if (!strcmp(key, "MinimumVariableRefreshRate")) { + vrr_ambiguous |= !!(vrr_seen & 1); + vrr_seen |= 1; ret = parse_int_bound(it.handle, &min_vrr, 0, U32_MAX); - else if (!strcmp(key, "MaximumVariableRefreshRate")) + } else if (!strcmp(key, "MaximumVariableRefreshRate")) { + vrr_ambiguous |= !!(vrr_seen & 2); + vrr_seen |= 2; ret = parse_int_bound(it.handle, &max_vrr, 0, U32_MAX); - else if (!strcmp(key, "ColorModes")) + } else if (!strcmp(key, "ColorModes")) ret = parse_color_modes(it.handle, out, external_sdr, &limits, handle->automatic_1080p180_rgb8 ? &unsafe_rgb8 : NULL); @@ -869,27 +875,11 @@ static int parse_mode(struct dcp_parse_ctx *handle, if (is_virtual) return -EINVAL; - /* - * An internal ProMotion panel carries no EDID or DisplayID, so DCP - * reports no adaptive-sync range for it. Assume the ProMotion floor of - * 24 Hz up to whatever rate the mode itself advertises. - */ - if (internal && vert.precise_sync_rate >> 16 == 120) { - out->min_vrr = 24 << 16; - out->max_vrr = vert.precise_sync_rate; - out->vrr = true; - } - - /* - * Refresh rates are reported by DCP as 16.16 fixed-point Hz. - * External H17P skips adaptive-sync programming: keep the fixed timing - * available, but do not advertise its sink VRR range as driver support. - */ - if (!external_sdr && min_vrr && max_vrr > min_vrr) { - out->min_vrr = min_vrr; - out->max_vrr = max_vrr; - out->vrr = true; - } + /* External H17P keeps fixed timings without adaptive-sync support. */ + if (!external_sdr && !vrr_ambiguous) + out->vrr = dcp_refresh_range(vert.precise_sync_rate, min_vrr, + max_vrr, internal, + &out->min_vrr, &out->max_vrr); clock = calculate_clock(&horiz, &vert); if (!clock || clock > INT_MAX) @@ -918,7 +908,8 @@ static int parse_mode(struct dcp_parse_ctx *handle, .height_mm = height_mm, }; - if (external_sdr && horiz.sync_polarity >= 0 && vert.sync_polarity >= 0) + if ((external_sdr || internal) && horiz.sync_polarity >= 0 && + vert.sync_polarity >= 0) mode->flags = (horiz.sync_polarity ? DRM_MODE_FLAG_PHSYNC : DRM_MODE_FLAG_NHSYNC) | (vert.sync_polarity ? DRM_MODE_FLAG_PVSYNC : DRM_MODE_FLAG_NVSYNC); out->metadata_timing_valid = external_sdr && !out->automatic_rgb8_quirk && diff --git a/drivers/gpu/drm/apple/parser_mode_test.c b/drivers/gpu/drm/apple/parser_mode_test.c index 40d0c8d9cd196..31b248c6fff87 100644 --- a/drivers/gpu/drm/apple/parser_mode_test.c +++ b/drivers/gpu/drm/apple/parser_mode_test.c @@ -7,6 +7,7 @@ #include #include "parser.h" +#include "dcp-refresh.h" struct dcp_mode_rank_case { const char *desc; @@ -40,7 +41,34 @@ static void dcp_mode_rank_test(struct kunit *test) c->above); } +static void dcp_refresh_range_test(struct kunit *test) +{ + u32 low = 0, high = 0; + + KUNIT_EXPECT_TRUE(test, dcp_refresh_range(120U << 16, 0, 0, + true, &low, &high)); + KUNIT_EXPECT_EQ(test, low, 24U << 16); + KUNIT_EXPECT_EQ(test, high, 120U << 16); + KUNIT_EXPECT_FALSE(test, dcp_refresh_range(60U << 16, 0, 0, + true, &low, &high)); + KUNIT_EXPECT_FALSE(test, dcp_refresh_range(120U << 16, 0, 0, + false, &low, &high)); + KUNIT_EXPECT_FALSE(test, dcp_refresh_range(120U << 16, 0, 120U << 16, + true, &low, &high)); + KUNIT_EXPECT_FALSE(test, dcp_refresh_range(120U << 16, 24U << 16, + 144U << 16, true, &low, &high)); + KUNIT_EXPECT_FALSE(test, dcp_refresh_range(120U << 16, 120U << 16, + 24U << 16, true, &low, &high)); + KUNIT_EXPECT_TRUE(test, dcp_refresh_range(120U << 16, 48U << 16, + 120U << 16, true, &low, &high)); + KUNIT_EXPECT_EQ(test, low, 48U << 16); + KUNIT_EXPECT_EQ(test, dcp_desktop_present_deadline(true, true), 120ULL); + KUNIT_EXPECT_EQ(test, dcp_desktop_present_deadline(false, true), 0ULL); + KUNIT_EXPECT_EQ(test, dcp_desktop_present_deadline(true, false), 0ULL); +} + static struct kunit_case dcp_mode_rank_test_cases[] = { + KUNIT_CASE(dcp_refresh_range_test), KUNIT_CASE_PARAM(dcp_mode_rank_test, dcp_mode_rank_gen_params), {} }; diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/README b/tools/testing/selftests/drivers/gpu/apple-dcp/README index c49b2d48949af..24437d55edfd8 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/README +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/README @@ -158,3 +158,14 @@ surviving an unrelated failed AGX request, both reply/timeout lock orderings, malformed replies, true owner failure, an unowned or invalid fault SID, probe rollback versus published DMA lifetime, exactly-once DRM event ownership, and mixed-owner, truncated, failed-translation and invalid-owner IOMMU tuples. + +Internal panel policy +--------------------- +The display group covers the shared nits ramp on a 600-nit panel, retargeting, +blanking, suspend, rejected submits and both submit/completion callback orders. +Synthetic TimingElements exercise 120 Hz and fractional lower refresh rates, +DCP color/timing identities, invalid geometry and adaptive-sync ranges. The +adaptive-sync callback test verifies the desktop parameter record, mode-info +ordering and one fixed-refresh retry after a rejected enable or modeset. +These tests do not establish which timings a particular panel reports or prove +refresh rate, flicker, brightness smoothness or adaptive-sync on hardware. diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/display.py b/tools/testing/selftests/drivers/gpu/apple-dcp/display.py new file mode 100644 index 0000000000000..fc76e557ee5a3 --- /dev/null +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/display.py @@ -0,0 +1,278 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +# Copyright 2026 Ryan Murray +"""Internal panel policy and real TimingElements parser, without hardware.""" +import copy +import struct +from common import block +import rate + + +def backlight(root): + header = (root / 'drivers/gpu/drm/apple/dcp_backlight.h').read_text() + header = '\n'.join(line for line in header.splitlines() + if not line.startswith('#include')) + return r''' +#include +#include +#include +#include +#include +typedef uint32_t u32; +typedef uint64_t u64; +typedef uint8_t u8; +#define min(a,b) ((a)<(b)?(a):(b)) +#define mul_u32_u32(a,b) ((u64)(a)*(b)) +#define div_u64(a,b) ((a)/(b)) +''' + header + r''' +static void completed(struct dcp_backlight_state *s, struct dcp_backlight_swap *p, + u32 id, bool early) +{ + u32 old=s->actual; + if(early) { + assert(!dcp_bl_swap_finish(s,p,id,false,true)); + assert(s->actual==old && s->in_flight); + assert(dcp_bl_swap_finish(s,p,id,true,true)); + } else { + assert(!dcp_bl_swap_finish(s,p,id,true,true)); + assert(s->actual==old && s->in_flight); + assert(dcp_bl_swap_finish(s,p,id,false,true)); + } + assert(!dcp_bl_swap_finish(s,p,id,false,true)); +} +int main(void) +{ + struct dcp_backlight_state s; + struct dcp_backlight_swap p={0}; + u32 id=1, nits; + assert(!dcp_bl_takeover_nits(600,600000,&nits) && nits==600); + /* A registration default must never change the loader's visible level. */ + assert(!dcp_bl_init(&s,600,false,0,true,250)); s.fade=true; + assert(!s.dirty && !s.level_known); + assert(dcp_bl_swap_prepare(&s,&p,id,true)==-EALREADY); + assert(!dcp_bl_request(&s,250,false,false)); + assert(!s.fading && s.dirty); + assert(dcp_bl_swap_prepare(&s,&p,id,false)==-ENODATA); + assert(!dcp_bl_swap_prepare(&s,&p,id,true)); + completed(&s,&p,id++,true); + assert(s.actual==250 && s.level_known); + assert(!dcp_bl_seed(&s,300)); + /* Same state as a slider request or an ALS retarget through backlight. */ + assert(!dcp_bl_request(&s,600,false,false) && s.fading); + for(u32 t=16;t<250;t+=16) { + u32 before=s.level; + assert(dcp_bl_fade_step(&s,t,250)); + assert(s.level>=before && s.level<600); + if(t==16) { + assert(!dcp_bl_swap_prepare(&s,&p,id,true)); + assert(dcp_bl_swap_finish(&s,&p,id++,true,false)); + assert(s.actual==250 && s.dirty && dcp_bl_retry_delay(&s)==100); + } + } + assert(!dcp_bl_fade_step(&s,250,250)); + assert(!dcp_bl_swap_prepare(&s,&p,id,true)); + assert(!dcp_bl_swap_finish(&s,&p,id+100,false,true)); + completed(&s,&p,id++,false); assert(s.actual==600); + assert(dcp_bl_request(&s,601,false,false)==-ERANGE); + assert(!dcp_bl_request(&s,0,false,false)); + assert(dcp_bl_fade_step(&s,125,250) && s.level==300); + assert(!dcp_bl_request(&s,100,false,false) && s.fade_from==300); + assert(dcp_bl_fade_step(&s,125,250) && s.level==200); + assert(!dcp_bl_request(&s,100,true,false)); + assert(!s.fading && dcp_bl_effective(&s)==0); + assert(!dcp_bl_request(&s,450,true,false) && dcp_bl_effective(&s)==0); + assert(!dcp_bl_request(&s,450,false,false) && s.level==450); + assert(!dcp_bl_request(&s,0,false,true) && dcp_bl_effective(&s)==0); + assert(!dcp_bl_request(&s,450,false,false) && s.level==450); + assert(!dcp_bl_dpms(&s,false) && dcp_bl_effective(&s)==0); + assert(!dcp_bl_dpms(&s,true) && dcp_bl_effective(&s)==450); + assert(!dcp_bl_resend(&s)); + return 0; +} +''' + + +def modes(root): + f = rate.parser_fixture(root) + # Use the exact production parser and public serializer already used by rate. + parser = f['parser'] + prefix = (f['prefix'] + f['epoch'] + f['metadata'] + f['header']).replace('static inline ', 'static inline __attribute__((unused)) ') + baseline = copy.deepcopy(f['timing_fixture'][0]) + checks = [] + blobs = [] + + def case(elements, expected, ranges=None): + raw = bytearray(struct.pack('','') + return r''' +#include +#include +#include +typedef uint32_t u32; +typedef uint64_t u64; +''' + h + r''' +int main(void) { + u32 lo=0,hi=0; + assert(dcp_refresh_range(120U<<16,0,0,true,&lo,&hi)); + assert(lo==24U<<16 && hi==120U<<16); + assert(!dcp_refresh_range(120U<<16,0,0,false,&lo,&hi)); + assert(!dcp_refresh_range(60U<<16,0,0,true,&lo,&hi)); + assert(!dcp_refresh_range(120U<<16,0,120U<<16,true,&lo,&hi)); + assert(!dcp_refresh_range(120U<<16,120U<<16,24U<<16,true,&lo,&hi)); + assert(!dcp_refresh_range(120U<<16,24U<<16,144U<<16,true,&lo,&hi)); + assert(dcp_desktop_present_deadline(true,true)==120); + assert(dcp_desktop_present_deadline(true,false)==0); + assert(dcp_desktop_present_deadline(false,true)==0); + return 0; +} +''' + + +def adaptive(root): + source=(root/'drivers/gpu/drm/apple/iomfb_template.c').read_text() + funcs='\n'.join(block(source, marker) for marker in ( + 'static void dcp_modeset_fixed_fallback(', + 'static void dcp_modeset_actual_done(', + 'static void dcp_modeset_after_actual_info(', + 'static void dcp_modeset_after_bootstrap(', + 'static void dcp_modeset_after_bootstrap_info(', + 'static void dcp_modeset_select(', + 'static void dcp_modeset_sync_done(', + 'static void dcp_modeset_sync(')) + cookie=block(source,'struct dcp_modeset_cookie {')+';\n' + main=(root/'drivers/gpu/drm/apple/iomfb.c').read_text() + gate=block(main,'bool dcp_desktop_vrr_supported(') + return r''' +#include +#include +#include +#include +typedef uint32_t u32; +typedef uint64_t u64; +#define READ_ONCE(x) (x) +#define WRITE_ONCE(x,v) ((x)=(v)) +#define DCP_FIRMWARE_V_26_6_DESKTOP 5 +#define IOMFBPARAM_ADAPTIVE_SYNC 14 +#define dev_warn(...) ((void)0) +struct dcp_wait_cookie { u32 status; bool done,replied; }; +struct dcp_set_digital_out_mode_req {u32 color_mode_id,timing_mode_id;}; +struct dcp_set_parameter_dcp { u32 param; u64 value[4]; u32 count; }; +struct apple_dcp {void *dev;int fw_compat;bool main_display,panel;struct {bool vrr_failed;} *desktop;}; +static bool t6041_vrr; +static bool dcp_has_panel(struct apple_dcp *d) {return d->panel;} +static u32 get_unaligned_le32(const void *p) {u32 v;memcpy(&v,p,4);return v;} +typedef void (*callback)(struct apple_dcp *,void *,void *); +static callback pending; +static void *cookie_pending; +static unsigned kind, parameters,infos,modes; +static struct dcp_set_parameter_dcp parameter; +static struct dcp_set_digital_out_mode_req requested; +static void dcp_modeset_select(struct apple_dcp *,void *,void *); +static void dcp_modeset_sync(struct apple_dcp *,void *); +static void dcp_set_parameter_dcp(struct apple_dcp *d,bool oob, + struct dcp_set_parameter_dcp *p,callback cb,void *c) +{assert(!pending && !oob);parameter=*p;pending=cb;cookie_pending=c;kind=1;parameters++;} +static void dcp_get_mode_info(struct apple_dcp *d,const struct dcp_set_digital_out_mode_req *m,callback cb,void *c) +{assert(!pending);requested=*m;pending=cb;cookie_pending=c;kind=2;infos++;} +static void dcp_set_digital_out_mode(struct apple_dcp *d,bool oob, + struct dcp_set_digital_out_mode_req *m,callback cb,void *c) +{assert(!pending && !oob);requested=*m;pending=cb;cookie_pending=c;kind=3;modes++;} +static void complete_set_digital_out_mode(struct apple_dcp *d,void *p,void *c) +{struct dcp_wait_cookie *w=c;w->done=true;w->replied=!!p;if(p)w->status=get_unaligned_le32(p);} +''' + cookie + gate + funcs + r''' +static void reply(struct apple_dcp *d,unsigned expected,u32 *status) +{assert(kind==expected && pending);callback cb=pending;void *c=cookie_pending;pending=NULL;cb(d,status,c);} +static void initialize(struct dcp_modeset_cookie *m) +{memset(m,0,sizeof(*m));m->vrr=true;m->min_vrr=24U<<16;m->actual=(struct dcp_set_digital_out_mode_req){11,12};m->bootstrap=(struct dcp_set_digital_out_mode_req){13,12};} +static void selected(struct apple_dcp *d,struct dcp_modeset_cookie *m) +{u32 zero=0;reply(d,2,&zero);assert(requested.color_mode_id==11);reply(d,3,&zero);assert(m->wait.done && m->wait.replied && !m->wait.status);} +int main(void) +{ + struct apple_dcp d={.fw_compat=DCP_FIRMWARE_V_26_6_DESKTOP,.main_display=true,.panel=true}; + __typeof__(*d.desktop) desktop={0};d.desktop=&desktop; + assert(!dcp_desktop_vrr_supported(&d));t6041_vrr=true; + assert(dcp_desktop_vrr_supported(&d)); + d.main_display=false;assert(!dcp_desktop_vrr_supported(&d));d.main_display=true; + d.panel=false;assert(!dcp_desktop_vrr_supported(&d));d.panel=true; + d.fw_compat=3;assert(!dcp_desktop_vrr_supported(&d));d.fw_compat=DCP_FIRMWARE_V_26_6_DESKTOP; + struct dcp_modeset_cookie m;u32 zero=0,error=1; + initialize(&m);dcp_modeset_sync(&d,&m); + assert(parameter.param==14 && parameter.count==3 && parameter.value[0]==24U<<16); + assert(!parameter.value[1] && !parameter.value[2] && parameter.value[3]==0xaaaaaaaaaaaaaaaaULL); + reply(&d,1,&zero);selected(&d,&m);assert(m.vrr && !desktop.vrr_failed); + /* Rejected enable is cleared before a fixed timing is selected. */ + initialize(&m);dcp_modeset_sync(&d,&m);reply(&d,1,&error); + assert(desktop.vrr_failed && !dcp_desktop_vrr_supported(&d) && !m.vrr && !parameter.value[0]); + reply(&d,1,&zero);selected(&d,&m); + /* A rejected modeset gets one fixed-refresh retry. */ + desktop.vrr_failed=false;initialize(&m);dcp_modeset_sync(&d,&m); + reply(&d,1,&zero);reply(&d,2,&zero);reply(&d,3,&error); + assert(!m.vrr && desktop.vrr_failed && kind==1 && !parameter.value[0]); + reply(&d,1,&zero);selected(&d,&m); + /* Failed disable and absent replies must stop, without claiming success. */ + initialize(&m);dcp_modeset_sync(&d,&m);reply(&d,1,&error);reply(&d,1,&error); + assert(m.wait.done && m.wait.status && !pending); + initialize(&m);dcp_modeset_sync(&d,&m);reply(&d,1,NULL); + assert(m.wait.done && !m.wait.replied && !pending); + /* Bootstrap keeps the DCP's actual color/timing identities. */ + desktop.vrr_failed=false;initialize(&m);m.use_bootstrap=true; + dcp_modeset_sync(&d,&m);reply(&d,1,&zero); + assert(requested.color_mode_id==13 && requested.timing_mode_id==12); + reply(&d,2,&zero);reply(&d,3,&zero);selected(&d,&m); + assert(parameters && infos && modes); + return 0; +} +''' diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py b/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py index 7bd087c364c52..61000228c8b49 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py @@ -48,6 +48,8 @@ def wire(value, out): metadata = block(metadata, 'struct dcp_display_metadata {') + ';\n' header = (base / 'parser.h').read_text() header = header[header.index('struct apple_dcp;'):header.index('int parse(')] + refresh = (base / 'dcp-refresh.h').read_text().replace('#include ', '') + header = refresh + '\n' + header # Header helpers stay header-like: unused here, as in any includer. header = header.replace('static inline ', 'static inline __attribute__((unused)) ') edid_header = (root / 'include/drm/drm_edid.h').read_text() diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/run.py b/tools/testing/selftests/drivers/gpu/apple-dcp/run.py index 54bcad84819d2..6913134d2a006 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/run.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/run.py @@ -29,6 +29,7 @@ import rate import boot import m4 +import display def parking_unit(root): @@ -40,7 +41,7 @@ def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--source', type=Path, default=Path(__file__).resolve().parents[6]) parser.add_argument('--output', type=Path, help='retain generated C, commands and results here') - parser.add_argument('--group', choices=('all', 'edid', 'rate', 'phy', 'lifecycle', 'boot', 'quiesce', 'm4'), default='all') + parser.add_argument('--group', choices=('all', 'edid', 'rate', 'phy', 'lifecycle', 'boot', 'quiesce', 'm4', 'display'), default='all') args = parser.parse_args() root = args.source.resolve() output = args.output or Path(tempfile.mkdtemp(prefix='apple-dcp-selftest-')) @@ -51,6 +52,10 @@ def main(): 'orientation-faults', 'backend-change', 'safe-first', 'failed-flip-return', 'live-park')) tests = [ + ('display', 'panel-backlight-ramp', display.backlight, ((),), ()), + ('display', 'panel-mode-catalog', display.modes, ((),), ()), + ('display', 'panel-refresh-range', display.refresh, ((),), ()), + ('display', 'panel-adaptive-fallback', display.adaptive, ((),), ()), ('m4', 'm4-wire-golden', lambda r: m4.golden_check(r, output), (), ()), ('m4', 'm4-protocol', m4.protocol, ((),), ('-Wno-unused-function', '-Wno-unused-const-variable')), ('m4', 'm4-common-plane', m4.plane, ((),), ('-Wno-unused-function', '-Wno-unused-const-variable')), From 9065d892529fa1b13a414076712799339429f358 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 00:28:41 +1100 Subject: [PATCH 088/388] drm/apple: preserve takeover brightness and validate desktop VRR requests Accept the first bounded powerlog hint before backlight registration, including a valid zero-nit hint. Otherwise report the same midpoint default as H17P while preserving the loader level. Handle firmware brightness resend requests through the shared policy. Reject unsupported desktop VRR requests before commit, allow the recovery commit to restore fixed refresh, and withdraw the capability after a failed adaptive modeset. Retry a rejected bootstrap at fixed refresh too. Extend the host tests for takeover hints and atomic VRR admission. Align the new documentation with the display maintainer patterns. Assisted-by: LLM Signed-off-by: Ryan Murray --- ...41-panel.rst => apple-dcp-t6041-panel.rst} | 2 + MAINTAINERS | 1 + drivers/gpu/drm/apple/dcp-internal.h | 3 ++ drivers/gpu/drm/apple/dcp.c | 7 ++- drivers/gpu/drm/apple/dcp_backlight.c | 31 +++++++---- drivers/gpu/drm/apple/dcp_backlight.h | 2 +- drivers/gpu/drm/apple/iomfb.c | 18 ++++++- drivers/gpu/drm/apple/iomfb_template.c | 23 +++++--- drivers/gpu/drm/apple/parser_mode_test.c | 14 ++--- drivers/gpu/drm/apple/systemep.c | 5 +- .../drivers/gpu/apple-dcp/display.py | 54 ++++++++++++++++++- .../drivers/gpu/apple-dcp/lifecycle.py | 3 ++ .../selftests/drivers/gpu/apple-dcp/run.py | 1 + 13 files changed, 134 insertions(+), 30 deletions(-) rename Documentation/gpu/{apple-t6041-panel.rst => apple-dcp-t6041-panel.rst} (97%) diff --git a/Documentation/gpu/apple-t6041-panel.rst b/Documentation/gpu/apple-dcp-t6041-panel.rst similarity index 97% rename from Documentation/gpu/apple-t6041-panel.rst rename to Documentation/gpu/apple-dcp-t6041-panel.rst index ba1b9de0f4094..c24ebd1e4bfc4 100644 --- a/Documentation/gpu/apple-t6041-panel.rst +++ b/Documentation/gpu/apple-dcp-t6041-panel.rst @@ -1,3 +1,5 @@ +.. SPDX-License-Identifier: GPL-2.0-only OR MIT + T6041 internal panel =================== diff --git a/MAINTAINERS b/MAINTAINERS index 897becc509f69..517a184d7f5ce 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -2062,6 +2062,7 @@ F: Documentation/gpu/apple-dcp-*.rst F: drivers/gpu/drm/apple/ F: drivers/gpu/drm/apple/dcp-fabric* F: tools/testing/selftests/drivers/gpu/apple-afkv2/ +F: tools/testing/selftests/drivers/gpu/apple-dcp/ APPLE PCIE CONTROLLER DRIVER M: Marc Zyngier diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index 2f6e107fa43e8..2e45252cb7b84 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -230,6 +230,8 @@ enum dcp_test_point { struct dcp_desktop_state { /* Protected by backlight.lock, independent of framebuffer retirement. */ struct dcp_backlight_swap backlight; + bool inherited_brightness_valid; + u32 inherited_brightness; /* Queued swap; too large for the shared swap union. */ struct dcp_swap_submit_req_v26_6_desktop swap; @@ -296,6 +298,7 @@ struct dcpext_scanout; struct dcp_hdmi; bool dcp_desktop_vrr_supported(struct apple_dcp *dcp); +int dcp_desktop_vrr_check(struct apple_dcp *dcp, struct drm_crtc_state *state); struct apple_dcp { /* Generation-specific wire/transaction state, sharing the common KMS owner. */ diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 03815b0f86e6b..f22d678b5a8d1 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -921,8 +921,8 @@ VISIBLE_IF_KUNIT void dcp_swap_watchdog(struct work_struct *work) READ_ONCE(dcp->vrr_enabled)) { WRITE_ONCE(dcp->desktop->vrr_failed, true); WRITE_ONCE(dcp->vrr_enabled, false); - dcp_backlight_desktop_finish(dcp, - READ_ONCE(dcp->desktop->present_id), true, false); + dcp_backlight_desktop_finish(dcp, READ_ONCE(dcp->desktop->present_id), + true, false); dev_warn(dcp->dev, "adaptive swap timed out, restoring fixed refresh\n"); } dev_warn(dcp->dev, "swap not completed, retraining the display\n"); @@ -1218,6 +1218,9 @@ int dcp_crtc_atomic_check(struct drm_crtc *crtc, struct drm_atomic_commit *commi crtc_state->vrr_enabled) return -EINVAL; + if (dcp_desktop_vrr_check(dcp, crtc_state)) + return -EINVAL; + /* DRM core only caps the blob size; the gamma update copies a full table. */ if (crtc_state->color_mgmt_changed && crtc_state->gamma_lut && drm_color_lut_size(crtc_state->gamma_lut) != dcp_gamma_lut_size(pdev)) diff --git a/drivers/gpu/drm/apple/dcp_backlight.c b/drivers/gpu/drm/apple/dcp_backlight.c index d32f52f11890b..475be119789f1 100644 --- a/drivers/gpu/drm/apple/dcp_backlight.c +++ b/drivers/gpu/drm/apple/dcp_backlight.c @@ -22,9 +22,6 @@ #define MIN_BRIGHTNESS_PART2 103U #define MAX_BRIGHTNESS_PART2 510U -/* Initial level of a desktop 26.x panel, which reports none. */ -#define DCP_DESKTOP_DEFAULT_NITS 250U - /* * lookup for display brightness 2 to 99 nits * */ @@ -129,6 +126,20 @@ int dcp_backlight_takeover(struct apple_dcp *dcp, u32 millinits) if (ret) return ret; + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) { + unsigned long flags; + + /* Freeze the first hint before publishing the class device. */ + spin_lock_irqsave(&dcp->backlight.lock, flags); + if (!dcp->backlight.state.ready && + !dcp->desktop->inherited_brightness_valid) { + dcp->desktop->inherited_brightness = nits; + dcp->desktop->inherited_brightness_valid = true; + } + spin_unlock_irqrestore(&dcp->backlight.lock, flags); + return 0; + } + /* Keep the first bounded hint. Later reports are not user requests. */ return iomfb_configure_backlight_h17p(dcp, maximum, true, nits, false, 0); } @@ -391,7 +402,7 @@ void dcp_backlight_desktop_power(struct apple_dcp *dcp, bool on) /* Reserve only after swap_start succeeds, using its actual firmware ID. */ bool dcp_backlight_desktop_prepare(struct apple_dcp *dcp, u32 id, - bool have_surface, u32 *nits) + bool have_surface, u32 *nits) { unsigned long flags; int ret; @@ -418,7 +429,7 @@ void dcp_backlight_desktop_finish(struct apple_dcp *dcp, u32 id, return; spin_lock_irqsave(&dcp->backlight.lock, flags); finished = dcp_bl_swap_finish(&dcp->backlight.state, - &dcp->desktop->backlight, id, reply, accepted); + &dcp->desktop->backlight, id, reply, accepted); delay = dcp_bl_retry_delay(&dcp->backlight.state); pending = dcp->backlight.state.dirty; spin_unlock_irqrestore(&dcp->backlight.lock, flags); @@ -662,13 +673,13 @@ int dcp_backlight_register(struct apple_dcp *dcp) if (!dcp->brightness.maximum || dcp->brightness.maximum > INT_MAX) return -ERANGE; props.max_brightness = dcp->brightness.maximum; - if (!props.brightness) - props.brightness = min_t(u32, DCP_DESKTOP_DEFAULT_NITS, - props.max_brightness); + /* Match the Neo's reporting default without presenting a guess. */ + props.brightness = props.max_brightness / 2; spin_lock_irqsave(&dcp->backlight.lock, flags); if (!dcp->backlight.state.ready) { - int ret = dcp_bl_init(&dcp->backlight.state, - props.max_brightness, false, 0, + int ret = dcp_bl_init(&dcp->backlight.state, props.max_brightness, + dcp->desktop->inherited_brightness_valid, + dcp->desktop->inherited_brightness, true, props.brightness); if (ret) { diff --git a/drivers/gpu/drm/apple/dcp_backlight.h b/drivers/gpu/drm/apple/dcp_backlight.h index a1ac47f166678..6585e58436e2a 100644 --- a/drivers/gpu/drm/apple/dcp_backlight.h +++ b/drivers/gpu/drm/apple/dcp_backlight.h @@ -319,7 +319,7 @@ struct apple_dcp; void dcp_backlight_desktop_power(struct apple_dcp *dcp, bool on); bool dcp_backlight_desktop_prepare(struct apple_dcp *dcp, u32 id, - bool have_surface, u32 *nits); + bool have_surface, u32 *nits); void dcp_backlight_desktop_finish(struct apple_dcp *dcp, u32 id, bool reply, bool accepted); diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index 2c93f24d5a006..6d5e91fa3fe35 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -102,6 +102,21 @@ bool dcp_desktop_vrr_supported(struct apple_dcp *dcp) !READ_ONCE(dcp->desktop->vrr_failed); } +int dcp_desktop_vrr_check(struct apple_dcp *dcp, struct drm_crtc_state *state) +{ + struct dcp_display_mode mode; + + if (dcp->fw_compat != DCP_FIRMWARE_V_26_6_DESKTOP || !state->vrr_enabled) + return 0; + /* Let the recovery commit re-apply an existing VRR state at fixed refresh. */ + if (READ_ONCE(dcp->desktop->vrr_failed)) + return 0; + if (!dcp_desktop_vrr_supported(dcp) || + !lookup_mode(dcp, &state->mode, &mode) || !mode.vrr) + return -EINVAL; + return 0; +} + static bool dcp_mode_vrr_supported(struct apple_dcp *dcp, const struct dcp_display_mode *mode) { @@ -2006,7 +2021,8 @@ static int dcp_desktop_modeset(struct apple_dcp *dcp, if (d->trial_modeset_ok) { d->trial_modeset_ok = false; - if (!crtc_state->vrr_enabled && d->mode_live && lookup_mode(dcp, &crtc_state->mode, &matched) && + if (!crtc_state->vrr_enabled && d->mode_live && + lookup_mode(dcp, &crtc_state->mode, &matched) && matched.timing_mode_id == d->trial_timing_mode_id && matched.color_mode_id == d->trial_color_mode_id) { dev_dbg(dcp->dev, "keeping the takeover mode " DRM_MODE_FMT "\n", diff --git a/drivers/gpu/drm/apple/iomfb_template.c b/drivers/gpu/drm/apple/iomfb_template.c index 3d9465ee5561e..2123666a21cab 100644 --- a/drivers/gpu/drm/apple/iomfb_template.c +++ b/drivers/gpu/drm/apple/iomfb_template.c @@ -1140,7 +1140,11 @@ dcpep_cb_read_edt_data(struct apple_dcp *dcp, struct dcp_read_edt_data_req *req) static void iomfbep_cb_enable_backlight_message_ap_gated(struct apple_dcp *dcp, u8 *enabled) { -#if DCP_FW_H17(DCP_FW_VER) +#if DCP_FW_DESKTOP_26(DCP_FW_VER) + if (dcp_has_panel(dcp) && dcp_backlight_resend(dcp)) + schedule_work(&dcp->bl_update_wq); + return; +#elif DCP_FW_H17(DCP_FW_VER) /* H17P carries the level only in presents; ask for one more. */ if (dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17G) { if (dcp_has_panel(dcp) && dcp_backlight_active(dcp) && @@ -2634,7 +2638,7 @@ static void dcp_swap_started(struct apple_dcp *dcp, void *data, void *cookie) for (unsigned int i = 0; i < SWAP_SURFACES; i++) have_surface |= !DCP_FW_SWAP(dcp).surf_null[i]; if (dcp_backlight_desktop_prepare(dcp, resp->swap_id, - have_surface, &nits)) + have_surface, &nits)) dcp_swap_set_brightness(&DCP_FW_SWAP(dcp).swap, nits); } #endif @@ -2917,7 +2921,7 @@ static void dcp_modeset_after_bootstrap(struct apple_dcp *dcp, void *data, struct dcp_modeset_cookie *modeset = cookie; if (!data || get_unaligned_le32(data)) { - complete_set_digital_out_mode(dcp, data, cookie); + dcp_modeset_actual_done(dcp, data, cookie); return; } dcp_get_mode_info(dcp, &modeset->actual, dcp_modeset_after_actual_info, @@ -3106,6 +3110,8 @@ int DCP_FW_NAME(iomfb_modeset)(struct apple_dcp *dcp, kref_put(&cookie->refcount, release_wait_cookie); dcp->desktop->mode_live = !ret; + if (READ_ONCE(dcp->desktop->vrr_failed) && dcp->connector) + dcp_queue_hotplug(dcp->connector); return ret; } #else @@ -3364,9 +3370,14 @@ void DCP_FW_NAME(iomfb_flush)(struct apple_dcp *dcp, struct drm_crtc *crtc, stru } #if DCP_FW_DESKTOP_26(DCP_FW_VER) - /* Types 1, 2 and 7 carry the same desktop presentation deadline. */ - req->swap.timestamp[0] = req->swap.timestamp[1] = req->swap.timestamp[6] = - dcp_desktop_present_deadline(has_surface, dcp->use_timestamps); + { + u64 deadline = dcp_desktop_present_deadline(has_surface, dcp->use_timestamps); + + /* Types 1, 2 and 7 carry the same desktop presentation deadline. */ + req->swap.timestamp[0] = deadline; + req->swap.timestamp[1] = deadline; + req->swap.timestamp[6] = deadline; + } #else if (has_surface && (dcp->use_timestamps || dcp->vrr_enabled)) { u64 submit_timestamp = dcp->swap_submit_timestamp; diff --git a/drivers/gpu/drm/apple/parser_mode_test.c b/drivers/gpu/drm/apple/parser_mode_test.c index 31b248c6fff87..877d012740df1 100644 --- a/drivers/gpu/drm/apple/parser_mode_test.c +++ b/drivers/gpu/drm/apple/parser_mode_test.c @@ -46,21 +46,21 @@ static void dcp_refresh_range_test(struct kunit *test) u32 low = 0, high = 0; KUNIT_EXPECT_TRUE(test, dcp_refresh_range(120U << 16, 0, 0, - true, &low, &high)); + true, &low, &high)); KUNIT_EXPECT_EQ(test, low, 24U << 16); KUNIT_EXPECT_EQ(test, high, 120U << 16); KUNIT_EXPECT_FALSE(test, dcp_refresh_range(60U << 16, 0, 0, - true, &low, &high)); + true, &low, &high)); KUNIT_EXPECT_FALSE(test, dcp_refresh_range(120U << 16, 0, 0, - false, &low, &high)); + false, &low, &high)); KUNIT_EXPECT_FALSE(test, dcp_refresh_range(120U << 16, 0, 120U << 16, - true, &low, &high)); + true, &low, &high)); KUNIT_EXPECT_FALSE(test, dcp_refresh_range(120U << 16, 24U << 16, - 144U << 16, true, &low, &high)); + 144U << 16, true, &low, &high)); KUNIT_EXPECT_FALSE(test, dcp_refresh_range(120U << 16, 120U << 16, - 24U << 16, true, &low, &high)); + 24U << 16, true, &low, &high)); KUNIT_EXPECT_TRUE(test, dcp_refresh_range(120U << 16, 48U << 16, - 120U << 16, true, &low, &high)); + 120U << 16, true, &low, &high)); KUNIT_EXPECT_EQ(test, low, 48U << 16); KUNIT_EXPECT_EQ(test, dcp_desktop_present_deadline(true, true), 120ULL); KUNIT_EXPECT_EQ(test, dcp_desktop_present_deadline(false, true), 0ULL); diff --git a/drivers/gpu/drm/apple/systemep.c b/drivers/gpu/drm/apple/systemep.c index a32f0a0bcf6a0..b8576aec5c6f5 100644 --- a/drivers/gpu/drm/apple/systemep.c +++ b/drivers/gpu/drm/apple/systemep.c @@ -100,8 +100,9 @@ static int powerlog_report(struct apple_epic_service *service, enum epic_subtype dev_dbg(dcp->dev, "systemep: mNits event: Nits: %u.%03u, iDAC: %u\n", mnits.millinits / 1000, mnits.millinits % 1000, mnits.idac); - if (dcp->fw_compat == DCP_FIRMWARE_H17P && - dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17G) { + if ((dcp->fw_compat == DCP_FIRMWARE_H17P && + dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17G) || + dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) { if (!dcp_has_panel(dcp)) return 0; ret = dcp_backlight_takeover(dcp, mnits.millinits); diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/display.py b/tools/testing/selftests/drivers/gpu/apple-dcp/display.py index fc76e557ee5a3..8ead9446fbe9b 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/display.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/display.py @@ -194,12 +194,13 @@ def adaptive(root): 'static void dcp_modeset_sync(')) cookie=block(source,'struct dcp_modeset_cookie {')+';\n' main=(root/'drivers/gpu/drm/apple/iomfb.c').read_text() - gate=block(main,'bool dcp_desktop_vrr_supported(') + gate=block(main,'bool dcp_desktop_vrr_supported(') + block(main,'int dcp_desktop_vrr_check(') return r''' #include #include #include #include +#include typedef uint32_t u32; typedef uint64_t u64; #define READ_ONCE(x) (x) @@ -212,6 +213,11 @@ def adaptive(root): struct dcp_set_parameter_dcp { u32 param; u64 value[4]; u32 count; }; struct apple_dcp {void *dev;int fw_compat;bool main_display,panel;struct {bool vrr_failed;} *desktop;}; static bool t6041_vrr; +struct drm_display_mode {int id;}; +struct drm_crtc_state {bool vrr_enabled;struct drm_display_mode mode;}; +struct dcp_display_mode {bool vrr;}; +static bool lookup_mode(struct apple_dcp *d,struct drm_display_mode *m,struct dcp_display_mode *out) +{out->vrr=m->id==120;return m->id==60 || m->id==120;} static bool dcp_has_panel(struct apple_dcp *d) {return d->panel;} static u32 get_unaligned_le32(const void *p) {u32 v;memcpy(&v,p,4);return v;} typedef void (*callback)(struct apple_dcp *,void *,void *); @@ -243,7 +249,13 @@ def adaptive(root): { struct apple_dcp d={.fw_compat=DCP_FIRMWARE_V_26_6_DESKTOP,.main_display=true,.panel=true}; __typeof__(*d.desktop) desktop={0};d.desktop=&desktop; + struct drm_crtc_state state={.vrr_enabled=true,.mode={120}}; + assert(dcp_desktop_vrr_check(&d,&state)==-EINVAL); assert(!dcp_desktop_vrr_supported(&d));t6041_vrr=true; + assert(!dcp_desktop_vrr_check(&d,&state)); + state.mode.id=60;assert(dcp_desktop_vrr_check(&d,&state)==-EINVAL); + state.vrr_enabled=false;assert(!dcp_desktop_vrr_check(&d,&state)); + state.vrr_enabled=true;state.mode.id=120; assert(dcp_desktop_vrr_supported(&d)); d.main_display=false;assert(!dcp_desktop_vrr_supported(&d));d.main_display=true; d.panel=false;assert(!dcp_desktop_vrr_supported(&d));d.panel=true; @@ -256,6 +268,7 @@ def adaptive(root): /* Rejected enable is cleared before a fixed timing is selected. */ initialize(&m);dcp_modeset_sync(&d,&m);reply(&d,1,&error); assert(desktop.vrr_failed && !dcp_desktop_vrr_supported(&d) && !m.vrr && !parameter.value[0]); + assert(!dcp_desktop_vrr_check(&d,&state)); /* Recovery re-applies fixed refresh. */ reply(&d,1,&zero);selected(&d,&m); /* A rejected modeset gets one fixed-refresh retry. */ desktop.vrr_failed=false;initialize(&m);dcp_modeset_sync(&d,&m); @@ -276,3 +289,42 @@ def adaptive(root): return 0; } ''' + + +def takeover(root): + prefix=backlight(root).split('static void completed(')[0] + prefix=prefix.replace('static inline ', 'static inline __attribute__((unused)) ') + function=block((root/'drivers/gpu/drm/apple/dcp_backlight.c').read_text(), + 'int dcp_backlight_takeover(') + return prefix + r''' +#define DCP_FIRMWARE_V_26_6_DESKTOP 5 +#define READ_ONCE(x) (x) +#define spin_lock_irqsave(p,f) do {assert(!*(p));*(p)=true;(f)=0;} while(0) +#define spin_unlock_irqrestore(p,f) do {assert(*(p));*(p)=false;(void)(f);} while(0) +struct apple_dcp { + int fw_compat; + struct {u32 maximum;} brightness; + struct {bool lock;struct dcp_backlight_state state;} backlight; + struct {bool inherited_brightness_valid;u32 inherited_brightness;} *desktop; +}; +static unsigned legacy; +bool dcp_backlight_active(struct apple_dcp *d) {return d->fw_compat==1 && d->backlight.state.ready;} +static int iomfb_configure_backlight_h17p(struct apple_dcp *d,u32 max,bool valid,u32 nits,bool fallback,u32 guessed) +{assert(max==600 && valid && nits==100 && !fallback && !guessed);legacy++;return 0;} +''' + function + r''' +int main(void) { + struct apple_dcp d={.fw_compat=5,.brightness={600}}; + __typeof__(*d.desktop) saved={0};d.desktop=&saved; + assert(!dcp_backlight_takeover(&d,0)); + assert(saved.inherited_brightness_valid && !saved.inherited_brightness && !legacy); + assert(!dcp_backlight_takeover(&d,500000) && !saved.inherited_brightness); + saved.inherited_brightness_valid=false; + assert(dcp_backlight_takeover(&d,1300000)==-ERANGE && !saved.inherited_brightness_valid); + assert(!dcp_backlight_takeover(&d,700000) && saved.inherited_brightness==600); + d.backlight.state.ready=true; saved.inherited_brightness_valid=false; + assert(!dcp_backlight_takeover(&d,100000) && !saved.inherited_brightness_valid); + d.fw_compat=1;d.backlight.state.ready=false; + assert(!dcp_backlight_takeover(&d,100000) && legacy==1); + return 0; +} +''' diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py index d7485a6681747..582156af8ca4d 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py @@ -1709,6 +1709,9 @@ def vrr(root): { (void)blob; assert(!"gamma LUT without a colour-management change"); return 0; } static unsigned int __attribute__((unused)) dcp_gamma_lut_size(struct platform_device *p) { (void)p; assert(!"gamma LUT without a colour-management change"); return 0; } +/* Desktop admission is covered by panel-adaptive-fallback. */ +static int dcp_desktop_vrr_check(struct apple_dcp *d, struct drm_crtc_state *s) +{ (void)d; (void)s; return 0; } /* Route-follow is for Type-C displays; this DCP has no Type-C routes. */ static bool __attribute__((unused)) typec_follow_crtc = true; static int __attribute__((unused)) dcp_typec_follow_check(struct apple_dcp *d, struct drm_crtc *c, diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/run.py b/tools/testing/selftests/drivers/gpu/apple-dcp/run.py index 6913134d2a006..265dfb9450a80 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/run.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/run.py @@ -53,6 +53,7 @@ def main(): 'failed-flip-return', 'live-park')) tests = [ ('display', 'panel-backlight-ramp', display.backlight, ((),), ()), + ('display', 'panel-brightness-takeover', display.takeover, ((),), ()), ('display', 'panel-mode-catalog', display.modes, ((),), ()), ('display', 'panel-refresh-range', display.refresh, ((),), ()), ('display', 'panel-adaptive-fallback', display.adaptive, ((),), ()), From 9df36a2c42b6f39a015ead5607b39ed2e157631c Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 00:29:56 +1100 Subject: [PATCH 089/388] drm/apple: keep ProMotion deadlines independent of VRR admission A malformed adaptive-sync range must leave the fixed 120 Hz timing usable. Keep its presentation-deadline requirement separate from VRR capability, and distinguish omitted range fields from explicit zeros. Notify capability withdrawal once per adaptive-sync failure so the fixed-refresh recovery does not generate repeated hotplug notifications. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/dcp-refresh.h | 11 ++++++++--- drivers/gpu/drm/apple/iomfb_template.c | 8 +++++--- drivers/gpu/drm/apple/parser.c | 4 +++- drivers/gpu/drm/apple/parser.h | 2 ++ .../selftests/drivers/gpu/apple-dcp/display.py | 5 ++++- 5 files changed, 22 insertions(+), 8 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp-refresh.h b/drivers/gpu/drm/apple/dcp-refresh.h index eea5058cc3645..1b26758b6ff7b 100644 --- a/drivers/gpu/drm/apple/dcp-refresh.h +++ b/drivers/gpu/drm/apple/dcp-refresh.h @@ -5,13 +5,18 @@ #include +/* Accept nominal 120 Hz and its 1000/1001 fractional timing. */ +static inline bool dcp_refresh_is_promotion(u32 rate) +{ + return rate >= (120U << 16) - (120U << 16) / 500 && + rate <= (120U << 16); +} + /* All rates are unsigned 16.16 Hz, including fractional fixed timings. */ static inline bool dcp_refresh_range(u32 rate, u32 minimum, u32 maximum, bool internal, u32 *low, u32 *high) { - if (!minimum && !maximum && internal && - rate >= (120U << 16) - (120U << 16) / 500 && - rate <= (120U << 16)) { + if (!minimum && !maximum && internal && dcp_refresh_is_promotion(rate)) { minimum = 24U << 16; maximum = rate; } diff --git a/drivers/gpu/drm/apple/iomfb_template.c b/drivers/gpu/drm/apple/iomfb_template.c index 2123666a21cab..2707fa6d80e7a 100644 --- a/drivers/gpu/drm/apple/iomfb_template.c +++ b/drivers/gpu/drm/apple/iomfb_template.c @@ -2851,6 +2851,7 @@ struct dcp_modeset_cookie { bool use_bootstrap; bool vrr; bool program_sync; + bool notify_vrr_failure; u32 min_vrr; }; @@ -2887,6 +2888,7 @@ static void dcp_modeset_fixed_fallback(struct apple_dcp *dcp, dev_warn(dcp->dev, "adaptive sync failed, restoring fixed refresh\n"); WRITE_ONCE(dcp->desktop->vrr_failed, true); modeset->vrr = false; + modeset->notify_vrr_failure = true; dcp_modeset_sync(dcp, modeset); } @@ -3026,7 +3028,7 @@ int DCP_FW_NAME(iomfb_modeset)(struct apple_dcp *dcp, }; /* ProMotion needs desktop timestamps in fixed-120 mode too. */ - dcp->use_timestamps = mode->vrr; + dcp->use_timestamps = mode->needs_timestamps; dcp->desktop->vrr_requested = crtc_state->vrr_enabled; /* @@ -3107,11 +3109,11 @@ int DCP_FW_NAME(iomfb_modeset)(struct apple_dcp *dcp, ret = 0; } dcp->vrr_enabled = !ret && modeset->vrr; + if (modeset->notify_vrr_failure && dcp->connector) + dcp_queue_hotplug(dcp->connector); kref_put(&cookie->refcount, release_wait_cookie); dcp->desktop->mode_live = !ret; - if (READ_ONCE(dcp->desktop->vrr_failed) && dcp->connector) - dcp_queue_hotplug(dcp->connector); return ret; } #else diff --git a/drivers/gpu/drm/apple/parser.c b/drivers/gpu/drm/apple/parser.c index 612232bd394df..ab84a8522ed73 100644 --- a/drivers/gpu/drm/apple/parser.c +++ b/drivers/gpu/drm/apple/parser.c @@ -878,8 +878,10 @@ static int parse_mode(struct dcp_parse_ctx *handle, /* External H17P keeps fixed timings without adaptive-sync support. */ if (!external_sdr && !vrr_ambiguous) out->vrr = dcp_refresh_range(vert.precise_sync_rate, min_vrr, - max_vrr, internal, + max_vrr, internal && !vrr_seen, &out->min_vrr, &out->max_vrr); + out->needs_timestamps = out->vrr || + (internal && dcp_refresh_is_promotion(vert.precise_sync_rate)); clock = calculate_clock(&horiz, &vert); if (!clock || clock > INT_MAX) diff --git a/drivers/gpu/drm/apple/parser.h b/drivers/gpu/drm/apple/parser.h index 97a75a8b046a0..ef9420c1109f9 100644 --- a/drivers/gpu/drm/apple/parser.h +++ b/drivers/gpu/drm/apple/parser.h @@ -120,6 +120,8 @@ struct dcp_display_mode { struct dcp_color_mode sdr; struct dcp_color_mode best; bool vrr; + /* ProMotion fixed timings still need presentation deadlines. */ + bool needs_timestamps; /* IsPreferred, outside the external SDR path */ bool preferred; bool metadata_timing_valid; diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/display.py b/tools/testing/selftests/drivers/gpu/apple-dcp/display.py index 8ead9446fbe9b..e047a65723d68 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/display.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/display.py @@ -115,6 +115,9 @@ def case(elements, expected, ranges=None): f'assert(m[{j}].timing_mode_id=={elements[j]["ID"]}); ' f'assert(m[{j}].mode.htotal==2200 && m[{j}].mode.vtotal==1125); ' f'assert(m[{j}].mode.flags==(DRM_MODE_FLAG_PHSYNC|DRM_MODE_FLAG_PVSYNC)); ') + for j, hz in enumerate(expected): + if hz >= 119.88: + check += f'assert(m[{j}].needs_timestamps); ' if ranges: for j, (lo, hi) in enumerate(ranges): check += f'assert(m[{j}].vrr=={int(bool(lo))}); ' @@ -135,7 +138,7 @@ def case(elements, expected, ranges=None): m=copy.deepcopy(supported[0]); m['VerticalAttributes']['PreciseSyncRate']=round(hz*65536) case([m], [hz], [(24<<16, round(hz*65536))]) for lo, hi, valid in ((48<<16,120<<16,True), (0,120<<16,False), - (60<<16,0,False), (120<<16,24<<16,False), + (60<<16,0,False), (0,0,False), (120<<16,24<<16,False), (24<<16,144<<16,False), (120<<16,120<<16,False)): m=copy.deepcopy(supported[0]); m['MinimumVariableRefreshRate']=lo; m['MaximumVariableRefreshRate']=hi case([m],[120],[(lo,hi) if valid else (0,0)]) From ff522318232732dc8c63c08e36736284d298c916 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 00:34:42 +1100 Subject: [PATCH 090/388] drm/apple: isolate late desktop completions during VRR recovery Let the receive callback and watchdog claim each desktop swap once. Track clear swaps too, ignore late or duplicate completions, and retain buffers displaced by a dropped swap until a later swap completes. Test watchdog recovery and stale completion isolation in the host runner and exercise desktop completion claims in KUnit. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/dcp-internal.h | 1 + drivers/gpu/drm/apple/dcp.c | 22 +++-- drivers/gpu/drm/apple/dcp_dropped_swap_test.c | 22 +++++ drivers/gpu/drm/apple/iomfb_template.c | 10 ++- .../selftests/drivers/gpu/apple-dcp/README | 4 +- .../drivers/gpu/apple-dcp/display.py | 84 +++++++++++++++++++ .../selftests/drivers/gpu/apple-dcp/run.py | 1 + 7 files changed, 133 insertions(+), 11 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index 2e45252cb7b84..f3c8ec3cc2fa5 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -297,6 +297,7 @@ struct dcp_desktop_state { struct dcpext_scanout; struct dcp_hdmi; +bool dcp_desktop_present_complete(struct apple_dcp *dcp, u32 id); bool dcp_desktop_vrr_supported(struct apple_dcp *dcp); int dcp_desktop_vrr_check(struct apple_dcp *dcp, struct drm_crtc_state *state); diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index f22d678b5a8d1..67f8aba5a5acd 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -891,6 +891,13 @@ static struct apple_dcp_afkep *dcp_afkep(struct apple_dcp *dcp, u8 endpoint) } } +/* RX and the watchdog may race to finish one present, never both. */ +bool dcp_desktop_present_complete(struct apple_dcp *dcp, u32 id) +{ + return id == READ_ONCE(dcp->desktop->present_id) && + xchg(&dcp->desktop->present_pending, false); +} + #define DCP_SWAP_WATCHDOG_MS 1000 #define DCP_SWAP_WATCHDOG_RETRAINS 5 @@ -909,10 +916,15 @@ VISIBLE_IF_KUNIT void dcp_swap_watchdog(struct work_struct *work) container_of(to_delayed_work(work), struct apple_dcp, swap_watchdog_wq); - if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && - (!READ_ONCE(dcp->desktop->present_pending) || - time_before(jiffies, dcp->swap_watchdog_deadline))) - return; + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) { + if (time_before(jiffies, dcp->swap_watchdog_deadline) || + !xchg(&dcp->desktop->present_pending, false)) + return; + /* Retain displaced buffers until a later present really completes. */ + dcp_unarm_retained_framebuffers(dcp, READ_ONCE(dcp->desktop->present_id)); + dcp_backlight_desktop_finish(dcp, READ_ONCE(dcp->desktop->present_id), + true, false); + } if (!iomfb_retire_dropped_present_h17p(dcp)) return; @@ -921,8 +933,6 @@ VISIBLE_IF_KUNIT void dcp_swap_watchdog(struct work_struct *work) READ_ONCE(dcp->vrr_enabled)) { WRITE_ONCE(dcp->desktop->vrr_failed, true); WRITE_ONCE(dcp->vrr_enabled, false); - dcp_backlight_desktop_finish(dcp, READ_ONCE(dcp->desktop->present_id), - true, false); dev_warn(dcp->dev, "adaptive swap timed out, restoring fixed refresh\n"); } dev_warn(dcp->dev, "swap not completed, retraining the display\n"); diff --git a/drivers/gpu/drm/apple/dcp_dropped_swap_test.c b/drivers/gpu/drm/apple/dcp_dropped_swap_test.c index b45010d38fac1..cd11dbc374aa8 100644 --- a/drivers/gpu/drm/apple/dcp_dropped_swap_test.c +++ b/drivers/gpu/drm/apple/dcp_dropped_swap_test.c @@ -497,7 +497,29 @@ static void retired_completion_in_flight_test(struct kunit *test) KUNIT_EXPECT_TRUE(test, READ_ONCE(dcp->crashed)); } +static void desktop_present_completion_test(struct kunit *test) +{ + struct apple_dcp *dcp = kunit_kzalloc(test, sizeof(*dcp), GFP_KERNEL); + + KUNIT_ASSERT_NOT_NULL(test, dcp); + dcp->desktop = kunit_kzalloc(test, sizeof(*dcp->desktop), GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, dcp->desktop); + dcp->desktop->present_id = 42; + dcp->desktop->present_pending = true; + KUNIT_EXPECT_FALSE(test, dcp_desktop_present_complete(dcp, 41)); + KUNIT_EXPECT_TRUE(test, dcp->desktop->present_pending); + KUNIT_EXPECT_TRUE(test, dcp_desktop_present_complete(dcp, 42)); + KUNIT_EXPECT_FALSE(test, dcp_desktop_present_complete(dcp, 42)); + /* A late completion cannot claim the replacement's event. */ + dcp->desktop->present_id = 43; + dcp->desktop->present_pending = true; + KUNIT_EXPECT_FALSE(test, dcp_desktop_present_complete(dcp, 42)); + KUNIT_EXPECT_TRUE(test, dcp->desktop->present_pending); + KUNIT_EXPECT_TRUE(test, dcp_desktop_present_complete(dcp, 43)); +} + static struct kunit_case dropped_swap_cases[] = { + KUNIT_CASE(desktop_present_completion_test), KUNIT_CASE(dropped_present_retired_test), KUNIT_CASE(dropped_present_buffers_test), KUNIT_CASE(completed_present_kept_test), diff --git a/drivers/gpu/drm/apple/iomfb_template.c b/drivers/gpu/drm/apple/iomfb_template.c index 2707fa6d80e7a..234debba0c7dd 100644 --- a/drivers/gpu/drm/apple/iomfb_template.c +++ b/drivers/gpu/drm/apple/iomfb_template.c @@ -461,8 +461,9 @@ static void dcpep_cb_swap_complete(struct apple_dcp *dcp, retire = iomfb_present_complete_h17p(dcp); #endif #if DCP_FW_DESKTOP_26(DCP_FW_VER) - if (resp->swap_id == READ_ONCE(dcp->desktop->present_id)) - WRITE_ONCE(dcp->desktop->present_pending, false); + /* Late or duplicate completions cannot retire the replacement's flip. */ + if (!dcp_desktop_present_complete(dcp, resp->swap_id)) + return; dcp_backlight_desktop_finish(dcp, resp->swap_id, false, true); #endif trace_iomfb_swap_complete(dcp, resp->swap_id); @@ -1744,6 +1745,9 @@ static bool dcp_present_begin(struct apple_dcp *dcp, u32 swap_id) dcp->crashed = true; return false; } +#elif DCP_FW_DESKTOP_26(DCP_FW_VER) + WRITE_ONCE(dcp->desktop->present_id, swap_id); + WRITE_ONCE(dcp->desktop->present_pending, true); #endif return true; } @@ -2629,8 +2633,6 @@ static void dcp_swap_started(struct apple_dcp *dcp, void *data, void *cookie) dcp_arm_retained_framebuffers(dcp, resp->swap_id); #if DCP_FW_DESKTOP_26(DCP_FW_VER) - WRITE_ONCE(dcp->desktop->present_id, resp->swap_id); - WRITE_ONCE(dcp->desktop->present_pending, true); if (dcp_has_panel(dcp)) { u32 nits; bool have_surface = false; diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/README b/tools/testing/selftests/drivers/gpu/apple-dcp/README index 24437d55edfd8..f7de1b1179c1d 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/README +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/README @@ -166,6 +166,8 @@ blanking, suspend, rejected submits and both submit/completion callback orders. Synthetic TimingElements exercise 120 Hz and fractional lower refresh rates, DCP color/timing identities, invalid geometry and adaptive-sync ranges. The adaptive-sync callback test verifies the desktop parameter record, mode-info -ordering and one fixed-refresh retry after a rejected enable or modeset. +ordering and one fixed-refresh retry after a rejected enable or modeset. The +watchdog test checks fixed-refresh recovery, retained-buffer arming and late +completion isolation. These tests do not establish which timings a particular panel reports or prove refresh rate, flicker, brightness smoothness or adaptive-sync on hardware. diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/display.py b/tools/testing/selftests/drivers/gpu/apple-dcp/display.py index e047a65723d68..8ba1f95206837 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/display.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/display.py @@ -331,3 +331,87 @@ def takeover(root): return 0; } ''' + + +def watchdog(root): + source=(root/'drivers/gpu/drm/apple/dcp.c').read_text() + production='\n'.join(block(source, marker) for marker in ( + 'bool dcp_desktop_present_complete(', + 'VISIBLE_IF_KUNIT void dcp_swap_watchdog(', + 'void dcp_swap_watchdog_arm(', + 'void dcp_swap_watchdog_complete(')) + definitions='\n'.join(line for line in source.splitlines() + if line.startswith('#define DCP_SWAP_WATCHDOG_')) + return r''' +#include +#include +#include +#include +typedef uint32_t u32; +#define READ_ONCE(x) (x) +#define WRITE_ONCE(x,v) ((x)=(v)) +#define xchg(p,v) __atomic_exchange_n((p),(v),__ATOMIC_SEQ_CST) +#define VISIBLE_IF_KUNIT +#define DCP_FIRMWARE_V_26_6_DESKTOP 5 +#define dev_warn(...) ((void)0) +#define container_of(p,t,m) ((t *)((char *)(p)-offsetof(t,m))) +#define to_delayed_work(p) container_of(p,struct delayed_work,work) +#define msecs_to_jiffies(n) (n) +#define time_before(a,b) ((long)((a)-(b))<0) +#define system_dfl_wq NULL +static unsigned long jiffies; +struct work_struct {unsigned unused;}; +struct delayed_work {struct work_struct work;}; +struct apple_dcp { + int fw_compat;bool vrr_enabled,typec; + struct {u32 present_id;bool present_pending,vrr_failed;} *desktop; + struct {bool valid;} mode_state; + struct delayed_work swap_watchdog_wq; + unsigned long swap_watchdog_deadline; + unsigned swap_watchdog_retrains; + void *crtc,*connector,*dev; +}; +static unsigned scheduled,cancelled,vblanks,hotplugs,unarmed,rejected; +static bool allow_retire=true; +static bool iomfb_retire_dropped_present_h17p(struct apple_dcp *d) {return allow_retire;} +static void dcp_unarm_retained_framebuffers(struct apple_dcp *d,u32 id) +{assert(id==d->desktop->present_id);unarmed++;} +static void dcp_backlight_desktop_finish(struct apple_dcp *d,u32 id,bool reply,bool accepted) +{assert(id==d->desktop->present_id && reply && !accepted);rejected++;} +static void dcp_mode_invalidate(__typeof__(((struct apple_dcp *)0)->mode_state) *m) {m->valid=false;} +static void dcp_drm_crtc_vblank(void *c) {vblanks++;} +static void dcp_queue_hotplug(void *c) {hotplugs++;} +static bool dcp_is_typec_output(struct apple_dcp *d) {return d->typec;} +static void mod_delayed_work(void *q,struct delayed_work *w,unsigned long delay) +{assert(delay==1000);scheduled++;} +static void cancel_delayed_work(struct delayed_work *w) {cancelled++;} +''' + definitions + '\n' + production + r''' +int main(void) { + struct apple_dcp d={.fw_compat=5,.vrr_enabled=true,.mode_state={true},.connector=(void *)1}; + __typeof__(*d.desktop) state={.present_id=42,.present_pending=true};d.desktop=&state; + dcp_swap_watchdog_arm(&d);assert(scheduled==1 && d.swap_watchdog_deadline==1000); + jiffies=999;dcp_swap_watchdog(&d.swap_watchdog_wq.work); + assert(d.mode_state.valid && !hotplugs && !unarmed && state.present_pending); + assert(!dcp_desktop_present_complete(&d,41) && state.present_pending); + assert(dcp_desktop_present_complete(&d,42) && !state.present_pending); + assert(!dcp_desktop_present_complete(&d,42)); + dcp_swap_watchdog_complete(&d);assert(cancelled==1); + jiffies=1000;dcp_swap_watchdog(&d.swap_watchdog_wq.work);assert(!hotplugs); + /* Expiry owns this dropped swap, retains its buffers and triggers reset. */ + state.present_id=43;state.present_pending=true; + dcp_swap_watchdog(&d.swap_watchdog_wq.work); + assert(state.vrr_failed && !d.vrr_enabled && !state.present_pending); + assert(!d.mode_state.valid && hotplugs==1 && vblanks==1 && unarmed==1 && rejected==1); + assert(!dcp_desktop_present_complete(&d,43)); + /* A late completion cannot finish the new fixed-refresh present. */ + state.present_id=44;state.present_pending=true; + assert(!dcp_desktop_present_complete(&d,43) && state.present_pending); + assert(dcp_desktop_present_complete(&d,44)); + unsigned before=scheduled;dcp_swap_watchdog_arm(&d);assert(scheduled==before); + /* The established external watchdog remains armed on a Type-C output. */ + d.fw_compat=1;d.typec=true;dcp_swap_watchdog_arm(&d);assert(scheduled==before+1); + allow_retire=false;d.mode_state.valid=true; + dcp_swap_watchdog(&d.swap_watchdog_wq.work);assert(d.mode_state.valid && hotplugs==1); + return 0; +} +''' diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/run.py b/tools/testing/selftests/drivers/gpu/apple-dcp/run.py index 265dfb9450a80..134b62e97b07e 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/run.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/run.py @@ -57,6 +57,7 @@ def main(): ('display', 'panel-mode-catalog', display.modes, ((),), ()), ('display', 'panel-refresh-range', display.refresh, ((),), ()), ('display', 'panel-adaptive-fallback', display.adaptive, ((),), ()), + ('display', 'panel-adaptive-watchdog', display.watchdog, ((),), ()), ('m4', 'm4-wire-golden', lambda r: m4.golden_check(r, output), (), ()), ('m4', 'm4-protocol', m4.protocol, ((),), ('-Wno-unused-function', '-Wno-unused-const-variable')), ('m4', 'm4-common-plane', m4.plane, ((),), ('-Wno-unused-function', '-Wno-unused-const-variable')), From 30068aecfed9eb6e7f40b1cbd00a221c30dde9ca Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 00:40:32 +1100 Subject: [PATCH 091/388] drm/apple: omit legacy brightness staging from desktop flush Desktop brightness is reserved and encoded after swap_start. Compile the legacy update flag only in the instances that consume it, fixing the unused-variable warning with WERROR enabled. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/iomfb_template.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/drivers/gpu/drm/apple/iomfb_template.c b/drivers/gpu/drm/apple/iomfb_template.c index 234debba0c7dd..0bddf9fa0aba6 100644 --- a/drivers/gpu/drm/apple/iomfb_template.c +++ b/drivers/gpu/drm/apple/iomfb_template.c @@ -3273,7 +3273,10 @@ void DCP_FW_NAME(iomfb_flush)(struct apple_dcp *dcp, struct drm_crtc *crtc, stru struct DCP_FW_NAME(dcp_swap_submit_req) *req = &DCP_FW_SWAP(dcp); int plane_idx, l; int has_surface = 0; + +#if !DCP_FW_DESKTOP_26(DCP_FW_VER) bool update_brightness; +#endif crtc_state = drm_atomic_get_new_crtc_state(commit, crtc); @@ -3409,16 +3412,16 @@ void DCP_FW_NAME(iomfb_flush)(struct apple_dcp *dcp, struct drm_crtc *crtc, stru /* These fields should be set together */ req->swap.swap_completed = req->swap.swap_enabled; +#if DCP_FW_DESKTOP_26(DCP_FW_VER) + /* The shared ramp is encoded after swap_start, at submission time. */ + dcp->brightness.update = false; +#else /* update brightness if changed */ update_brightness = dcp_has_panel(dcp) && dcp->brightness.update; #if DCP_FW_H17(DCP_FW_VER) update_brightness = update_brightness && dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17G; #endif -#if DCP_FW_DESKTOP_26(DCP_FW_VER) - /* The shared ramp is encoded after swap_start, at submission time. */ - dcp->brightness.update = false; -#else if (update_brightness) { #if DCP_FW_LAYOUT_14_7(DCP_FW_VER) req->swap.bl_update = 1; From 7bbacbe6595bb78fcb9a1e932fe456d5d9be387c Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 00:45:32 +1100 Subject: [PATCH 092/388] drm/apple: accept nominal ProMotion rates either side of 120 Hz Keep desktop presentation deadlines for reported rates just above 120 Hz too. The fixed timing remains the rate DCP supplied; this only classifies its ProMotion presentation requirement. Extend the real parser fixture with a slightly higher nominal timing. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/dcp-refresh.h | 2 +- tools/testing/selftests/drivers/gpu/apple-dcp/display.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp-refresh.h b/drivers/gpu/drm/apple/dcp-refresh.h index 1b26758b6ff7b..06a8d1c703294 100644 --- a/drivers/gpu/drm/apple/dcp-refresh.h +++ b/drivers/gpu/drm/apple/dcp-refresh.h @@ -9,7 +9,7 @@ static inline bool dcp_refresh_is_promotion(u32 rate) { return rate >= (120U << 16) - (120U << 16) / 500 && - rate <= (120U << 16); + rate <= (120U << 16) + (120U << 16) / 500; } /* All rates are unsigned 16.16 Hz, including fractional fixed timings. */ diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/display.py b/tools/testing/selftests/drivers/gpu/apple-dcp/display.py index 8ba1f95206837..68049f6799a52 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/display.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/display.py @@ -134,7 +134,7 @@ def case(elements, expected, ranges=None): supported.append(m) case(supported, (120, 60, 59.94, 50, 48, 47.952), [(24<<16, 120<<16)] + [(0,0)]*5) - for hz in (119.88, 120): + for hz in (119.88, 120, 120.01): m=copy.deepcopy(supported[0]); m['VerticalAttributes']['PreciseSyncRate']=round(hz*65536) case([m], [hz], [(24<<16, round(hz*65536))]) for lo, hi, valid in ((48<<16,120<<16,True), (0,120<<16,False), From e8963b5a2f9e5cea8b361bd0362a858a9cb9d75e Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 00:55:00 +1100 Subject: [PATCH 093/388] drm/apple: initialise backlight state in lifecycle KUnit fixtures The hotplug worker synchronises connector backlight state. Construct that state in the relink fixture before running the real worker and balance its teardown. Initialise the desktop retry work in the announcement fixture before teardown can cancel it. This fixes the uninitialised backlight mutex warning with panic_on_warn=1 in the arm64 KUnit run. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/dcp_external_announce_test.c | 1 + drivers/gpu/drm/apple/dcp_external_relink_test.c | 2 ++ 2 files changed, 3 insertions(+) diff --git a/drivers/gpu/drm/apple/dcp_external_announce_test.c b/drivers/gpu/drm/apple/dcp_external_announce_test.c index bd0af843698f9..5ac3880f5d0d3 100644 --- a/drivers/gpu/drm/apple/dcp_external_announce_test.c +++ b/drivers/gpu/drm/apple/dcp_external_announce_test.c @@ -113,6 +113,7 @@ static struct announce_test *announce_test_init(struct kunit *test) INIT_DELAYED_WORK(&dcp->hdmi_recheck_wq, announce_test_noop); INIT_DELAYED_WORK(&dcp->bl_fallback_wq, announce_test_noop); INIT_DELAYED_WORK(&dcp->backlight.fade_work, announce_test_noop); + INIT_DELAYED_WORK(&dcp->backlight.retry_work, announce_test_noop); INIT_WORK(&dcp->external_stop_work, announce_test_noop); INIT_DELAYED_WORK(&dcp->external_stop_retry_wq, announce_test_noop); INIT_WORK(&dcp->vblank_wq, announce_test_noop); diff --git a/drivers/gpu/drm/apple/dcp_external_relink_test.c b/drivers/gpu/drm/apple/dcp_external_relink_test.c index b9d7767be80d8..cf3dfa6d911bf 100644 --- a/drivers/gpu/drm/apple/dcp_external_relink_test.c +++ b/drivers/gpu/drm/apple/dcp_external_relink_test.c @@ -222,6 +222,7 @@ static void relink_test_exit(void *data) wait_for_completion_timeout(&t->userspace_done, 10 * HZ); disable_delayed_work_sync(&t->dcp.announce_wq); disable_work_sync(&t->con.hotplug_wq); + apple_connector_backlight_early_unregister(&t->con); drm_atomic_helper_shutdown(&t->drm); drm_framebuffer_put(&t->fb); } @@ -408,6 +409,7 @@ static struct relink_test *relink_test_init(struct kunit *test) spin_lock_init(&dcp->mode_state.lock); INIT_DELAYED_WORK(&dcp->announce_wq, dcp_announce_work); INIT_WORK(&dcp->vblank_wq, relink_test_noop); + apple_connector_backlight_init(&t->con); INIT_WORK(&t->con.hotplug_wq, dcp_hotplug); dcp->test_point = relink_test_point; dcp->typec_cable_connected = true; From d2d0fddc2d2f29c264b31bef69d15fd93e40f518 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 01:11:43 +1100 Subject: [PATCH 094/388] drm/apple: avoid color updates on desktop brightness-only commits Re-present the affected planes for a brightness-only commit without marking color management changed. With a user gamma LUT installed, that flag would otherwise resend the gamma table at every fade step. Keep the legacy brightness trigger for the other firmware paths. Exercise the real atomic backlight helper with an active gamma LUT, blanked and clean states, and allocation failure in the host runner. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/dcp_backlight.c | 5 +- .../selftests/drivers/gpu/apple-dcp/README | 1 + .../drivers/gpu/apple-dcp/display.py | 57 +++++++++++++++++++ .../selftests/drivers/gpu/apple-dcp/run.py | 1 + 4 files changed, 63 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/apple/dcp_backlight.c b/drivers/gpu/drm/apple/dcp_backlight.c index 475be119789f1..4ed3b573e5101 100644 --- a/drivers/gpu/drm/apple/dcp_backlight.c +++ b/drivers/gpu/drm/apple/dcp_backlight.c @@ -527,7 +527,10 @@ static int drm_crtc_set_brightness(struct apple_dcp *dcp) if (ret) goto out; - crtc_state->color_mgmt_changed |= true; + if (dcp_backlight_desktop(dcp)) + crtc_state->planes_changed = true; + else + crtc_state->color_mgmt_changed = true; ret = drm_atomic_commit(commit); out: diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/README b/tools/testing/selftests/drivers/gpu/apple-dcp/README index f7de1b1179c1d..8a7b42bb7a4fe 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/README +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/README @@ -163,6 +163,7 @@ Internal panel policy --------------------- The display group covers the shared nits ramp on a 600-nit panel, retargeting, blanking, suspend, rejected submits and both submit/completion callback orders. +Brightness-only commits retain the active gamma state without marking it changed. Synthetic TimingElements exercise 120 Hz and fractional lower refresh rates, DCP color/timing identities, invalid geometry and adaptive-sync ranges. The adaptive-sync callback test verifies the desktop parameter record, mode-info diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/display.py b/tools/testing/selftests/drivers/gpu/apple-dcp/display.py index 68049f6799a52..d35feaed549d0 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/display.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/display.py @@ -415,3 +415,60 @@ def watchdog(root): return 0; } ''' + + +def brightness_commit(root): + source=(root/'drivers/gpu/drm/apple/dcp_backlight.c').read_text() + production=block(source,'static int drm_crtc_set_brightness(') + return r''' +#include +#include +#include +#include +#define DRM_MODESET_ACQUIRE_INTERRUPTIBLE 0 +#define IS_ERR(p) false +#define PTR_ERR(p) (-EINVAL) +struct drm_crtc_state {bool active,color_mgmt_changed,planes_changed;void *gamma_lut;}; +struct drm_crtc {void *dev;int mutex;}; +struct drm_modeset_acquire_ctx {int unused;}; +struct drm_atomic_commit {struct drm_modeset_acquire_ctx *acquire_ctx;}; +struct apple_dcp {bool desktop,pending;struct {bool update;} brightness;struct {struct drm_crtc base;} *crtc;}; +static struct drm_atomic_commit commit; +static struct drm_crtc_state state; +static unsigned allocations,commits,planes; +static bool fail_alloc; +static bool dcp_backlight_desktop(struct apple_dcp *d) {return d->desktop;} +static bool dcp_backlight_pending(struct apple_dcp *d) {return d->pending;} +static struct drm_atomic_commit *drm_atomic_commit_alloc(void *dev) +{allocations++;return fail_alloc?NULL:&commit;} +static void drm_modeset_acquire_init(struct drm_modeset_acquire_ctx *c,int flags) {} +static int drm_modeset_lock(int *m,struct drm_modeset_acquire_ctx *c) {assert(!*m);*m=1;return 0;} +static struct drm_crtc_state *drm_atomic_get_crtc_state(struct drm_atomic_commit *a,struct drm_crtc *c) +{assert(c->mutex);return &state;} +static int drm_atomic_add_affected_planes(struct drm_atomic_commit *a,struct drm_crtc *c) +{assert(c->mutex);planes++;return 0;} +static int drm_atomic_commit(struct drm_atomic_commit *a) {commits++;return 0;} +static void drm_atomic_commit_clear(struct drm_atomic_commit *a) {assert(false);} +static int drm_modeset_backoff(struct drm_modeset_acquire_ctx *c) {assert(false);return 0;} +static void drm_atomic_commit_put(struct drm_atomic_commit *a) {} +static void drm_modeset_drop_locks(struct drm_modeset_acquire_ctx *c) {} +static void drm_modeset_acquire_fini(struct drm_modeset_acquire_ctx *c) {} +''' + production + r''' +int main(void) { + struct apple_dcp d={.desktop=true,.pending=true,.brightness={true}}; + __typeof__(*d.crtc) crtc={0};d.crtc=&crtc; + state.active=true;state.gamma_lut=&d; + assert(!drm_crtc_set_brightness(&d)); + assert(commits==1 && planes==1 && state.planes_changed && !state.color_mgmt_changed); + crtc.base.mutex=0;state=(struct drm_crtc_state){.active=true};d.desktop=false; + assert(!drm_crtc_set_brightness(&d)); + assert(commits==2 && planes==2 && state.color_mgmt_changed); + crtc.base.mutex=0;state=(struct drm_crtc_state){.active=false};d.desktop=true; + assert(!drm_crtc_set_brightness(&d) && commits==2); + crtc.base.mutex=0;state.active=true;d.pending=false; + assert(!drm_crtc_set_brightness(&d) && commits==2); + fail_alloc=true;assert(drm_crtc_set_brightness(&d)==-ENOMEM && commits==2); + assert(allocations==5); + return 0; +} +''' diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/run.py b/tools/testing/selftests/drivers/gpu/apple-dcp/run.py index 134b62e97b07e..933c8425c9fbc 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/run.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/run.py @@ -53,6 +53,7 @@ def main(): 'failed-flip-return', 'live-park')) tests = [ ('display', 'panel-backlight-ramp', display.backlight, ((),), ()), + ('display', 'panel-brightness-commit', display.brightness_commit, ((),), ()), ('display', 'panel-brightness-takeover', display.takeover, ((),), ()), ('display', 'panel-mode-catalog', display.modes, ((),), ()), ('display', 'panel-refresh-range', display.refresh, ((),), ()), From eecb5aaa08168237a21778b2d1273dee9b74460a Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 01:40:06 +1100 Subject: [PATCH 095/388] drm/apple: construct parser fixture metadata and notification state Initialise the metadata lock with the mode catalogue, matching probe. The deferred-attribute test also needs an initialised hotplug work item and backlight notification state now that attributes can queue them. Drain those fixture-owned workers before releasing their storage. This fixes the lockdep key report and uninitialised work warning in the strict arm64 parser KUnit run. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/parser-test.c | 44 ++++++++++++++++++----------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/drivers/gpu/drm/apple/parser-test.c b/drivers/gpu/drm/apple/parser-test.c index 9cf57af1b3c94..46b2f6b40579e 100644 --- a/drivers/gpu/drm/apple/parser-test.c +++ b/drivers/gpu/drm/apple/parser-test.c @@ -22,6 +22,13 @@ static struct apple_dcp parser_dcp = { .dev = &parser_device, }; +/* The probe owns the metadata lock beside the mode catalogue. */ +static void parser_modes_init(struct apple_dcp *dcp) +{ + dcp_modes_init(dcp); + spin_lock_init(&dcp->metadata.lock); +} + struct parser_blob { u8 data[8192]; u32 size; @@ -445,7 +452,7 @@ static void parser_replacement(struct kunit *test) struct drm_display_mode requested; KUNIT_ASSERT_NOT_NULL(test, dcp); - dcp_modes_init(dcp); + parser_modes_init(dcp); blob = make_blob(test, &record, 1); KUNIT_ASSERT_NOT_NULL(test, blob); KUNIT_ASSERT_EQ(test, parse(blob->data, blob->size, &ctx), 0); @@ -493,8 +500,7 @@ static void parser_held_keeps_edid(struct kunit *test) KUNIT_ASSERT_NOT_NULL(test, dcp); KUNIT_ASSERT_NOT_NULL(test, connector); KUNIT_ASSERT_NOT_NULL(test, pipeline); - dcp_modes_init(dcp); - spin_lock_init(&dcp->metadata.lock); + parser_modes_init(dcp); apple_connector_edid_init(connector); apple_connector_set_pipeline(connector, pipeline); dcp->connector = connector; @@ -528,7 +534,7 @@ static void parser_attachment_admission(struct kunit *test) struct drm_display_mode requested; KUNIT_ASSERT_NOT_NULL(test, dcp); - dcp_modes_init(dcp); + parser_modes_init(dcp); spin_lock_init(&dcp->mode_state.lock); blob = make_blob(test, &record, 1); KUNIT_ASSERT_NOT_NULL(test, blob); @@ -575,7 +581,7 @@ static void parser_stale_transfer(struct kunit *test) u64 old_generation, current_generation; KUNIT_ASSERT_NOT_NULL(test, dcp); - dcp_modes_init(dcp); + parser_modes_init(dcp); blob = make_blob(test, &record, 1); KUNIT_ASSERT_NOT_NULL(test, blob); KUNIT_ASSERT_EQ(test, parse(blob->data, blob->size, &ctx), 0); @@ -649,7 +655,7 @@ static void parser_route_catalog_handoff(struct kunit *test) KUNIT_ASSERT_NOT_NULL(test, connector); KUNIT_ASSERT_NOT_NULL(test, other); KUNIT_ASSERT_NOT_NULL(test, pipeline); - dcp_modes_init(dcp); + parser_modes_init(dcp); dcp->dev = &pipeline->dev; connector->dcp = pipeline; observer->dcp = dcp; @@ -770,7 +776,7 @@ static void parser_route_programming_guard(struct kunit *test) KUNIT_ASSERT_NOT_NULL(test, connector->base.state); connector->base.state->crtc = &crtc->base; crtc_state.connector_mask = drm_connector_mask(&connector->base); - dcp_modes_init(dcp); + parser_modes_init(dcp); dcp->modes_admitted = true; dcp->fw_compat = DCP_FIRMWARE_V_13_5; dcp->nr_typec_routes = 1; @@ -856,7 +862,7 @@ static void parser_route_invalid_own_mode(struct kunit *test) KUNIT_ASSERT_NOT_NULL(test, dcp); KUNIT_ASSERT_NOT_NULL(test, crtc); KUNIT_ASSERT_NOT_NULL(test, pdev); - dcp_modes_init(dcp); + parser_modes_init(dcp); dcp->fw_compat = DCP_FIRMWARE_V_13_5; dcp->nr_typec_routes = 1; crtc->dcp = pdev; @@ -875,7 +881,7 @@ static void parser_connector_ownership(struct kunit *test) KUNIT_ASSERT_NOT_NULL(test, dcp); KUNIT_ASSERT_NOT_NULL(test, fixed); KUNIT_ASSERT_NOT_NULL(test, borrowed); - dcp_modes_init(dcp); + parser_modes_init(dcp); mutex_lock(&dcp->modes_lock); dcp->connector = borrowed; dcp->modes_admitted = true; @@ -903,7 +909,7 @@ static void parser_route_retirement(struct kunit *test) KUNIT_ASSERT_NOT_NULL(test, fixed); KUNIT_ASSERT_NOT_NULL(test, route); KUNIT_ASSERT_NOT_NULL(test, other); - dcp_modes_init(dcp); + parser_modes_init(dcp); dcp->connector = fixed; dcp->active_typec_route = route; dcp->modes_admitted = true; @@ -927,7 +933,7 @@ static void parser_rejected_replacement(struct kunit *test) struct parser_blob *blob; KUNIT_ASSERT_NOT_NULL(test, dcp); - dcp_modes_init(dcp); + parser_modes_init(dcp); blob = make_blob(test, &record, 1); KUNIT_ASSERT_NOT_NULL(test, blob); KUNIT_ASSERT_EQ(test, parse(blob->data, blob->size, &ctx), 0); @@ -969,7 +975,7 @@ static void parser_attribute_epoch(struct kunit *test) KUNIT_ASSERT_NOT_NULL(test, dcp); KUNIT_ASSERT_NOT_NULL(test, blob); - dcp_modes_init(dcp); + parser_modes_init(dcp); put_unaligned_le32(0xd3, blob->data); blob->size = 4; blob_tag(blob, 1, 3); @@ -1003,7 +1009,7 @@ static void parser_attributes_malformed(struct kunit *test) KUNIT_ASSERT_NOT_NULL(test, dcp); KUNIT_ASSERT_NOT_NULL(test, blob); - dcp_modes_init(dcp); + parser_modes_init(dcp); dcp->width_mm = 520; dcp->height_mm = 290; dcp->ext_backlight = true; @@ -1040,7 +1046,7 @@ static void parser_optional_flag_extent(struct kunit *test) KUNIT_ASSERT_NOT_NULL(test, dcp); KUNIT_ASSERT_NOT_NULL(test, blob); - dcp_modes_init(dcp); + parser_modes_init(dcp); put_unaligned_le32(0xd3, blob->data); blob->size = 4; blob_tag(blob, 1, 3); @@ -1089,11 +1095,12 @@ static void parser_attribute_deferred(struct kunit *test) KUNIT_ASSERT_NOT_NULL(test, connector); KUNIT_ASSERT_NOT_NULL(test, dev); KUNIT_ASSERT_NOT_NULL(test, blob); - dcp_modes_init(dcp); + parser_modes_init(dcp); observer->dcp = dcp; INIT_WORK(&connector->hotplug_wq, parser_catalog_ready); mutex_init(&dev->mode_config.mutex); connector->base.dev = dev; + apple_connector_backlight_init(connector); dcp->connector = connector; dcp->panel.width_mm = 600; dcp->modes = kmalloc_obj(*dcp->modes); @@ -1118,6 +1125,9 @@ static void parser_attribute_deferred(struct kunit *test) KUNIT_EXPECT_EQ(test, connector->base.display_info.width_mm, 520U); KUNIT_EXPECT_EQ(test, connector->base.display_info.height_mm, 290U); dcp_modes_release(dcp); + cancel_work_sync(&connector->hotplug_wq); + cancel_work_sync(&connector->bl_sync_wq); + apple_connector_backlight_early_unregister(connector); } static void parser_dimensions_lifetime(struct kunit *test) @@ -1129,7 +1139,7 @@ static void parser_dimensions_lifetime(struct kunit *test) KUNIT_ASSERT_NOT_NULL(test, dcp); KUNIT_ASSERT_NOT_NULL(test, connector); KUNIT_ASSERT_NOT_NULL(test, dev); - dcp_modes_init(dcp); + parser_modes_init(dcp); mutex_init(&dev->mode_config.mutex); connector->base.dev = dev; dcp->connector = connector; @@ -1158,7 +1168,7 @@ static void parser_catalog_release(struct kunit *test) struct apple_dcp *dcp = kunit_kzalloc(test, sizeof(*dcp), GFP_KERNEL); KUNIT_ASSERT_NOT_NULL(test, dcp); - dcp_modes_init(dcp); + parser_modes_init(dcp); dcp->modes = kmalloc_obj(*dcp->modes); KUNIT_ASSERT_NOT_NULL(test, dcp->modes); dcp->nr_modes = 1; From 367d76551b46a804a3244bc02c60d75a853c0973 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 02:06:07 +1100 Subject: [PATCH 096/388] drm/apple: initialise the session lock in the route preparation test The strict route preparation path snapshots the tunnel session under hpd_mutex and tb_lock before checking the PHY. Initialise both locks in the fixture, matching the probe, so panic_on_warn can reach the intended missing-PHY result. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/parser-test.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/drivers/gpu/drm/apple/parser-test.c b/drivers/gpu/drm/apple/parser-test.c index 46b2f6b40579e..69d7deb3d8357 100644 --- a/drivers/gpu/drm/apple/parser-test.c +++ b/drivers/gpu/drm/apple/parser-test.c @@ -1248,7 +1248,9 @@ static void parser_route_prepare_scope(struct kunit *test) dcp->typec_follow_start = true; dcp->typec_follow_gen = dcp->typec_generation; mutex_init(&dcp->hpd_mutex); + mutex_init(&dcp->tb_lock); KUNIT_EXPECT_EQ(test, dcp_typec_prepare_route(dcp), -ENODEV); + mutex_destroy(&dcp->tb_lock); mutex_destroy(&dcp->hpd_mutex); mutex_destroy(&dcp->modes_lock); } From e940bc6286899f7f9a4c8935f8a2b1d7dc6c4c6c Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 02:36:59 +1100 Subject: [PATCH 097/388] selftests: apple-dcp: keep external rate fixtures scoped after panel VRR Supply the desktop capability boundary for the external H17P rate fixture, which cannot select that firmware, and the refresh rounding helper used by connector range publication. Preserve the firmware lineage and lookup extraction from the current display base. Assisted-by: LLM Signed-off-by: Ryan Murray --- tools/testing/selftests/drivers/gpu/apple-dcp/rate.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py b/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py index 61000228c8b49..db4170e7793e7 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py @@ -681,6 +681,10 @@ def publication(root): #endif /* Nor the desktop 26.6 firmware, the one that withholds VRR modes. */ #define DCP_FIRMWARE_V_26_6_DESKTOP (DCP_FIRMWARE_H17P + 3) +#define DIV_ROUND_UP(n,d) (((n)+(d)-1)/(d)) +/* This fixture selects an external H17P sink, never desktop adaptive sync. */ +static bool dcp_desktop_vrr_supported(struct apple_dcp *d) +{ assert(d->fw_compat != DCP_FIRMWARE_V_26_6_DESKTOP); return false; } '''+firmware_lineage(root)+internal_inline(root,'dcp_fw_is_14_7')+r'''/* Off T6030 machines the direct crossbar output is not clocked separately. */ static int __attribute__((unused)) dcp_direct_crossbar_link(struct apple_dcp *d,bool up) { (void)d;(void)up;return 0; } ''' From 2f6fc9b411ee1636c1c7ce71cb63c898c9c97407 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 03:41:18 +1100 Subject: [PATCH 098/388] selftests: apple-dcp: anchor publication lookup extraction to its definition The desktop VRR admission check calls lookup_mode before its definition. Extract the definition explicitly so the publication fixture does not start in the middle of the admission check or duplicate unrelated helpers. Assisted-by: LLM Signed-off-by: Ryan Murray --- tools/testing/selftests/drivers/gpu/apple-dcp/rate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py b/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py index db4170e7793e7..9176515c38d51 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py @@ -706,7 +706,7 @@ def publication(root): + ''.join(apple_function(root, name) for name in ('dcp_queue_hotplug', 'dcp_modes_for_connector', 'apple_connector_invalidate_edid', 'apple_connector_edid_set_live')) + mode_helpers) - publication = block(iomfb,'static void dcp_clear_connector_edid(')+'\n'+block(iomfb,'static void dcp_consume_edid_invalidation(')+mode_helpers+block(iomfb,'static bool dcp_mode_vrr_supported(')+'\n'+block(iomfb,'int dcp_get_modes(')+block(iomfb,re.search(r'^[^\n;(]*\blookup_mode\(',iomfb,re.M).start()) + publication = block(iomfb,'static void dcp_clear_connector_edid(')+'\n'+block(iomfb,'static void dcp_consume_edid_invalidation(')+mode_helpers+block(iomfb,'static bool dcp_mode_vrr_supported(')+'\n'+block(iomfb,'int dcp_get_modes(')+block(iomfb,'bool lookup_mode(') # Drive malformed replacements through the same chunk/end ingress as firmware. # The dictionary cache is a boundary substitute and takes ownership of the blob. receipt = r''' From 23ff713d4f1dd04f9602cff0fcc3001f1d4d0f95 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 06:10:53 +1100 Subject: [PATCH 099/388] drm/apple: use the internal VRR fallback for zero range fields Desktop firmware may supply both variable-refresh fields as zero placeholders. Treat that pair like an omitted range so the internal ProMotion timing gets its 24 Hz lower bound and native upper bound. Keep partial, duplicate and invalid ranges fixed. Cover the zero pair through the real parser in KUnit and host tests, including rejection of partial zero fields. Assisted-by: LLM Signed-off-by: Ryan Murray --- Documentation/gpu/apple-dcp-t6041-panel.rst | 3 +- drivers/gpu/drm/apple/parser-test.c | 37 ++++++++++++++++++- drivers/gpu/drm/apple/parser.c | 8 +++- .../drivers/gpu/apple-dcp/display.py | 7 +++- 4 files changed, 48 insertions(+), 7 deletions(-) diff --git a/Documentation/gpu/apple-dcp-t6041-panel.rst b/Documentation/gpu/apple-dcp-t6041-panel.rst index c24ebd1e4bfc4..82f5f5445c759 100644 --- a/Documentation/gpu/apple-dcp-t6041-panel.rst +++ b/Documentation/gpu/apple-dcp-t6041-panel.rst @@ -20,7 +20,8 @@ Adaptive sync is experimental and disabled by default. To qualify the internal panel, boot with ``appledrm.t6041_vrr=1``. This enables the connector capability only for the internal main display and a valid adaptive-sync range. The existing internal ProMotion convention supplies 24 Hz to the mode rate when -the firmware omits both range fields; an explicit partial or invalid range +the firmware omits both range fields or reports both as zero placeholders; +a partial, duplicate or invalid range leaves the timing usable at fixed refresh. A rejected adaptive-sync parameter or mode selection gets one retry with diff --git a/drivers/gpu/drm/apple/parser-test.c b/drivers/gpu/drm/apple/parser-test.c index 69d7deb3d8357..749ad582caccd 100644 --- a/drivers/gpu/drm/apple/parser-test.c +++ b/drivers/gpu/drm/apple/parser-test.c @@ -41,6 +41,7 @@ struct parser_record { s64 first_format; bool bad_score_type; bool vrr; + bool vrr_zero; }; enum parser_missing { @@ -150,8 +151,8 @@ static void blob_record(struct parser_blob *blob, const struct parser_record *re record->second_color_missing, DCP_COLOR_FORMAT_RGB); } if (record->vrr) { - blob_int(blob, "MinimumVariableRefreshRate", 48 << 16); - blob_int(blob, "MaximumVariableRefreshRate", 120 << 16); + blob_int(blob, "MinimumVariableRefreshRate", record->vrr_zero ? 0 : 48 << 16); + blob_int(blob, "MaximumVariableRefreshRate", record->vrr_zero ? 0 : 120 << 16); } } @@ -442,6 +443,37 @@ static void parser_notch_vrr(struct kunit *test) kfree(modes); } +static void parser_vrr_empty_range(struct kunit *test) +{ + struct parser_record record = valid_record(7, 10); + struct dcp_display_mode *modes; + struct parser_blob *blob; + unsigned int count; + + /* Desktop firmware can publish both fields as zero placeholders. */ + record.rate = 120 << 16; + record.vrr = true; + record.vrr_zero = true; + blob = make_blob(test, &record, 1); + KUNIT_ASSERT_NOT_NULL(test, blob); + modes = parse_blob(blob, &count, 0, true); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, modes); + KUNIT_ASSERT_EQ(test, count, 1U); + KUNIT_EXPECT_TRUE(test, modes[0].vrr); + KUNIT_EXPECT_EQ(test, modes[0].min_vrr, 24U << 16); + KUNIT_EXPECT_EQ(test, modes[0].max_vrr, 120U << 16); + kfree(modes); + + /* A display without the internal ProMotion policy stays fixed. */ + modes = parse_blob(blob, &count, 0, false); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, modes); + KUNIT_ASSERT_EQ(test, count, 1U); + KUNIT_EXPECT_FALSE(test, modes[0].vrr); + KUNIT_EXPECT_EQ(test, modes[0].min_vrr, 0U); + KUNIT_EXPECT_EQ(test, modes[0].max_vrr, 0U); + kfree(modes); +} + static void parser_replacement(struct kunit *test) { struct parser_record record = valid_record(7, 10); @@ -1264,6 +1296,7 @@ static struct kunit_case parser_cases[] = { KUNIT_CASE(parser_horizontal_rate), KUNIT_CASE(parser_bad_score), KUNIT_CASE(parser_notch_vrr), + KUNIT_CASE(parser_vrr_empty_range), KUNIT_CASE(parser_replacement), KUNIT_CASE(parser_held_keeps_edid), KUNIT_CASE(parser_attachment_admission), diff --git a/drivers/gpu/drm/apple/parser.c b/drivers/gpu/drm/apple/parser.c index ab84a8522ed73..761271339bed1 100644 --- a/drivers/gpu/drm/apple/parser.c +++ b/drivers/gpu/drm/apple/parser.c @@ -875,10 +875,14 @@ static int parse_mode(struct dcp_parse_ctx *handle, if (is_virtual) return -EINVAL; - /* External H17P keeps fixed timings without adaptive-sync support. */ + /* + * External H17P stays fixed. A pair of zero placeholders is also an + * absent range; a partial or duplicate pair must not enable fallback. + */ if (!external_sdr && !vrr_ambiguous) out->vrr = dcp_refresh_range(vert.precise_sync_rate, min_vrr, - max_vrr, internal && !vrr_seen, + max_vrr, internal && + (!vrr_seen || vrr_seen == 3), &out->min_vrr, &out->max_vrr); out->needs_timestamps = out->vrr || (internal && dcp_refresh_is_promotion(vert.precise_sync_rate)); diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/display.py b/tools/testing/selftests/drivers/gpu/apple-dcp/display.py index d35feaed549d0..c53a9d489e51e 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/display.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/display.py @@ -138,10 +138,13 @@ def case(elements, expected, ranges=None): m=copy.deepcopy(supported[0]); m['VerticalAttributes']['PreciseSyncRate']=round(hz*65536) case([m], [hz], [(24<<16, round(hz*65536))]) for lo, hi, valid in ((48<<16,120<<16,True), (0,120<<16,False), - (60<<16,0,False), (0,0,False), (120<<16,24<<16,False), + (60<<16,0,False), (0,0,True), (120<<16,24<<16,False), (24<<16,144<<16,False), (120<<16,120<<16,False)): m=copy.deepcopy(supported[0]); m['MinimumVariableRefreshRate']=lo; m['MaximumVariableRefreshRate']=hi - case([m],[120],[(lo,hi) if valid else (0,0)]) + case([m],[120],[(lo or 24<<16,hi or 120<<16) if valid else (0,0)]) + for key in ('MinimumVariableRefreshRate', 'MaximumVariableRefreshRate'): + m=copy.deepcopy(supported[0]);m[key]=0 + case([m],[120],[(0,0)]) m=copy.deepcopy(supported[0]);m['MinimumVariableRefreshRate']=24<<16 m=f['ObjectPairs'](list(m.items())+[('MinimumVariableRefreshRate',48<<16)]) # Check duplicates against the parser directly without dict indexing in case(). From 6bc143748fb90e7d8946bcdf58cc80206906b87a Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 06:12:42 +1100 Subject: [PATCH 100/388] drm/apple: keep unknown takeover brightness out of restore policy Report ENODATA for desktop actual_brightness until a loader hint or an accepted, completed brightness present establishes the panel level. A reporting midpoint must not be saved and restored as a measured level by userspace. Keep framebuffer unblank and core suspend notifications from acquiring brightness control while the inherited level is unknown. Accept the first bounded loader hint after registration until a user request takes ownership. Test unknown-level reads, lifecycle notifications, rejected presents and explicit writes in KUnit and host coverage. Assisted-by: LLM Signed-off-by: Ryan Murray --- Documentation/gpu/apple-dcp-t6041-panel.rst | 4 +++ drivers/gpu/drm/apple/dcp_backlight.c | 24 +++++++++++--- drivers/gpu/drm/apple/dcp_backlight.h | 15 +++++++++ drivers/gpu/drm/apple/dcp_backlight_test.c | 32 +++++++++++++++++++ .../drivers/gpu/apple-dcp/display.py | 22 ++++++++++--- 5 files changed, 87 insertions(+), 10 deletions(-) diff --git a/Documentation/gpu/apple-dcp-t6041-panel.rst b/Documentation/gpu/apple-dcp-t6041-panel.rst index 82f5f5445c759..94be14250e8f7 100644 --- a/Documentation/gpu/apple-dcp-t6041-panel.rst +++ b/Documentation/gpu/apple-dcp-t6041-panel.rst @@ -8,6 +8,10 @@ integer nits from zero to the panel ceiling, with the same 250 ms fade used by the H17P panel. The backlight interface reports the requested target and ``actual_brightness`` follows accepted, completed presents. A registration default does not change the brightness inherited from the boot loader. +Until a loader hint or a completed user request supplies a known level, +``actual_brightness`` returns ``ENODATA``. This prevents brightness save/restore +services from treating the registration default as a measured panel level. +Framebuffer unblank and core suspend notifications also leave that level alone. Ambient-light policy can use the normal backlight interface, just as a slider does. Requests made while blanked or suspended preserve the restore target. diff --git a/drivers/gpu/drm/apple/dcp_backlight.c b/drivers/gpu/drm/apple/dcp_backlight.c index 4ed3b573e5101..1d8125f32c4d6 100644 --- a/drivers/gpu/drm/apple/dcp_backlight.c +++ b/drivers/gpu/drm/apple/dcp_backlight.c @@ -128,15 +128,20 @@ int dcp_backlight_takeover(struct apple_dcp *dcp, u32 millinits) if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) { unsigned long flags; + bool seeded = false; - /* Freeze the first hint before publishing the class device. */ + /* Accept the first hint until a user request takes ownership. */ spin_lock_irqsave(&dcp->backlight.lock, flags); - if (!dcp->backlight.state.ready && - !dcp->desktop->inherited_brightness_valid) { + if (!dcp->desktop->inherited_brightness_valid && + !dcp->backlight.state.controlled) { dcp->desktop->inherited_brightness = nits; dcp->desktop->inherited_brightness_valid = true; + if (dcp->backlight.state.ready) + seeded = dcp_bl_seed(&dcp->backlight.state, nits); } spin_unlock_irqrestore(&dcp->backlight.lock, flags); + if (seeded && READ_ONCE(dcp->brightness.bl_dev)) + backlight_force_update(dcp->brightness.bl_dev, BACKLIGHT_UPDATE_HOTKEY); return 0; } @@ -148,14 +153,17 @@ static int dcp_get_brightness(struct backlight_device *bd) { struct apple_dcp *dcp = bl_get_data(bd); unsigned long flags; - u32 actual; + int actual; if (READ_ONCE(dcp->quiescing)) return -ENODEV; if (dcp_backlight_active(dcp) || dcp_backlight_desktop(dcp)) { spin_lock_irqsave(&dcp->backlight.lock, flags); - actual = dcp->backlight.state.actual; + if (dcp_backlight_desktop(dcp)) + actual = dcp_bl_actual(&dcp->backlight.state); + else + actual = dcp->backlight.state.actual; spin_unlock_irqrestore(&dcp->backlight.lock, flags); return actual; } @@ -628,6 +636,12 @@ static bool dcp_backlight_controls_device(struct backlight_device *bd, { struct apple_dcp *dcp = bl_get_data(bd); + /* A framebuffer's first unblank must not apply the reporting default. */ + if (dcp_backlight_desktop(dcp) && + !READ_ONCE(dcp->backlight.state.level_known) && + !READ_ONCE(dcp->backlight.state.controlled)) + return false; + return !display_dev || display_dev == dcp->dev || (dcp->crtc && display_dev == dcp->crtc->base.dev->dev); } diff --git a/drivers/gpu/drm/apple/dcp_backlight.h b/drivers/gpu/drm/apple/dcp_backlight.h index 6585e58436e2a..b130a5ff0437e 100644 --- a/drivers/gpu/drm/apple/dcp_backlight.h +++ b/drivers/gpu/drm/apple/dcp_backlight.h @@ -107,6 +107,14 @@ static inline bool dcp_bl_seed(struct dcp_backlight_state *state, u32 nits) return true; } +/* Do not let userspace save or restore an unmeasured registration default. */ +static inline int dcp_bl_actual(const struct dcp_backlight_state *state) +{ + if (!state->ready || !state->level_known) + return -ENODATA; + return state->actual; +} + static inline bool dcp_bl_lit(const struct dcp_backlight_state *state) { return !state->dpms_off && !state->core_blank && !state->suspended; @@ -139,6 +147,13 @@ static inline int dcp_bl_request(struct dcp_backlight_state *state, u32 nits, return -ENODATA; if (nits > state->maximum) return -ERANGE; + /* Core blank/suspend notifications do not establish a panel level. */ + if (!state->controlled && !state->level_known && state->target == nits && + (state->core_blank != core_blank || state->suspended != suspended)) { + state->core_blank = core_blank; + state->suspended = suspended; + return 0; + } state->controlled = true; state->retries = 0; /* diff --git a/drivers/gpu/drm/apple/dcp_backlight_test.c b/drivers/gpu/drm/apple/dcp_backlight_test.c index 61cf775c683fc..74a2b33801542 100644 --- a/drivers/gpu/drm/apple/dcp_backlight_test.c +++ b/drivers/gpu/drm/apple/dcp_backlight_test.c @@ -292,6 +292,37 @@ static void backlight_fallback_test(struct kunit *test) KUNIT_EXPECT_EQ(test, present.nits, 262U); } +static void backlight_unknown_takeover_test(struct kunit *test) +{ + struct dcp_backlight_state state; + struct dcp_backlight_present present; + + KUNIT_ASSERT_EQ(test, dcp_bl_init(&state, 600, false, 0, true, 300), 0); + KUNIT_EXPECT_EQ(test, dcp_bl_actual(&state), -ENODATA); + /* Core suspend/blank and resume do not turn the guess into a write. */ + KUNIT_ASSERT_EQ(test, dcp_bl_request(&state, 300, true, true), 0); + KUNIT_ASSERT_EQ(test, dcp_bl_request(&state, 300, false, false), 0); + KUNIT_EXPECT_FALSE(test, state.controlled); + KUNIT_EXPECT_FALSE(test, state.dirty); + KUNIT_EXPECT_EQ(test, dcp_bl_prepare(&state, true, &present), -EALREADY); + KUNIT_EXPECT_EQ(test, dcp_bl_actual(&state), -ENODATA); + + /* A late loader hint supplies the inherited level without a present. */ + KUNIT_EXPECT_TRUE(test, dcp_bl_seed(&state, 250)); + KUNIT_EXPECT_EQ(test, dcp_bl_actual(&state), 250); + KUNIT_EXPECT_FALSE(test, state.dirty); + + /* An explicit write, even of the default, takes ownership. */ + KUNIT_ASSERT_EQ(test, dcp_bl_init(&state, 600, false, 0, true, 300), 0); + KUNIT_ASSERT_EQ(test, dcp_bl_request(&state, 300, false, false), 0); + KUNIT_ASSERT_EQ(test, dcp_bl_prepare(&state, true, &present), 0); + KUNIT_EXPECT_TRUE(test, dcp_bl_complete(&state, present.sequence, false)); + KUNIT_EXPECT_EQ(test, dcp_bl_actual(&state), -ENODATA); + KUNIT_ASSERT_EQ(test, dcp_bl_prepare(&state, true, &present), 0); + KUNIT_EXPECT_TRUE(test, dcp_bl_complete(&state, present.sequence, true)); + KUNIT_EXPECT_EQ(test, dcp_bl_actual(&state), 300); +} + static void backlight_fallback_same_level_test(struct kunit *test) { struct dcp_backlight_state state; @@ -548,6 +579,7 @@ static struct kunit_case backlight_cases[] = { KUNIT_CASE(backlight_inflight_dpms_test), KUNIT_CASE(backlight_resend_test), KUNIT_CASE(backlight_fallback_test), + KUNIT_CASE(backlight_unknown_takeover_test), KUNIT_CASE(backlight_fallback_same_level_test), KUNIT_CASE(backlight_seed_known_test), KUNIT_CASE(backlight_desktop_swap_test), diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/display.py b/tools/testing/selftests/drivers/gpu/apple-dcp/display.py index c53a9d489e51e..6dce2e1149ff1 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/display.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/display.py @@ -47,14 +47,17 @@ def backlight(root): assert(!dcp_bl_takeover_nits(600,600000,&nits) && nits==600); /* A registration default must never change the loader's visible level. */ assert(!dcp_bl_init(&s,600,false,0,true,250)); s.fade=true; - assert(!s.dirty && !s.level_known); + assert(!s.dirty && !s.level_known && dcp_bl_actual(&s)==-ENODATA); + assert(!dcp_bl_request(&s,250,true,true)); + assert(!dcp_bl_request(&s,250,false,false)); + assert(!s.controlled && !s.dirty && dcp_bl_actual(&s)==-ENODATA); assert(dcp_bl_swap_prepare(&s,&p,id,true)==-EALREADY); assert(!dcp_bl_request(&s,250,false,false)); assert(!s.fading && s.dirty); assert(dcp_bl_swap_prepare(&s,&p,id,false)==-ENODATA); assert(!dcp_bl_swap_prepare(&s,&p,id,true)); completed(&s,&p,id++,true); - assert(s.actual==250 && s.level_known); + assert(s.actual==250 && s.level_known && dcp_bl_actual(&s)==250); assert(!dcp_bl_seed(&s,300)); /* Same state as a slider request or an ALS retarget through backlight. */ assert(!dcp_bl_request(&s,600,false,false) && s.fading); @@ -309,11 +312,14 @@ def takeover(root): #define spin_unlock_irqrestore(p,f) do {assert(*(p));*(p)=false;(void)(f);} while(0) struct apple_dcp { int fw_compat; - struct {u32 maximum;} brightness; + struct {u32 maximum;void *bl_dev;} brightness; struct {bool lock;struct dcp_backlight_state state;} backlight; struct {bool inherited_brightness_valid;u32 inherited_brightness;} *desktop; }; -static unsigned legacy; +#define BACKLIGHT_UPDATE_HOTKEY 1 +static unsigned legacy,notifications; +static void backlight_force_update(void *bd,int reason) +{assert(bd && reason==BACKLIGHT_UPDATE_HOTKEY);notifications++;} bool dcp_backlight_active(struct apple_dcp *d) {return d->fw_compat==1 && d->backlight.state.ready;} static int iomfb_configure_backlight_h17p(struct apple_dcp *d,u32 max,bool valid,u32 nits,bool fallback,u32 guessed) {assert(max==600 && valid && nits==100 && !fallback && !guessed);legacy++;return 0;} @@ -327,7 +333,13 @@ def takeover(root): saved.inherited_brightness_valid=false; assert(dcp_backlight_takeover(&d,1300000)==-ERANGE && !saved.inherited_brightness_valid); assert(!dcp_backlight_takeover(&d,700000) && saved.inherited_brightness==600); - d.backlight.state.ready=true; saved.inherited_brightness_valid=false; + assert(!dcp_bl_init(&d.backlight.state,600,false,0,true,300)); + d.brightness.bl_dev=&d;saved.inherited_brightness_valid=false; + assert(!dcp_backlight_takeover(&d,100000) && saved.inherited_brightness_valid); + assert(dcp_bl_actual(&d.backlight.state)==100 && !d.backlight.state.dirty && notifications==1); + assert(!dcp_backlight_takeover(&d,200000) && d.backlight.state.target==100 && notifications==1); + saved.inherited_brightness_valid=false; + assert(!dcp_bl_request(&d.backlight.state,250,false,false)); assert(!dcp_backlight_takeover(&d,100000) && !saved.inherited_brightness_valid); d.fw_compat=1;d.backlight.state.ready=false; assert(!dcp_backlight_takeover(&d,100000) && legacy==1); From e3bb614fcc56131d3b098d257999b9d244a6c953 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 06:18:44 +1100 Subject: [PATCH 101/388] drm/apple: preserve the panel level when clients echo the initial target Brightness restore clients can read brightness and write the same value back before a loader hint arrives. On the desktop panel that value is a reporting default, so leave the inherited panel level alone. A hint or a different target establishes brightness control. Keep this policy specific to desktop takeover. Add KUnit and host coverage for an echoed midpoint and for a new target becoming known only after its accepted present completes. Assisted-by: LLM Signed-off-by: Ryan Murray --- Documentation/gpu/apple-dcp-t6041-panel.rst | 8 +++++--- drivers/gpu/drm/apple/dcp_backlight.c | 1 + drivers/gpu/drm/apple/dcp_backlight.h | 9 +++++++++ drivers/gpu/drm/apple/dcp_backlight_test.c | 10 ++++++++-- .../testing/selftests/drivers/gpu/apple-dcp/display.py | 6 +++--- 5 files changed, 26 insertions(+), 8 deletions(-) diff --git a/Documentation/gpu/apple-dcp-t6041-panel.rst b/Documentation/gpu/apple-dcp-t6041-panel.rst index 94be14250e8f7..5bbe4ef6872ca 100644 --- a/Documentation/gpu/apple-dcp-t6041-panel.rst +++ b/Documentation/gpu/apple-dcp-t6041-panel.rst @@ -9,9 +9,11 @@ by the H17P panel. The backlight interface reports the requested target and ``actual_brightness`` follows accepted, completed presents. A registration default does not change the brightness inherited from the boot loader. Until a loader hint or a completed user request supplies a known level, -``actual_brightness`` returns ``ENODATA``. This prevents brightness save/restore -services from treating the registration default as a measured panel level. -Framebuffer unblank and core suspend notifications also leave that level alone. +``actual_brightness`` returns ``ENODATA``. Echoing the unchanged registration +value also leaves the panel alone while its level is unknown. A request for a +different target takes control. Once a hint or completed request establishes +the level, all targets are usable. Framebuffer unblank and core suspend +notifications also preserve an unknown inherited level. Ambient-light policy can use the normal backlight interface, just as a slider does. Requests made while blanked or suspended preserve the restore target. diff --git a/drivers/gpu/drm/apple/dcp_backlight.c b/drivers/gpu/drm/apple/dcp_backlight.c index 1d8125f32c4d6..042711030eb4b 100644 --- a/drivers/gpu/drm/apple/dcp_backlight.c +++ b/drivers/gpu/drm/apple/dcp_backlight.c @@ -704,6 +704,7 @@ int dcp_backlight_register(struct apple_dcp *dcp) return ret; } dcp->backlight.state.fade = true; + dcp->backlight.state.preserve_inherited = true; dcp->backlight.kick = dcp_backlight_desktop_kick; } props.brightness = dcp->backlight.state.target; diff --git a/drivers/gpu/drm/apple/dcp_backlight.h b/drivers/gpu/drm/apple/dcp_backlight.h index b130a5ff0437e..7683883137224 100644 --- a/drivers/gpu/drm/apple/dcp_backlight.h +++ b/drivers/gpu/drm/apple/dcp_backlight.h @@ -26,6 +26,8 @@ struct dcp_backlight_state { u8 retries; bool ready; bool controlled; + /* An unchanged registration default cannot take over an unknown level. */ + bool preserve_inherited; bool dpms_off; bool core_blank; bool suspended; @@ -147,6 +149,13 @@ static inline int dcp_bl_request(struct dcp_backlight_state *state, u32 nits, return -ENODATA; if (nits > state->maximum) return -ERANGE; + /* Save/restore clients may echo the reporting default without a change. */ + if (state->preserve_inherited && !state->controlled && !state->level_known && + state->target == nits) { + state->core_blank = core_blank; + state->suspended = suspended; + return 0; + } /* Core blank/suspend notifications do not establish a panel level. */ if (!state->controlled && !state->level_known && state->target == nits && (state->core_blank != core_blank || state->suspended != suspended)) { diff --git a/drivers/gpu/drm/apple/dcp_backlight_test.c b/drivers/gpu/drm/apple/dcp_backlight_test.c index 74a2b33801542..48959efd2665a 100644 --- a/drivers/gpu/drm/apple/dcp_backlight_test.c +++ b/drivers/gpu/drm/apple/dcp_backlight_test.c @@ -298,6 +298,7 @@ static void backlight_unknown_takeover_test(struct kunit *test) struct dcp_backlight_present present; KUNIT_ASSERT_EQ(test, dcp_bl_init(&state, 600, false, 0, true, 300), 0); + state.preserve_inherited = true; KUNIT_EXPECT_EQ(test, dcp_bl_actual(&state), -ENODATA); /* Core suspend/blank and resume do not turn the guess into a write. */ KUNIT_ASSERT_EQ(test, dcp_bl_request(&state, 300, true, true), 0); @@ -312,15 +313,20 @@ static void backlight_unknown_takeover_test(struct kunit *test) KUNIT_EXPECT_EQ(test, dcp_bl_actual(&state), 250); KUNIT_EXPECT_FALSE(test, state.dirty); - /* An explicit write, even of the default, takes ownership. */ + /* Echoing the unknown registration value leaves the panel alone. */ KUNIT_ASSERT_EQ(test, dcp_bl_init(&state, 600, false, 0, true, 300), 0); + state.preserve_inherited = true; KUNIT_ASSERT_EQ(test, dcp_bl_request(&state, 300, false, false), 0); + KUNIT_EXPECT_FALSE(test, state.controlled); + KUNIT_EXPECT_EQ(test, dcp_bl_prepare(&state, true, &present), -EALREADY); + /* A new target takes ownership; rejection still leaves actual unknown. */ + KUNIT_ASSERT_EQ(test, dcp_bl_request(&state, 250, false, false), 0); KUNIT_ASSERT_EQ(test, dcp_bl_prepare(&state, true, &present), 0); KUNIT_EXPECT_TRUE(test, dcp_bl_complete(&state, present.sequence, false)); KUNIT_EXPECT_EQ(test, dcp_bl_actual(&state), -ENODATA); KUNIT_ASSERT_EQ(test, dcp_bl_prepare(&state, true, &present), 0); KUNIT_EXPECT_TRUE(test, dcp_bl_complete(&state, present.sequence, true)); - KUNIT_EXPECT_EQ(test, dcp_bl_actual(&state), 300); + KUNIT_EXPECT_EQ(test, dcp_bl_actual(&state), 250); } static void backlight_fallback_same_level_test(struct kunit *test) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/display.py b/tools/testing/selftests/drivers/gpu/apple-dcp/display.py index 6dce2e1149ff1..85d12dea91ab2 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/display.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/display.py @@ -46,10 +46,10 @@ def backlight(root): u32 id=1, nits; assert(!dcp_bl_takeover_nits(600,600000,&nits) && nits==600); /* A registration default must never change the loader's visible level. */ - assert(!dcp_bl_init(&s,600,false,0,true,250)); s.fade=true; + assert(!dcp_bl_init(&s,600,false,0,true,300)); s.fade=true;s.preserve_inherited=true; assert(!s.dirty && !s.level_known && dcp_bl_actual(&s)==-ENODATA); - assert(!dcp_bl_request(&s,250,true,true)); - assert(!dcp_bl_request(&s,250,false,false)); + assert(!dcp_bl_request(&s,300,true,true)); + assert(!dcp_bl_request(&s,300,false,false)); assert(!s.controlled && !s.dirty && dcp_bl_actual(&s)==-ENODATA); assert(dcp_bl_swap_prepare(&s,&p,id,true)==-EALREADY); assert(!dcp_bl_request(&s,250,false,false)); From 0c677232749fa9b310d1b43d14889927b98bb0ef Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 06:20:46 +1100 Subject: [PATCH 102/388] selftests: apple-dcp: exercise desktop backlight registration and restore Run the actual registration, get_brightness, controls_device and update_status functions with the backlight-core boundary mocked. Check that echoing the midpoint and core suspend notifications preserve an unknown panel level, while a different target takes control and only its completed present publishes actual_brightness. Assisted-by: LLM Signed-off-by: Ryan Murray --- .../drivers/gpu/apple-dcp/desktop.py | 107 ++++++++++++++++++ .../selftests/drivers/gpu/apple-dcp/run.py | 2 + 2 files changed, 109 insertions(+) create mode 100644 tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py new file mode 100644 index 0000000000000..79d9750a7e316 --- /dev/null +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py @@ -0,0 +1,107 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +# Copyright 2026 Ryan Murray +"""Desktop modeset callbacks through the production dispatcher and DMA policy.""" +from common import block + + +def brightness_takeover(root): + import display + source = (root / 'drivers/gpu/drm/apple/dcp_backlight.c').read_text() + prefix = display.backlight(root).split('static void completed(')[0] + prefix = prefix.replace('static inline ', 'static inline __attribute__((unused)) ') + functions = '\n'.join(block(source, marker) for marker in ( + 'static bool dcp_backlight_desktop(', 'static int dcp_get_brightness(', + 'int dcp_backlight_register(', 'static int dcp_set_brightness(', + 'static bool dcp_backlight_controls_device(')) + return prefix + r''' +#include +#define READ_ONCE(x) (x) +#define DCP_FIRMWARE_V_26_6_DESKTOP 5 +#define DCP_FIRMWARE_H17P 1 +#define DCP_IOMFB_METHODS_H17G 7 +#define MAX_BRIGHTNESS_PART2 510U +#define BACKLIGHT_PLATFORM 1 +#define BACKLIGHT_SCALE_LINEAR 1 +#define BL_CORE_SUSPENDED 1 +#define BL_CORE_FBBLANK 2 +#define DCP_BL_FADE_STEP_MS 16 +#define system_dfl_wq NULL +#define IS_ERR(p) false +#define PTR_ERR(p) (-EINVAL) +#define msecs_to_jiffies(n) (n) +#define spin_lock_irqsave(p,f) do {assert(!*(p));*(p)=true;(f)=0;} while(0) +#define spin_unlock_irqrestore(p,f) do {assert(*(p));*(p)=false;(void)(f);} while(0) +struct device {int unused;}; +struct backlight_properties {int type,brightness,max_brightness,scale,state;}; +struct backlight_device {struct backlight_properties props;void *data;}; +struct apple_dcp { + int fw_compat;bool quiescing; + struct {int iomfb_method_profile;} hw; + struct {u32 maximum,nits,dac;bool update;struct backlight_device *bl_dev;} brightness; + struct {bool lock;struct dcp_backlight_state state;unsigned long fade_start; + int fade_work;void (*kick)(struct apple_dcp *);} backlight; + struct {bool inherited_brightness_valid;u32 inherited_brightness;} *desktop; + struct {struct {struct {struct device *dev;} *dev;int mutex;} base;} *crtc; + struct device *dev; +}; +static unsigned long jiffies; +static struct backlight_device registered; +static const int dcp_backlight_ops; +static void *bl_get_data(struct backlight_device *bd) {return bd->data;} +static bool iomfb_is_m4(struct apple_dcp *d) {(void)d;return false;} +bool dcp_backlight_active(struct apple_dcp *d) {(void)d;return false;} +static void dcp_backlight_desktop_kick(struct apple_dcp *d) {(void)d;} +static void dcp_backlight_kick(struct apple_dcp *d) {(void)d;} +static unsigned fades; +static void queue_delayed_work(void *q,int *work,unsigned delay) +{assert(!q && delay==16);(void)work;fades++;} +static struct backlight_device *devm_backlight_device_register(struct device *dev, + const char *name,struct device *parent,void *data,const int *ops, + const struct backlight_properties *props) +{(void)dev;(void)name;(void)parent;assert(ops==&dcp_backlight_ops); + registered=(struct backlight_device){.props=*props,.data=data};return ®istered;} +static int backlight_get_brightness(struct backlight_device *bd) +{return bd->props.state?0:bd->props.brightness;} +static bool backlight_is_blank(struct backlight_device *bd) {return !!bd->props.state;} +static u32 calculate_dac(struct apple_dcp *d,int nits) {(void)d;(void)nits;assert(false);return 0;} +static int drm_modeset_lock_single_interruptible(int *p) {(void)p;assert(false);return 0;} +static void drm_modeset_unlock(int *p) {(void)p;assert(false);} +static int dcp_backlight_update(struct apple_dcp *d) {(void)d;assert(false);return 0;} +''' + functions + r''' +int main(void) +{ + struct apple_dcp d={.fw_compat=5,.brightness={.maximum=600}}; + __typeof__(*d.desktop) saved={0};d.desktop=&saved; + assert(!dcp_backlight_register(&d)); + assert(registered.props.brightness==300 && registered.props.max_brightness==600); + assert(dcp_get_brightness(®istered)==-ENODATA); + assert(!dcp_backlight_controls_device(®istered,NULL)); + /* A save/restore client echoes the exposed value, not a new target. */ + assert(!dcp_set_brightness(®istered)); + assert(!d.backlight.state.controlled && !d.backlight.state.dirty); + registered.props.state=BL_CORE_SUSPENDED|BL_CORE_FBBLANK; + assert(!dcp_set_brightness(®istered)); + registered.props.state=0;assert(!dcp_set_brightness(®istered)); + assert(!d.backlight.state.controlled && !d.backlight.state.dirty); + assert(dcp_get_brightness(®istered)==-ENODATA); + /* A different explicit target takes over the unknown inherited level. */ + registered.props.brightness=250;assert(!dcp_set_brightness(®istered)); + assert(d.backlight.state.controlled && d.backlight.state.dirty && !fades); + assert(dcp_backlight_controls_device(®istered,NULL)); + struct dcp_backlight_present present; + assert(!dcp_bl_prepare(&d.backlight.state,true,&present) && present.nits==250); + assert(dcp_get_brightness(®istered)==-ENODATA); + assert(dcp_bl_complete(&d.backlight.state,present.sequence,true)); + assert(dcp_get_brightness(®istered)==250); + /* Even the former midpoint is an ordinary faded target after takeover. */ + registered.props.brightness=300;assert(!dcp_set_brightness(®istered)); + assert(d.backlight.state.fading && fades==1 && d.backlight.state.target==300); + /* A real loader hint seeds registration without needing a present. */ + d.brightness.bl_dev=NULL;d.backlight.state=(struct dcp_backlight_state){0}; + saved.inherited_brightness_valid=true;saved.inherited_brightness=180; + assert(!dcp_backlight_register(&d) && registered.props.brightness==180); + assert(dcp_get_brightness(®istered)==180); + assert(dcp_backlight_controls_device(®istered,NULL)); + return 0; +} +''' diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/run.py b/tools/testing/selftests/drivers/gpu/apple-dcp/run.py index 933c8425c9fbc..9f08c6ec53499 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/run.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/run.py @@ -30,6 +30,7 @@ import boot import m4 import display +import desktop def parking_unit(root): @@ -53,6 +54,7 @@ def main(): 'failed-flip-return', 'live-park')) tests = [ ('display', 'panel-backlight-ramp', display.backlight, ((),), ()), + ('display', 'panel-takeover-registration', desktop.brightness_takeover, ((),), ()), ('display', 'panel-brightness-commit', display.brightness_commit, ((),), ()), ('display', 'panel-brightness-takeover', display.takeover, ((),), ()), ('display', 'panel-mode-catalog', display.modes, ((),), ()), From 42a2a2e123dcbccb77b49148fa11b28af348e10f Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 07:07:34 +1100 Subject: [PATCH 103/388] drm/apple: keep late loader hints from replacing a user's target A loader hint can arrive after the backlight property snapshot or while a user writes a new target. Synchronize its reporting value under the backlight ops lock and the panel state lock, without the unconditional actual-to-target copy performed by backlight_force_update(). Reconcile a hint that arrived during device registration too. The host registration test covers both that boundary and a user write racing the hint notification. Neither path submits a brightness change on its own. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/dcp_backlight.c | 24 +++++++++++++++-- .../drivers/gpu/apple-dcp/desktop.py | 26 ++++++++++++++++--- .../drivers/gpu/apple-dcp/display.py | 5 ++-- 3 files changed, 47 insertions(+), 8 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp_backlight.c b/drivers/gpu/drm/apple/dcp_backlight.c index 042711030eb4b..381bdedee65b1 100644 --- a/drivers/gpu/drm/apple/dcp_backlight.c +++ b/drivers/gpu/drm/apple/dcp_backlight.c @@ -113,6 +113,23 @@ static void dcp_backlight_desktop_kick(struct apple_dcp *dcp) schedule_work(&dcp->bl_update_wq); } +/* A loader hint describes the inherited level, not a new brightness request. */ +static void dcp_backlight_sync_hint(struct apple_dcp *dcp) +{ + struct backlight_device *bd = READ_ONCE(dcp->brightness.bl_dev); + unsigned long flags; + + if (!bd) + return; + /* Match update_status's lock order and never replace a user's target. */ + mutex_lock(&bd->ops_lock); + spin_lock_irqsave(&dcp->backlight.lock, flags); + if (!dcp->backlight.state.controlled && dcp->backlight.state.level_known) + bd->props.brightness = dcp->backlight.state.target; + spin_unlock_irqrestore(&dcp->backlight.lock, flags); + mutex_unlock(&bd->ops_lock); +} + int dcp_backlight_takeover(struct apple_dcp *dcp, u32 millinits) { u32 maximum, nits; @@ -140,8 +157,8 @@ int dcp_backlight_takeover(struct apple_dcp *dcp, u32 millinits) seeded = dcp_bl_seed(&dcp->backlight.state, nits); } spin_unlock_irqrestore(&dcp->backlight.lock, flags); - if (seeded && READ_ONCE(dcp->brightness.bl_dev)) - backlight_force_update(dcp->brightness.bl_dev, BACKLIGHT_UPDATE_HOTKEY); + if (seeded) + dcp_backlight_sync_hint(dcp); return 0; } @@ -717,6 +734,9 @@ int dcp_backlight_register(struct apple_dcp *dcp) return PTR_ERR(bl_dev); dcp->brightness.bl_dev = bl_dev; + /* A hint may have arrived between the property snapshot and registration. */ + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) + dcp_backlight_sync_hint(dcp); if (!dcp_backlight_active(dcp) && dcp->fw_compat != DCP_FIRMWARE_V_26_6_DESKTOP) dcp->brightness.dac = calculate_dac(dcp, dcp->brightness.nits); diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py index 79d9750a7e316..00b42ac0f8ec0 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py @@ -11,6 +11,7 @@ def brightness_takeover(root): prefix = prefix.replace('static inline ', 'static inline __attribute__((unused)) ') functions = '\n'.join(block(source, marker) for marker in ( 'static bool dcp_backlight_desktop(', 'static int dcp_get_brightness(', + 'static void dcp_backlight_sync_hint(', 'int dcp_backlight_register(', 'static int dcp_set_brightness(', 'static bool dcp_backlight_controls_device(')) return prefix + r''' @@ -33,7 +34,7 @@ def brightness_takeover(root): #define spin_unlock_irqrestore(p,f) do {assert(*(p));*(p)=false;(void)(f);} while(0) struct device {int unused;}; struct backlight_properties {int type,brightness,max_brightness,scale,state;}; -struct backlight_device {struct backlight_properties props;void *data;}; +struct backlight_device {struct backlight_properties props;void *data;bool ops_lock;}; struct apple_dcp { int fw_compat;bool quiescing; struct {int iomfb_method_profile;} hw; @@ -45,6 +46,19 @@ def brightness_takeover(root): struct device *dev; }; static unsigned long jiffies; +static bool inject_hint,inject_user; +static void mutex_lock(bool *p) +{ + assert(!*p);*p=true; + if(inject_user) { + inject_user=false; + struct backlight_device *bd=(void *)((char *)p - offsetof(struct backlight_device,ops_lock)); + struct apple_dcp *d=bd->data; + bd->props.brightness=450; + assert(!dcp_bl_request(&d->backlight.state,450,false,false)); + } +} +static void mutex_unlock(bool *p) {assert(*p);*p=false;} static struct backlight_device registered; static const int dcp_backlight_ops; static void *bl_get_data(struct backlight_device *bd) {return bd->data;} @@ -59,7 +73,9 @@ def brightness_takeover(root): const char *name,struct device *parent,void *data,const int *ops, const struct backlight_properties *props) {(void)dev;(void)name;(void)parent;assert(ops==&dcp_backlight_ops); - registered=(struct backlight_device){.props=*props,.data=data};return ®istered;} + registered=(struct backlight_device){.props=*props,.data=data}; + if(inject_hint) {inject_hint=false;struct apple_dcp *d=data;assert(dcp_bl_seed(&d->backlight.state,180));} + return ®istered;} static int backlight_get_brightness(struct backlight_device *bd) {return bd->props.state?0:bd->props.brightness;} static bool backlight_is_blank(struct backlight_device *bd) {return !!bd->props.state;} @@ -98,10 +114,14 @@ def brightness_takeover(root): assert(d.backlight.state.fading && fades==1 && d.backlight.state.target==300); /* A real loader hint seeds registration without needing a present. */ d.brightness.bl_dev=NULL;d.backlight.state=(struct dcp_backlight_state){0}; - saved.inherited_brightness_valid=true;saved.inherited_brightness=180; + inject_hint=true; assert(!dcp_backlight_register(&d) && registered.props.brightness==180); assert(dcp_get_brightness(®istered)==180); assert(dcp_backlight_controls_device(®istered,NULL)); + /* A user write racing the late-hint notification keeps its target. */ + inject_user=true;dcp_backlight_sync_hint(&d); + assert(d.backlight.state.controlled && d.backlight.state.target==450); + assert(registered.props.brightness==450 && !registered.ops_lock); return 0; } ''' diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/display.py b/tools/testing/selftests/drivers/gpu/apple-dcp/display.py index 85d12dea91ab2..41aaa8b28fe51 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/display.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/display.py @@ -316,10 +316,9 @@ def takeover(root): struct {bool lock;struct dcp_backlight_state state;} backlight; struct {bool inherited_brightness_valid;u32 inherited_brightness;} *desktop; }; -#define BACKLIGHT_UPDATE_HOTKEY 1 static unsigned legacy,notifications; -static void backlight_force_update(void *bd,int reason) -{assert(bd && reason==BACKLIGHT_UPDATE_HOTKEY);notifications++;} +static void dcp_backlight_sync_hint(struct apple_dcp *d) +{assert(d->brightness.bl_dev);notifications++;} bool dcp_backlight_active(struct apple_dcp *d) {return d->fw_compat==1 && d->backlight.state.ready;} static int iomfb_configure_backlight_h17p(struct apple_dcp *d,u32 max,bool valid,u32 nits,bool fallback,u32 guessed) {assert(max==600 && valid && nits==100 && !fallback && !guessed);legacy++;return 0;} From 99766390022d64c236312387a4c945953d01c882 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 07:17:30 +1100 Subject: [PATCH 104/388] drm/apple: scope zero VRR placeholders to desktop firmware A pair of zero range fields is missing metadata on desktop 26.6, but older panel paths may use it as an explicit fixed-refresh range. Carry the firmware convention in the parse context instead of applying it to every internal ProMotion panel. Exercise the actual catalog replacement path in KUnit for desktop and legacy firmware, and retain host coverage for zero placeholders, absent fields, partial ranges and duplicates. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/iomfb.c | 1 + drivers/gpu/drm/apple/parser-test.c | 29 ++++++++++++++++--- drivers/gpu/drm/apple/parser.c | 8 ++--- drivers/gpu/drm/apple/parser.h | 2 ++ .../drivers/gpu/apple-dcp/display.py | 6 ++-- 5 files changed, 36 insertions(+), 10 deletions(-) diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index 6d5e91fa3fe35..1575f57c783bb 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -1630,6 +1630,7 @@ int dcp_modes_replace(struct apple_dcp *dcp, struct dcp_parse_ctx *ctx, if (ret) return ret; } + ctx->zero_vrr_range_fallback = dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP; modes = enumerate_modes(ctx, &nr_modes, dcp->width_mm, dcp->height_mm, dcp->notch_height, dcp->fixed_connector_type == DRM_MODE_CONNECTOR_eDP, diff --git a/drivers/gpu/drm/apple/parser-test.c b/drivers/gpu/drm/apple/parser-test.c index 749ad582caccd..05648a14cf8e6 100644 --- a/drivers/gpu/drm/apple/parser-test.c +++ b/drivers/gpu/drm/apple/parser-test.c @@ -448,7 +448,8 @@ static void parser_vrr_empty_range(struct kunit *test) struct parser_record record = valid_record(7, 10); struct dcp_display_mode *modes; struct parser_blob *blob; - unsigned int count; + unsigned int count, desktop; + struct dcp_parse_ctx ctx; /* Desktop firmware can publish both fields as zero placeholders. */ record.rate = 120 << 16; @@ -459,9 +460,9 @@ static void parser_vrr_empty_range(struct kunit *test) modes = parse_blob(blob, &count, 0, true); KUNIT_ASSERT_NOT_ERR_OR_NULL(test, modes); KUNIT_ASSERT_EQ(test, count, 1U); - KUNIT_EXPECT_TRUE(test, modes[0].vrr); - KUNIT_EXPECT_EQ(test, modes[0].min_vrr, 24U << 16); - KUNIT_EXPECT_EQ(test, modes[0].max_vrr, 120U << 16); + KUNIT_EXPECT_FALSE(test, modes[0].vrr); + KUNIT_EXPECT_EQ(test, modes[0].min_vrr, 0U); + KUNIT_EXPECT_EQ(test, modes[0].max_vrr, 0U); kfree(modes); /* A display without the internal ProMotion policy stays fixed. */ @@ -472,6 +473,26 @@ static void parser_vrr_empty_range(struct kunit *test) KUNIT_EXPECT_EQ(test, modes[0].min_vrr, 0U); KUNIT_EXPECT_EQ(test, modes[0].max_vrr, 0U); kfree(modes); + /* The catalog owner enables this convention only for desktop firmware. */ + for (desktop = 0; desktop < 2; desktop++) { + struct apple_dcp *dcp = kunit_kzalloc(test, sizeof(*dcp), GFP_KERNEL); + u64 generation; + + KUNIT_ASSERT_NOT_NULL(test, dcp); + parser_modes_init(dcp); + dcp->fw_compat = desktop ? DCP_FIRMWARE_V_26_6_DESKTOP : + DCP_FIRMWARE_V_13_5; + dcp->fixed_connector_type = DRM_MODE_CONNECTOR_eDP; + KUNIT_ASSERT_EQ(test, parse(blob->data, blob->size, &ctx), 0); + ctx.dcp = &parser_dcp; + generation = dcp_modes_transfer_begin(dcp); + KUNIT_ASSERT_EQ(test, dcp_modes_replace(dcp, &ctx, generation), 0); + KUNIT_ASSERT_EQ(test, dcp->nr_modes, 1U); + KUNIT_EXPECT_EQ(test, dcp->modes[0].vrr, !!desktop); + KUNIT_EXPECT_EQ(test, dcp->modes[0].min_vrr, desktop ? 24U << 16 : 0U); + KUNIT_EXPECT_EQ(test, dcp->modes[0].max_vrr, desktop ? 120U << 16 : 0U); + kfree(dcp->modes); + } } static void parser_replacement(struct kunit *test) diff --git a/drivers/gpu/drm/apple/parser.c b/drivers/gpu/drm/apple/parser.c index 761271339bed1..25a2562f34807 100644 --- a/drivers/gpu/drm/apple/parser.c +++ b/drivers/gpu/drm/apple/parser.c @@ -724,7 +724,7 @@ static int parse_mode(struct dcp_parse_ctx *handle, struct timing_color_restrictions limits = {}; struct dcp_color_mode unsafe_rgb8 = { .score = -1 }; bool have_horiz = false, have_vert = false, have_virtual = false; - bool duplicate_scope = false, vrr_ambiguous = false; + bool duplicate_scope = false, vrr_ambiguous = false, vrr_fallback; unsigned int vrr_seen = 0; s64 min_vrr = 0, max_vrr = 0; u64 clock; @@ -876,13 +876,13 @@ static int parse_mode(struct dcp_parse_ctx *handle, return -EINVAL; /* - * External H17P stays fixed. A pair of zero placeholders is also an + * External H17P stays fixed. Desktop zero placeholders are also an * absent range; a partial or duplicate pair must not enable fallback. */ + vrr_fallback = !vrr_seen || (vrr_seen == 3 && handle->zero_vrr_range_fallback); if (!external_sdr && !vrr_ambiguous) out->vrr = dcp_refresh_range(vert.precise_sync_rate, min_vrr, - max_vrr, internal && - (!vrr_seen || vrr_seen == 3), + max_vrr, internal && vrr_fallback, &out->min_vrr, &out->max_vrr); out->needs_timestamps = out->vrr || (internal && dcp_refresh_is_promotion(vert.precise_sync_rate)); diff --git a/drivers/gpu/drm/apple/parser.h b/drivers/gpu/drm/apple/parser.h index ef9420c1109f9..135124680ec6a 100644 --- a/drivers/gpu/drm/apple/parser.h +++ b/drivers/gpu/drm/apple/parser.h @@ -21,6 +21,8 @@ struct dcp_parse_ctx { /* External H17P candidates require separate publication/lookup proof. */ bool automatic_1080p180_rgb8; bool ambiguous_180hz; + /* Desktop 26.6 uses zero placeholders for an unreported VRR range. */ + bool zero_vrr_range_fallback; #if IS_ENABLED(CONFIG_DRM_APPLE_DCP_T6030_AUDIO) /* * T6030 external processor audio elements (DCP firmware 14.7, external diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/display.py b/tools/testing/selftests/drivers/gpu/apple-dcp/display.py index 41aaa8b28fe51..e91202924f264 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/display.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/display.py @@ -103,14 +103,14 @@ def modes(root): checks = [] blobs = [] - def case(elements, expected, ranges=None): + def case(elements, expected, ranges=None, desktop=True): raw = bytearray(struct.pack(' Date: Mon, 26 Oct 2026 08:35:28 +1100 Subject: [PATCH 105/388] drm/apple: report takeover brightness and fade the first change Keep the inherited panel level untouched until an explicit target changes. Report the target while no loader hint or completed present has supplied a measured level, then report the completed level. An unchanged reporting default still cannot acquire brightness control. Use the reporting level as the first fade's origin when the loader report is absent. A valid hint remains the preferred origin. Cover readback, unchanged restore, the first fade and its completion in KUnit and the host backlight fixture. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/dcp_backlight.h | 9 ++--- drivers/gpu/drm/apple/dcp_backlight_test.c | 36 ++++++++++++++++--- .../drivers/gpu/apple-dcp/desktop.py | 11 +++--- .../drivers/gpu/apple-dcp/display.py | 8 +++-- 4 files changed, 48 insertions(+), 16 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp_backlight.h b/drivers/gpu/drm/apple/dcp_backlight.h index 7683883137224..704e985f49a78 100644 --- a/drivers/gpu/drm/apple/dcp_backlight.h +++ b/drivers/gpu/drm/apple/dcp_backlight.h @@ -109,12 +109,12 @@ static inline bool dcp_bl_seed(struct dcp_backlight_state *state, u32 nits) return true; } -/* Do not let userspace save or restore an unmeasured registration default. */ +/* Report a usable target until a hint or completed present gives the level. */ static inline int dcp_bl_actual(const struct dcp_backlight_state *state) { - if (!state->ready || !state->level_known) + if (!state->ready) return -ENODATA; - return state->actual; + return state->level_known ? state->actual : state->target; } static inline bool dcp_bl_lit(const struct dcp_backlight_state *state) @@ -174,7 +174,8 @@ static inline int dcp_bl_request(struct dcp_backlight_state *state, u32 nits, state->target = nits; state->core_blank = core_blank; state->suspended = suspended; - if (state->fade && state->level_known && was_lit && + /* With no hint, the reporting target is the first fade's origin. */ + if (state->fade && (state->level_known || state->preserve_inherited) && was_lit && dcp_bl_lit(state) && state->level != nits) { state->fade_from = state->level; state->fading = true; diff --git a/drivers/gpu/drm/apple/dcp_backlight_test.c b/drivers/gpu/drm/apple/dcp_backlight_test.c index 48959efd2665a..57029cecb2f23 100644 --- a/drivers/gpu/drm/apple/dcp_backlight_test.c +++ b/drivers/gpu/drm/apple/dcp_backlight_test.c @@ -299,14 +299,14 @@ static void backlight_unknown_takeover_test(struct kunit *test) KUNIT_ASSERT_EQ(test, dcp_bl_init(&state, 600, false, 0, true, 300), 0); state.preserve_inherited = true; - KUNIT_EXPECT_EQ(test, dcp_bl_actual(&state), -ENODATA); + KUNIT_EXPECT_EQ(test, dcp_bl_actual(&state), 300); /* Core suspend/blank and resume do not turn the guess into a write. */ KUNIT_ASSERT_EQ(test, dcp_bl_request(&state, 300, true, true), 0); KUNIT_ASSERT_EQ(test, dcp_bl_request(&state, 300, false, false), 0); KUNIT_EXPECT_FALSE(test, state.controlled); KUNIT_EXPECT_FALSE(test, state.dirty); KUNIT_EXPECT_EQ(test, dcp_bl_prepare(&state, true, &present), -EALREADY); - KUNIT_EXPECT_EQ(test, dcp_bl_actual(&state), -ENODATA); + KUNIT_EXPECT_EQ(test, dcp_bl_actual(&state), 300); /* A late loader hint supplies the inherited level without a present. */ KUNIT_EXPECT_TRUE(test, dcp_bl_seed(&state, 250)); @@ -319,16 +319,43 @@ static void backlight_unknown_takeover_test(struct kunit *test) KUNIT_ASSERT_EQ(test, dcp_bl_request(&state, 300, false, false), 0); KUNIT_EXPECT_FALSE(test, state.controlled); KUNIT_EXPECT_EQ(test, dcp_bl_prepare(&state, true, &present), -EALREADY); - /* A new target takes ownership; rejection still leaves actual unknown. */ + /* A new target is reported even before its present completes. */ KUNIT_ASSERT_EQ(test, dcp_bl_request(&state, 250, false, false), 0); KUNIT_ASSERT_EQ(test, dcp_bl_prepare(&state, true, &present), 0); KUNIT_EXPECT_TRUE(test, dcp_bl_complete(&state, present.sequence, false)); - KUNIT_EXPECT_EQ(test, dcp_bl_actual(&state), -ENODATA); + KUNIT_EXPECT_EQ(test, dcp_bl_actual(&state), 250); KUNIT_ASSERT_EQ(test, dcp_bl_prepare(&state, true, &present), 0); KUNIT_EXPECT_TRUE(test, dcp_bl_complete(&state, present.sequence, true)); KUNIT_EXPECT_EQ(test, dcp_bl_actual(&state), 250); } +static void backlight_first_change_fades_test(struct kunit *test) +{ + struct dcp_backlight_state state; + struct dcp_backlight_present present; + + KUNIT_ASSERT_EQ(test, dcp_bl_init(&state, 600, false, 0, true, 300), 0); + state.preserve_inherited = true; + state.fade = true; + KUNIT_EXPECT_EQ(test, dcp_bl_actual(&state), 300); + KUNIT_ASSERT_EQ(test, dcp_bl_request(&state, 300, false, false), 0); + KUNIT_EXPECT_FALSE(test, state.dirty); + KUNIT_EXPECT_FALSE(test, state.controlled); + KUNIT_ASSERT_EQ(test, dcp_bl_request(&state, 100, false, false), 0); + KUNIT_EXPECT_TRUE(test, state.fading); + KUNIT_EXPECT_FALSE(test, state.level_known); + KUNIT_EXPECT_FALSE(test, state.dirty); + KUNIT_EXPECT_EQ(test, state.fade_from, 300U); + KUNIT_EXPECT_EQ(test, dcp_bl_actual(&state), 100); + KUNIT_EXPECT_TRUE(test, dcp_bl_fade_step(&state, 125, 250)); + KUNIT_EXPECT_EQ(test, state.level, 200U); + KUNIT_ASSERT_EQ(test, dcp_bl_prepare(&state, true, &present), 0); + KUNIT_EXPECT_TRUE(test, dcp_bl_complete(&state, present.sequence, true)); + KUNIT_EXPECT_EQ(test, dcp_bl_actual(&state), 200); + KUNIT_EXPECT_FALSE(test, dcp_bl_fade_step(&state, 250, 250)); + KUNIT_EXPECT_EQ(test, state.level, 100U); +} + static void backlight_fallback_same_level_test(struct kunit *test) { struct dcp_backlight_state state; @@ -586,6 +613,7 @@ static struct kunit_case backlight_cases[] = { KUNIT_CASE(backlight_resend_test), KUNIT_CASE(backlight_fallback_test), KUNIT_CASE(backlight_unknown_takeover_test), + KUNIT_CASE(backlight_first_change_fades_test), KUNIT_CASE(backlight_fallback_same_level_test), KUNIT_CASE(backlight_seed_known_test), KUNIT_CASE(backlight_desktop_swap_test), diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py index 00b42ac0f8ec0..f6d0cd4de7c64 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py @@ -90,7 +90,7 @@ def brightness_takeover(root): __typeof__(*d.desktop) saved={0};d.desktop=&saved; assert(!dcp_backlight_register(&d)); assert(registered.props.brightness==300 && registered.props.max_brightness==600); - assert(dcp_get_brightness(®istered)==-ENODATA); + assert(dcp_get_brightness(®istered)==300); assert(!dcp_backlight_controls_device(®istered,NULL)); /* A save/restore client echoes the exposed value, not a new target. */ assert(!dcp_set_brightness(®istered)); @@ -99,19 +99,20 @@ def brightness_takeover(root): assert(!dcp_set_brightness(®istered)); registered.props.state=0;assert(!dcp_set_brightness(®istered)); assert(!d.backlight.state.controlled && !d.backlight.state.dirty); - assert(dcp_get_brightness(®istered)==-ENODATA); + assert(dcp_get_brightness(®istered)==300); /* A different explicit target takes over the unknown inherited level. */ registered.props.brightness=250;assert(!dcp_set_brightness(®istered)); - assert(d.backlight.state.controlled && d.backlight.state.dirty && !fades); + assert(d.backlight.state.controlled && d.backlight.state.fading && !d.backlight.state.dirty && fades==1); + assert(!dcp_bl_fade_step(&d.backlight.state,250,250)); assert(dcp_backlight_controls_device(®istered,NULL)); struct dcp_backlight_present present; assert(!dcp_bl_prepare(&d.backlight.state,true,&present) && present.nits==250); - assert(dcp_get_brightness(®istered)==-ENODATA); + assert(dcp_get_brightness(®istered)==250); assert(dcp_bl_complete(&d.backlight.state,present.sequence,true)); assert(dcp_get_brightness(®istered)==250); /* Even the former midpoint is an ordinary faded target after takeover. */ registered.props.brightness=300;assert(!dcp_set_brightness(®istered)); - assert(d.backlight.state.fading && fades==1 && d.backlight.state.target==300); + assert(d.backlight.state.fading && fades==2 && d.backlight.state.target==300); /* A real loader hint seeds registration without needing a present. */ d.brightness.bl_dev=NULL;d.backlight.state=(struct dcp_backlight_state){0}; inject_hint=true; diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/display.py b/tools/testing/selftests/drivers/gpu/apple-dcp/display.py index e91202924f264..e5793a84e0bb0 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/display.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/display.py @@ -47,13 +47,15 @@ def backlight(root): assert(!dcp_bl_takeover_nits(600,600000,&nits) && nits==600); /* A registration default must never change the loader's visible level. */ assert(!dcp_bl_init(&s,600,false,0,true,300)); s.fade=true;s.preserve_inherited=true; - assert(!s.dirty && !s.level_known && dcp_bl_actual(&s)==-ENODATA); + assert(!s.dirty && !s.level_known && dcp_bl_actual(&s)==300); assert(!dcp_bl_request(&s,300,true,true)); assert(!dcp_bl_request(&s,300,false,false)); - assert(!s.controlled && !s.dirty && dcp_bl_actual(&s)==-ENODATA); + assert(!s.controlled && !s.dirty && dcp_bl_actual(&s)==300); assert(dcp_bl_swap_prepare(&s,&p,id,true)==-EALREADY); assert(!dcp_bl_request(&s,250,false,false)); - assert(!s.fading && s.dirty); + assert(s.fading && !s.dirty); + assert(dcp_bl_fade_step(&s,125,250) && s.level==275); + assert(!dcp_bl_fade_step(&s,250,250) && s.level==250); assert(dcp_bl_swap_prepare(&s,&p,id,false)==-ENODATA); assert(!dcp_bl_swap_prepare(&s,&p,id,true)); completed(&s,&p,id++,true); From 1a20d7709805ea69cd246daf2056fcf4692eb428 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 08:58:52 +1100 Subject: [PATCH 106/388] drm/apple: bind the remaining desktop memory and resize callbacks The shared template pairs descriptor allocation and release with D452 physical mapping. The public native 26.6 backend also handles D588 as an empty resize notification. Bind both in the desktop table after its D202/D454 adapters, preserving the shared display-register bounds and resource ownership checks. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/iomfb_v26_6_desktop.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c b/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c index 05417d2c2b2cb..4a398d4db33f0 100644 --- a/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c +++ b/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c @@ -178,6 +178,7 @@ static const iomfb_cb_handler cb_handlers[IOMFB_MAX_CB] = { [414] = trampoline_sr_set_property_int, [415] = trampoline_true, /* sr_set_property_bool */ [451] = trampoline_allocate_buffer, + [452] = trampoline_map_physical, [454] = trampoline_release_mem_desc_v26_6, [552] = trampoline_true, /* set_property_dict_0 */ [561] = trampoline_true, /* set_property_dict */ @@ -190,6 +191,7 @@ static const iomfb_cb_handler cb_handlers[IOMFB_MAX_CB] = { [576] = trampoline_nop, [581] = trampoline_nop, [582] = trampoline_true, /* create_default_fb_surface */ + [588] = trampoline_nop, /* resize_default_fb_surface_gated */ [590] = trampoline_swap_complete, [592] = trampoline_swap_complete_intent_gated, [593] = trampoline_abort_swap_ap_gated, From b81e397b79468fd4bf872dd32fbc528ef65b86b0 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 08:59:39 +1100 Subject: [PATCH 107/388] drm/apple: drain unsupported desktop modesets before reporting failure Bound callback envelopes and descriptor records before decoding. An unknown callback during a mode set gets an unsupported zero reply and records an error. Drain the real enclosing RPC ACK, stop the continuation and return EOPNOTSUPP. Signal an error on the accepted atomic operation out-fence. A timed-out command retains ownership of its busy channel. Exercise the contributor-provided desktop D202/D454 adapters and their normal allocation lifecycle. Inject an unknown callback during A413, drain the ACK, fail that set and complete a later set. Check every mode-selection continuation boundary. Keep the panel mode list from DCP now that the known mode-change callbacks are handled. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/apple_drv.c | 7 +- drivers/gpu/drm/apple/dcp-internal.h | 3 + drivers/gpu/drm/apple/iomfb.c | 23 +- drivers/gpu/drm/apple/iomfb_template.c | 28 +++ drivers/gpu/drm/apple/iomfb_v26_6_desktop.c | 18 ++ drivers/gpu/drm/apple/iomfb_v26_6_desktop.h | 2 + .../selftests/drivers/gpu/apple-dcp/Makefile | 2 +- .../drivers/gpu/apple-dcp/desktop.py | 231 ++++++++++++++++++ .../drivers/gpu/apple-dcp/display.py | 15 +- .../drivers/gpu/apple-dcp/lifecycle.py | 8 +- .../selftests/drivers/gpu/apple-dcp/run.py | 4 +- 11 files changed, 330 insertions(+), 11 deletions(-) diff --git a/drivers/gpu/drm/apple/apple_drv.c b/drivers/gpu/drm/apple/apple_drv.c index 2ce9fb420f2f4..8fc8cb7f7a6ab 100644 --- a/drivers/gpu/drm/apple/apple_drv.c +++ b/drivers/gpu/drm/apple/apple_drv.c @@ -268,8 +268,11 @@ static void apple_crtc_atomic_enable(struct drm_crtc *crtc, /* A cable generation can change while the CRTC stays active. */ dcp_external_recover(to_apple_crtc(crtc)->dcp); } - if (crtc_state->active) - dcp_crtc_atomic_modeset(crtc, commit); + if (crtc_state->active) { + ret = dcp_crtc_atomic_modeset(crtc, commit); + if (ret) + dcp_drm_crtc_error(apple_crtc, crtc_state, ret); + } dcp_typec_follow_done(locked); return; diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index f3c8ec3cc2fa5..9aa05a5b3b257 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -243,6 +243,9 @@ struct dcp_desktop_state { * element before the real one is selected. */ bool mode_live; + /* RX records an unsupported callback until the modeset RPC has drained. */ + bool modeset_active; + bool modeset_callback_error; /* An opt-in adaptive-sync failure stays fixed until the next boot. */ bool vrr_failed; bool vrr_requested; diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index 1575f57c783bb..6cc67676fb9db 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -1299,7 +1299,8 @@ static void dcpep_handle_cb(struct apple_dcp *dcp, enum dcp_context_id context, u8 depth; bool handled, zero_reply; - if (dcp->fw_compat == DCP_FIRMWARE_H17P && + if ((dcp->fw_compat == DCP_FIRMWARE_H17P || + dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) && (length < sizeof(*hdr) || !ch || ch->depth >= DCP_MAX_CALL_DEPTH || offset >= 0x8000 || length > 0x8000 - offset)) { dev_err(dev, "invalid IOMFB callback envelope\n"); @@ -1309,6 +1310,14 @@ static void dcpep_handle_cb(struct apple_dcp *dcp, enum dcp_context_id context, tag = dcp_parse_tag(hdr->tag); + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && + (!iomfb_check_callback_v26_6_desktop(tag, hdr->in_len, hdr->out_len) || + (u64)sizeof(*hdr) + hdr->in_len + hdr->out_len != length)) { + dev_err(dev, "invalid desktop IOMFB callback %d (%u, %u)\n", + tag, hdr->in_len, hdr->out_len); + dcp->crashed = true; + return; + } handled = tag >= 0 && tag < IOMFB_MAX_CB && dcp->cb_handlers && dcp->cb_handlers[tag]; @@ -1344,11 +1353,15 @@ static void dcpep_handle_cb(struct apple_dcp *dcp, enum dcp_context_id context, return; } /* - * Leaving a callback unanswered wedges the coprocessor: it - * waits for the ack forever and the outer call never returns. - * On H17P answer it with a zeroed output instead. + * Drain an unsupported desktop callback too. The outer modeset + * must see an error even if firmware accepts the zero reply; + * never pop its command before its real ACK arrives. */ - if (dcp->fw_compat == DCP_FIRMWARE_H17P) { + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && + READ_ONCE(dcp->desktop->modeset_active)) + WRITE_ONCE(dcp->desktop->modeset_callback_error, true); + if (dcp->fw_compat == DCP_FIRMWARE_H17P || + dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) { if (hdr->out_len) memset(out, 0, hdr->out_len); depth = dcp_push_depth(&ch->depth); diff --git a/drivers/gpu/drm/apple/iomfb_template.c b/drivers/gpu/drm/apple/iomfb_template.c index 0bddf9fa0aba6..abb1eb8bd6dac 100644 --- a/drivers/gpu/drm/apple/iomfb_template.c +++ b/drivers/gpu/drm/apple/iomfb_template.c @@ -2857,6 +2857,15 @@ struct dcp_modeset_cookie { u32 min_vrr; }; +/* Finish only after the RPC which contained the unknown callback has replied. */ +static bool dcp_modeset_callback_failed(struct apple_dcp *dcp, void *cookie) +{ + if (!READ_ONCE(dcp->desktop->modeset_callback_error)) + return false; + complete_set_digital_out_mode(dcp, NULL, cookie); + return true; +} + /* * get_mode_info (A364) fills the firmware's mode record for a timing and * color pair before set_digital_out_mode (A413) selects it. The request @@ -2899,6 +2908,8 @@ static void dcp_modeset_actual_done(struct apple_dcp *dcp, void *data, { struct dcp_modeset_cookie *modeset = cookie; + if (dcp_modeset_callback_failed(dcp, cookie)) + return; if (data && get_unaligned_le32(data) && modeset->vrr) { dcp_modeset_fixed_fallback(dcp, modeset); return; @@ -2911,6 +2922,8 @@ static void dcp_modeset_after_actual_info(struct apple_dcp *dcp, void *data, { struct dcp_modeset_cookie *modeset = cookie; + if (dcp_modeset_callback_failed(dcp, cookie)) + return; if (!data) { complete_set_digital_out_mode(dcp, NULL, cookie); return; @@ -2924,6 +2937,8 @@ static void dcp_modeset_after_bootstrap(struct apple_dcp *dcp, void *data, { struct dcp_modeset_cookie *modeset = cookie; + if (dcp_modeset_callback_failed(dcp, cookie)) + return; if (!data || get_unaligned_le32(data)) { dcp_modeset_actual_done(dcp, data, cookie); return; @@ -2937,6 +2952,8 @@ static void dcp_modeset_after_bootstrap_info(struct apple_dcp *dcp, void *data, { struct dcp_modeset_cookie *modeset = cookie; + if (dcp_modeset_callback_failed(dcp, cookie)) + return; if (!data) { complete_set_digital_out_mode(dcp, NULL, cookie); return; @@ -2963,6 +2980,8 @@ static void dcp_modeset_sync_done(struct apple_dcp *dcp, void *data, { struct dcp_modeset_cookie *modeset = cookie; + if (dcp_modeset_callback_failed(dcp, cookie)) + return; if (!data || get_unaligned_le32(data)) { if (data && modeset->vrr) { dcp_modeset_fixed_fallback(dcp, modeset); @@ -2999,6 +3018,9 @@ int DCP_FW_NAME(iomfb_modeset)(struct apple_dcp *dcp, if (dcp->crashed) return -ENODEV; + /* A timed-out command still owns its channel until firmware replies. */ + if (READ_ONCE(dcp->ch_cmd.depth)) + return -EBUSY; if (!lookup_mode(dcp, &crtc_state->mode, mode)) { dev_err(dcp->dev, "no match for " DRM_MODE_FMT "\n", @@ -3077,6 +3099,8 @@ int DCP_FW_NAME(iomfb_modeset)(struct apple_dcp *dcp, modeset->bootstrap.timing_mode_id); } + WRITE_ONCE(dcp->desktop->modeset_callback_error, false); + WRITE_ONCE(dcp->desktop->modeset_active, true); if (modeset->program_sync) dcp_modeset_sync(dcp, cookie); else @@ -3097,6 +3121,9 @@ int DCP_FW_NAME(iomfb_modeset)(struct apple_dcp *dcp, dev_err(dcp->dev, "waiting on set_digital_out_mode failed:%d\n", ret); ret = -EIO; + } else if (READ_ONCE(dcp->desktop->modeset_callback_error)) { + dev_err(dcp->dev, "unsupported callback during mode set\n"); + ret = -EOPNOTSUPP; } else if (!READ_ONCE(cookie->replied)) { dev_err(dcp->dev, "set_digital_out_mode got no reply\n"); ret = -EIO; @@ -3110,6 +3137,7 @@ int DCP_FW_NAME(iomfb_modeset)(struct apple_dcp *dcp, jiffies_to_msecs(ret)); ret = 0; } + WRITE_ONCE(dcp->desktop->modeset_active, false); dcp->vrr_enabled = !ret && modeset->vrr; if (modeset->notify_vrr_failure && dcp->connector) dcp_queue_hotplug(dcp->connector); diff --git a/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c b/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c index 4a398d4db33f0..ed7f5faadf0f6 100644 --- a/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c +++ b/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c @@ -91,6 +91,24 @@ static bool trampoline_passthrough_dict_v26_6(struct apple_dcp *dcp, int tag, return true; } +bool iomfb_check_callback_v26_6_desktop(int tag, u32 in_len, u32 out_len) +{ + switch (tag) { + case 201: + return in_len == 12 && out_len == 16; + case 202: + return in_len == 24 && !out_len; + case 451: + return in_len == 20 && out_len == 28; + case 452: + return in_len == 24 && out_len == 20; + case 454: + return in_len == 4 && out_len == 4; + default: + return true; + } +} + /* * unmap_piodma (D202): the buffer id, the id again, the DVA (unaligned) and a * trailing word. The older firmware sends the DVA after a virtual address. diff --git a/drivers/gpu/drm/apple/iomfb_v26_6_desktop.h b/drivers/gpu/drm/apple/iomfb_v26_6_desktop.h index 697edc47fde3b..6f61493858e8b 100644 --- a/drivers/gpu/drm/apple/iomfb_v26_6_desktop.h +++ b/drivers/gpu/drm/apple/iomfb_v26_6_desktop.h @@ -49,4 +49,6 @@ struct dcp_set_frame_sync_props_resp_v26_6_desktop { u8 props[0x50]; } __packed; +bool iomfb_check_callback_v26_6_desktop(int tag, u32 in_len, u32 out_len); + #endif /* __APPLE_IOMFB_V26_6_DESKTOP_H__ */ diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/Makefile b/tools/testing/selftests/drivers/gpu/apple-dcp/Makefile index af6d27b20f062..2c25d757aca5e 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/Makefile +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/Makefile @@ -1,4 +1,4 @@ # SPDX-License-Identifier: GPL-2.0-only OR MIT TEST_PROGS := run.sh m4-error-paths.py plane-layout.py -TEST_FILES := run.py common.py edid.py recovery.py rate.py phy.py lifecycle.py pm.py idle.py boot.py quiesce.py transaction.py accounting.py m4.py m4-fixtures native-m4-kms.c run-native-m4-kms.py dwc3 README +TEST_FILES := run.py common.py desktop.py display.py edid.py recovery.py rate.py phy.py lifecycle.py pm.py idle.py boot.py quiesce.py transaction.py accounting.py m4.py m4-fixtures native-m4-kms.c run-native-m4-kms.py dwc3 README include ../../../lib.mk diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py index f6d0cd4de7c64..1ccc513a96f35 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py @@ -2,6 +2,195 @@ # Copyright 2026 Ryan Murray """Desktop modeset callbacks through the production dispatcher and DMA policy.""" from common import block +import lifecycle + + +def modeset_unmap(root): + base = root / 'drivers/gpu/drm/apple' + iomfb = (base / 'iomfb.c').read_text() + template = (base / 'iomfb_template.c').read_text() + desktop = (base / 'iomfb_v26_6_desktop.c').read_text() + header = (base / 'iomfb_v26_6_desktop.h').read_text() + # Reuse the transport boundary mocks, retaining the real callback dispatcher. + prefix = lifecycle.dispatcher(root).split('static bool dcp_external_zero_reply(')[0] + prefix = prefix.replace('struct dcp_channel { unsigned depth; void *output[8]; unsigned end[8]; };', r''' +struct apple_dcp; +typedef void (*dcp_callback_t)(struct apple_dcp *, void *, void *); +struct dcp_channel { + unsigned depth; void *output[8], *cookies[8]; unsigned end[8]; + dcp_callback_t callbacks[8]; struct dcp_packet_header header[8]; +}; +struct sg_table {void *sgl;}; +struct dcp_mem_descriptor {void *buf;u64 dva,reg;size_t size;bool piodma_mapped;struct sg_table map;}; +''') + prefix = prefix.replace(' struct dcp_channel channel;', r''' + struct dcp_channel channel, command; + struct dcp_mem_descriptor memdesc[4]; + unsigned long memdesc_map[1];void *iommu_dom; + struct resource {u64 start,end;} *disp_registers[1];int nr_disp_registers; +''') + prefix = prefix.replace('DCP_CONTEXT_CB = 0, DCP_CONTEXT_ASYNC = 3', + 'DCP_CONTEXT_CB = 0, DCP_CONTEXT_CMD = 1, DCP_CONTEXT_ASYNC = 3') + prefix = prefix.replace('(void)c; return &d->channel;', + 'return c==DCP_CONTEXT_CMD?&d->command:&d->channel;') + prefix = prefix.replace('((void)dev)', '((void)dcp->dev)') + prefix += r''' +#include +typedef u64 __le64;typedef u32 __le32; +#define __packed __attribute__((packed)) +typedef u64 dma_addr_t; +#define DCP_FW_NAME(n) n +#define DCP_FW_H17(v) 0 +#define DCP_FW_DESKTOP_26(v) 1 +#define SZ_16K 16384U +#define ARRAY_SIZE(a) (sizeof(a)/sizeof((a)[0])) +#define DCP_MAX_MAPPINGS 4 +#undef static_assert +#define static_assert(c) _Static_assert(c,#c) +#define trace_iomfb_callback(...) ((void)0) +#define dev_warn_dma(...) ((void)0) +struct dcp_map_physical_req {u64 paddr,size;u32 flags;u8 dva_null,dva_size_null,padding[2];} __attribute__((packed)); +struct dcp_map_physical_resp {u64 dva,dva_size;u32 mem_desc_id;} __attribute__((packed)); +struct dcp_map_buf_req {u64 buffer;}; +struct dcp_map_buf_resp {u64 buffer,dva;}; +struct dcp_unmap_buf_resp {u64 buffer,vaddr,dva;u8 unk,buf_null;}; +struct dcp_wait_cookie {unsigned status;bool replied;int done,refcount;}; +#define dev_warn_release(...) ((void)0) +#define dev_err_release(...) ((void)0) +#define DMA_BIDIRECTIONAL 0 +#define round_down(x,y) ((x) & ~((__typeof__(x))(y)-1)) +#define check_add_overflow(a,b,p) __builtin_add_overflow(a,b,p) +static unsigned physical_maps; +static unsigned find_first_zero_bit(unsigned long *map,unsigned count) +{for(unsigned i=0;isgl);} +static void dma_free_coherent(struct device *dev,size_t size,void *buf,u64 dva) +{assert(dev && size==SZ_16K && buf && dva==0x8000);frees++;} +static void dma_unmap_resource(struct device *dev,u64 dva,size_t size,int dir,int flags) +{assert(dev && dva==0x8000 && size==4096 && !dir && !flags);resource_unmaps++;} +static void clear_bit(unsigned b,unsigned long *p) {*p &= ~(1UL<sgl && flags==3);maps++;return SZ_16K;} +static size_t iommu_unmap(void *domain,u64 dva,size_t size) +{assert(domain && dva==0x8000 && size==SZ_16K);unmaps++;return size;} +''' + # dev logging in the DMA helper has no local 'dev', unlike the dispatcher. + unmap = block(template, 'static bool dcpep_unmap_piodma(') + unmap = unmap.replace('dev_warn(', 'dev_warn_dma(').replace('dev_err(', 'dev_warn_dma(') + handlers = block(desktop, 'static const iomfb_cb_handler cb_handlers[') + binding = '\n'.join(line for line in handlers.splitlines() if '[202]' in line or '[454]' in line) + mapping = block(template, 'static bool dcpep_map_piodma(').replace('dev_err(', 'dev_warn_dma(') + release = block(template, 'static u8 dcpep_cb_release_mem_desc(').replace('dev_warn(', 'dev_warn_release(').replace('dev_err(', 'dev_err_release(') + physical = block(template, 'static bool is_disp_register(') + '\n' + block(template, 'static struct dcp_map_physical_resp\ndcpep_cb_map_physical(').replace('dev_err(', 'dev_err_release(').replace('dev_warn(', 'dev_warn_release(') + physical += r""" +static bool trampoline_map_physical(struct apple_dcp *dcp,int tag,void *out,void *in) +{struct dcp_map_physical_resp r=dcpep_cb_map_physical(dcp,in);(void)tag;memcpy(out,&r,sizeof(r));return true;} +""" + # Assert the table still binds the shared production mapper. + assert '[452] = trampoline_map_physical,' in handlers + binding += '\n[452] = trampoline_map_physical,' + wire = block(desktop, 'struct dcp_unmap_buf_req_v26_6 {')+';\n' + production = (wire + '\n' + physical + '\n' + release + '\n' + block(desktop, 'static bool trampoline_release_mem_desc_v26_6(') + '\n' + mapping + '\n' + unmap + '\n' + block(desktop, 'static bool trampoline_unmap_piodma_v26_6(') + + '\n' + block(iomfb, 'static bool dcp_external_zero_reply(') + + '\n' + block(iomfb, 'static void dcpep_handle_cb(').replace('struct device *dev =', 'struct device *dev __attribute__((unused)) =') + + '\n' + block(iomfb, 'static void dcpep_handle_ack(') + + '\n' + block(template, 'static void complete_set_digital_out_mode(')) + return prefix + production + '\n' + r''' +int main(void) +{ + struct device_node node={0};struct device dev={&node}; + bool (*handlers[IOMFB_MAX_CB])(struct apple_dcp *,int,void *,void *)={ +''' + binding + r''' + }; + struct apple_dcp d={.dev=&dev,.fw_compat=DCP_FIRMWARE_V_26_6_DESKTOP, + .cb_handlers=handlers,.memdesc_map={2},.iommu_dom=&d}; + union {u64 align;u8 data[sizeof(struct dcp_packet_header)+24];} packet; + struct dcp_packet_header *h=(void *)packet.data; + struct {struct dcp_packet_header h;u32 input[2];u32 status;} command={ + .h={.in_len=8,.out_len=4},.status=0}; + struct dcp_wait_cookie wait={.status=~0U,.refcount=2}; + /* The firmware pauses an A413 modeset until its D202 is acknowledged. */ + __typeof__(*d.desktop) desktop={0};d.desktop=&desktop; + d.command.depth=1;d.command.header[0]=command.h; + d.command.callbacks[0]=complete_set_digital_out_mode; + d.command.cookies[0]=&wait;d.command.output[0]=&command.status; + d.memdesc[1]=(struct dcp_mem_descriptor){.buf=&d,.dva=0x8000,.size=4096,.map={.sgl=&d}}; + struct dcp_map_buf_req map_request={.buffer=1};struct dcp_map_buf_resp map_reply; + assert(dcpep_map_piodma(&d,&map_request,&map_reply)); + assert(maps==1 && map_reply.buffer==1 && map_reply.dva==0x8000); + memset(&packet,0,sizeof(packet));h->in_len=24; + struct dcp_unmap_buf_req_v26_6 fields={.buffer=1,.id=1,.dva=0x8000};memcpy(packet.data+sizeof(*h),&fields,24); + case_tag=202;acks=0; + dcpep_handle_cb(&d,DCP_CONTEXT_CB,h,sizeof(*h)+24,0); + assert(acks==1 && !d.crashed && !d.channel.depth && unmaps==1); + assert(!d.memdesc[1].piodma_mapped && d.command.depth==1 && !wait.done); + /* No firmware response can arrive until the empty callback ACK above. */ + dcpep_handle_ack(&d,DCP_CONTEXT_CMD); + assert(wait.done && wait.replied && !wait.status && wait.refcount==1 && !d.command.depth); + /* A later mode can reuse a descriptor after the firmware unmaps it. */ + assert(dcpep_map_piodma(&d,&map_request,&map_reply) && maps==2); + /* Descriptor release handles both RAM and mapped resources, once. */ + fields.buffer=1;fields.dva=0x8000;memcpy(packet.data+sizeof(*h),&fields,24); + dcpep_handle_cb(&d,DCP_CONTEXT_CB,h,sizeof(*h)+24,0); + assert(unmaps==2); + case_tag=454;h->in_len=4;h->out_len=4; + put_unaligned_le32(1,packet.data+sizeof(*h)); + memset(packet.data+sizeof(*h)+4,0xa5,4); + dcpep_handle_cb(&d,DCP_CONTEXT_CB,h,sizeof(*h)+8,0); + assert(get_unaligned_le32(packet.data+sizeof(*h)+4)==1); + assert(frees==1 && !d.memdesc_map[0] && !d.memdesc[1].buf && !d.memdesc[1].size); + dcpep_handle_cb(&d,DCP_CONTEXT_CB,h,sizeof(*h)+8,0); + assert(!get_unaligned_le32(packet.data+sizeof(*h)+4) && frees==1); + d.memdesc_map[0]=1; + struct resource reg={.start=0x123400,.end=0x1243ff};d.disp_registers[0]=®d.nr_disp_registers=1; + struct dcp_map_physical_req request={.paddr=0x123400,.size=4096}; + struct {struct dcp_packet_header h;struct dcp_map_physical_req in;struct dcp_map_physical_resp out;} __attribute__((packed)) phys={.h={.in_len=24,.out_len=20},.in=request}; + case_tag=452; + dcpep_handle_cb(&d,DCP_CONTEXT_CB,&phys,sizeof(phys),0); + assert(physical_maps==1 && phys.out.mem_desc_id==1 && phys.out.dva==0x8000 && phys.out.dva_size==4096); + assert(d.memdesc_map[0]==3 && d.memdesc[1].reg==0x123400); + case_tag=454;put_unaligned_le32(1,packet.data+sizeof(*h)); + dcpep_handle_cb(&d,DCP_CONTEXT_CB,h,sizeof(*h)+8,0); + assert(get_unaligned_le32(packet.data+sizeof(*h)+4)==1 && resource_unmaps==1 && d.memdesc_map[0]==1); + for(u32 id=4;id<8;id++) { + put_unaligned_le32(id,packet.data+sizeof(*h)); + dcpep_handle_cb(&d,DCP_CONTEXT_CB,h,sizeof(*h)+8,0); + assert(!get_unaligned_le32(packet.data+sizeof(*h)+4) && frees==1); + } + /* Out-of-range and overflowing physical regions receive no mapping. */ + case_tag=452;phys.in.paddr=0x124000; + dcpep_handle_cb(&d,DCP_CONTEXT_CB,&phys,sizeof(phys),0); + assert(!phys.out.dva_size && physical_maps==1); + phys.in.paddr=~0ULL;phys.in.size=8192; + dcpep_handle_cb(&d,DCP_CONTEXT_CB,&phys,sizeof(phys),0); + assert(!phys.out.dva_size && physical_maps==1); + unsigned before=acks; + assert(!iomfb_check_callback_v26_6_desktop(454,4,1)); + assert(!iomfb_check_callback_v26_6_desktop(452,23,20)); + /* The declared desktop shape is exact, with no guessed reply bytes. */ + assert(!iomfb_check_callback_v26_6_desktop(202,26,0)); + assert(!iomfb_check_callback_v26_6_desktop(202,24,4)); + case_tag=202;h->in_len=23;h->out_len=0; + dcpep_handle_cb(&d,DCP_CONTEXT_CB,h,sizeof(*h)+24,0); + assert(d.crashed && acks==before && unmaps==2); + return 0; +} +''' def brightness_takeover(root): @@ -126,3 +315,45 @@ def brightness_takeover(root): return 0; } ''' + + +def modeset_unknown(root): + template = (root / 'drivers/gpu/drm/apple/iomfb_template.c').read_text() + unit = modeset_unmap(root) + helper = block(template, 'static bool dcp_modeset_callback_failed(') + # Keep the real modeset completion and its callback-error check. The + # adaptive retry boundary must never be reached after an unknown callback. + helper += r''' +struct dcp_modeset_cookie {struct dcp_wait_cookie wait;bool vrr;}; +static void dcp_modeset_fixed_fallback(struct apple_dcp *d,struct dcp_modeset_cookie *m) +{(void)d;(void)m;assert(false);} +''' + block(template, 'static void dcp_modeset_actual_done(') + unit = unit.replace('int main(void)', helper + '\nint main(void)') + tests = r''' + /* A fresh channel has an A413 outstanding; inject a valid but unknown D. */ + d.crashed=false;desktop.modeset_active=true;desktop.modeset_callback_error=false; + struct dcp_modeset_cookie mode={.wait={.status=~0U,.refcount=2},.vrr=true}; + d.command.depth=1;d.command.header[0]=command.h; + d.command.callbacks[0]=dcp_modeset_actual_done; + d.command.cookies[0]=&mode;d.command.output[0]=&command.status; + case_tag=499;h->in_len=4;h->out_len=4; + memset(packet.data+sizeof(*h)+4,0xa5,4);before=acks; + dcpep_handle_cb(&d,DCP_CONTEXT_CB,h,sizeof(*h)+8,0); + assert(acks==before+1 && !d.crashed && !d.channel.depth); + assert(!get_unaligned_le32(packet.data+sizeof(*h)+4)); + assert(desktop.modeset_callback_error && d.command.depth==1 && !mode.wait.done); + /* Draining the real ACK fails the set, even if firmware reports success. */ + dcpep_handle_ack(&d,DCP_CONTEXT_CMD); + assert(mode.wait.done && !mode.wait.replied && mode.wait.status==~0U); + assert(mode.wait.refcount==1 && !d.command.depth); + /* The next fixed modeset is independent and completes normally. */ + desktop.modeset_active=false;desktop.modeset_callback_error=false; + mode=(struct dcp_modeset_cookie){.wait={.status=~0U,.refcount=2}}; + d.command.depth=1;d.command.header[0]=command.h; + d.command.callbacks[0]=dcp_modeset_actual_done; + d.command.cookies[0]=&mode;d.command.output[0]=&command.status; + dcpep_handle_ack(&d,DCP_CONTEXT_CMD); + assert(mode.wait.done && mode.wait.replied && !mode.wait.status); + assert(!d.crashed && !d.command.depth && !d.channel.depth); +''' + return unit.replace(' return 0;\n}', tests + ' return 0;\n}') diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/display.py b/tools/testing/selftests/drivers/gpu/apple-dcp/display.py index e5793a84e0bb0..e64e4f9a16473 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/display.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/display.py @@ -197,6 +197,7 @@ def refresh(root): def adaptive(root): source=(root/'drivers/gpu/drm/apple/iomfb_template.c').read_text() funcs='\n'.join(block(source, marker) for marker in ( + 'static bool dcp_modeset_callback_failed(', 'static void dcp_modeset_fixed_fallback(', 'static void dcp_modeset_actual_done(', 'static void dcp_modeset_after_actual_info(', @@ -224,7 +225,7 @@ def adaptive(root): struct dcp_wait_cookie { u32 status; bool done,replied; }; struct dcp_set_digital_out_mode_req {u32 color_mode_id,timing_mode_id;}; struct dcp_set_parameter_dcp { u32 param; u64 value[4]; u32 count; }; -struct apple_dcp {void *dev;int fw_compat;bool main_display,panel;struct {bool vrr_failed;} *desktop;}; +struct apple_dcp {void *dev;int fw_compat;bool main_display,panel;struct {bool vrr_failed,modeset_callback_error;} *desktop;}; static bool t6041_vrr; struct drm_display_mode {int id;}; struct drm_crtc_state {bool vrr_enabled;struct drm_display_mode mode;}; @@ -298,6 +299,18 @@ def adaptive(root): dcp_modeset_sync(&d,&m);reply(&d,1,&zero); assert(requested.color_mode_id==13 && requested.timing_mode_id==12); reply(&d,2,&zero);reply(&d,3,&zero);selected(&d,&m); + /* Unsupported callbacks at every chained RPC stop after its real ACK. */ + for(unsigned phase=0;phase<5;phase++) { + desktop.vrr_failed=false;desktop.modeset_callback_error=false; + initialize(&m);m.use_bootstrap=true;dcp_modeset_sync(&d,&m); + if(phase>0)reply(&d,1,&zero); + if(phase>1)reply(&d,2,&zero); + if(phase>2)reply(&d,3,&zero); + if(phase>3)reply(&d,2,&zero); + desktop.modeset_callback_error=true; + reply(&d,kind,&zero); + assert(m.wait.done && !m.wait.replied && !pending && !desktop.vrr_failed); + } assert(parameters && infos && modes); return 0; } diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py index 582156af8ca4d..cc82ade333fb3 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py @@ -1840,7 +1840,10 @@ def dispatcher(root): typedef uint16_t u16; typedef uint32_t u32; typedef uint64_t u64; +#define READ_ONCE(x) (x) +#define WRITE_ONCE(x,v) ((x)=(v)) #define DCP_FIRMWARE_H17P 1 +#define DCP_FIRMWARE_V_26_6_DESKTOP 5 #define DCP_IOMFB_METHODS_H17P_EXTERNAL 2 #define DCP_IOMFB_METHODS_H17G 3 #define DCP_MAX_CALL_DEPTH 8 @@ -1863,6 +1866,7 @@ def dispatcher(root): bool (**cb_handlers)(struct apple_dcp *, int, void *, void *); unsigned long unknown_callbacks[16]; int start_done; + struct {bool modeset_active,modeset_callback_error;} *desktop; struct dcp_channel channel; }; static bool machine; @@ -1922,4 +1926,6 @@ def dispatcher(root): } """ - return prefix + source[start:end] + suffix + desktop = (root / "drivers/gpu/drm/apple/iomfb_v26_6_desktop.c").read_text() + checker = block(desktop, 'bool iomfb_check_callback_v26_6_desktop(') + return prefix + checker + source[start:end] + suffix diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/run.py b/tools/testing/selftests/drivers/gpu/apple-dcp/run.py index 9f08c6ec53499..1adcfe62035f5 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/run.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/run.py @@ -26,11 +26,11 @@ import transaction import accounting import idle +import desktop import rate import boot import m4 import display -import desktop def parking_unit(root): @@ -53,6 +53,8 @@ def main(): 'orientation-faults', 'backend-change', 'safe-first', 'failed-flip-return', 'live-park')) tests = [ + ('lifecycle', 'desktop-modeset-unmap', desktop.modeset_unmap, ((),), ()), + ('lifecycle', 'desktop-unknown-modeset-recovery', desktop.modeset_unknown, ((),), ()), ('display', 'panel-backlight-ramp', display.backlight, ((),), ()), ('display', 'panel-takeover-registration', desktop.brightness_takeover, ((),), ()), ('display', 'panel-brightness-commit', display.brightness_commit, ((),), ()), From 0618cae05907a6ad923b442cba5254960723ec42 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 09:04:16 +1100 Subject: [PATCH 108/388] drm/apple: publish the desktop panel identity as a valid EDID The internal desktop panel has no external EDID service. Its product identity was also excluded from the firmware-derived EDID path, leaving userspace without monitor identity strings. Parse the panel ProductAttributes independently of optional color fields and publish an identity-only EDID base block. Keep DCP TimingElements as the authoritative mode list, including timings which EDID cannot encode. Use the panel dimensions already supplied by the device tree, and retain the firmware vendor, product and serial without embedding device data. Protect replacement and allocation with an identity generation check. Cover the real parser, EDID checksum and identity bytes, metadata replacement and other-profile exclusion in host tests, plus KUnit. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/dcp-display-metadata.c | 58 +++++++++++++++ drivers/gpu/drm/apple/dcp-display-metadata.h | 3 + drivers/gpu/drm/apple/dcp-internal.h | 8 ++ drivers/gpu/drm/apple/dcp.c | 54 ++++++++++++++ drivers/gpu/drm/apple/iomfb.c | 8 +- drivers/gpu/drm/apple/iomfb_template.c | 6 ++ drivers/gpu/drm/apple/parser-test.c | 35 +++++++++ drivers/gpu/drm/apple/parser.c | 74 +++++++++++++++++-- drivers/gpu/drm/apple/parser.h | 1 + .../drivers/gpu/apple-dcp/desktop.py | 64 ++++++++++++++++ .../selftests/drivers/gpu/apple-dcp/rate.py | 4 + .../selftests/drivers/gpu/apple-dcp/run.py | 1 + 12 files changed, 308 insertions(+), 8 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp-display-metadata.c b/drivers/gpu/drm/apple/dcp-display-metadata.c index cc4c69736d098..5bc60abd95e4b 100644 --- a/drivers/gpu/drm/apple/dcp-display-metadata.c +++ b/drivers/gpu/drm/apple/dcp-display-metadata.c @@ -202,3 +202,61 @@ const struct drm_edid *dcp_metadata_edid(const struct dcp_display_metadata *meta } return edid; } + +/* + * Internal modes come from TimingElements. Supply identity without inventing + * an EDID timing, chromaticities or gamma when the panel omits those fields. + */ +const struct drm_edid *dcp_panel_edid(const struct dcp_display_metadata *metadata, + u32 width_mm, u32 height_mm) +{ + struct edid block = {}; + const struct drm_edid *edid; + char serial[14]; + u16 manufacturer = 0; + u8 sum = 0; + int ret; + + if (strnlen(metadata->manufacturer, 4) != 3 || metadata->product > U16_MAX || + !width_mm || width_mm > 2550 || !height_mm || height_mm > 2550) + return ERR_PTR(-EINVAL); + for (unsigned int i = 0; i < 3; i++) { + if (metadata->manufacturer[i] < 'A' || metadata->manufacturer[i] > 'Z') + return ERR_PTR(-EINVAL); + manufacturer = manufacturer << 5 | (metadata->manufacturer[i] - '@'); + } + memcpy(block.header, "\x00\xff\xff\xff\xff\xff\xff\x00", 8); + block.product_id.manufacturer_name = cpu_to_be16(manufacturer); + block.product_id.product_code = cpu_to_le16(metadata->product); + block.product_id.serial_number = cpu_to_le32(metadata->serial); + block.version = 1; + block.revision = 3; + block.input = DRM_EDID_INPUT_DIGITAL; + block.width_cm = DIV_ROUND_CLOSEST(width_mm, 10); + block.height_cm = DIV_ROUND_CLOSEST(height_mm, 10); + block.gamma = 0xff; /* Unspecified. */ + memset(block.standard_timings, 1, sizeof(block.standard_timings)); + for (unsigned int i = 0; i < ARRAY_SIZE(block.detailed_timings); i++) + block.detailed_timings[i].data.other_data.type = 0x10 /* Dummy descriptor. */; + ret = dcp_metadata_text(&block.detailed_timings[0], EDID_DETAIL_MONITOR_NAME, + metadata->name); + if (ret) + return ERR_PTR(ret); + /* A zero numeric serial is a valid unspecified EDID serial, not NULL. */ + snprintf(serial, sizeof(serial), "%08x", metadata->serial); + ret = dcp_metadata_text(&block.detailed_timings[1], EDID_DETAIL_MONITOR_SERIAL, + metadata->serial_text[0] ? metadata->serial_text : serial); + if (ret) + return ERR_PTR(ret); + for (unsigned int i = 0; i < sizeof(block) - 1; i++) + sum += ((const u8 *)&block)[i]; + block.checksum = -sum; + edid = drm_edid_alloc(&block, sizeof(block)); + if (!edid) + return ERR_PTR(-ENOMEM); + if (!drm_edid_valid(edid)) { + drm_edid_free(edid); + return ERR_PTR(-EINVAL); + } + return edid; +} diff --git a/drivers/gpu/drm/apple/dcp-display-metadata.h b/drivers/gpu/drm/apple/dcp-display-metadata.h index 537994420ebd6..b7ec1505d5728 100644 --- a/drivers/gpu/drm/apple/dcp-display-metadata.h +++ b/drivers/gpu/drm/apple/dcp-display-metadata.h @@ -21,4 +21,7 @@ struct drm_display_mode; const struct drm_edid *dcp_metadata_edid(const struct dcp_display_metadata *metadata, const struct drm_display_mode *mode); +const struct drm_edid *dcp_panel_edid(const struct dcp_display_metadata *metadata, + u32 width_mm, u32 height_mm); + #endif diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index 9aa05a5b3b257..ef8cc5b093573 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -243,6 +243,10 @@ struct dcp_desktop_state { * element before the real one is selected. */ bool mode_live; + /* Identity supplied by this fixed panel, independent of optional color data. */ + struct dcp_display_metadata panel_metadata; + u64 panel_metadata_generation; + bool panel_metadata_valid; /* RX records an unsupported callback until the modeset RPC has drained. */ bool modeset_active; bool modeset_callback_error; @@ -301,6 +305,10 @@ struct dcpext_scanout; struct dcp_hdmi; bool dcp_desktop_present_complete(struct apple_dcp *dcp, u32 id); +void dcp_panel_metadata_replace(struct apple_dcp *dcp, + const struct dcp_display_metadata *metadata); +const struct drm_edid *dcp_panel_edid_snapshot(struct apple_dcp *dcp, u64 *generation); +bool dcp_panel_metadata_current(struct apple_dcp *dcp, u64 generation); bool dcp_desktop_vrr_supported(struct apple_dcp *dcp); int dcp_desktop_vrr_check(struct apple_dcp *dcp, struct drm_crtc_state *state); diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 67f8aba5a5acd..dbfb03fa2a040 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -776,6 +776,60 @@ const struct drm_edid *dcp_firmware_edid(struct apple_dcp *dcp, u64 *generation) return edid; } +/* Internal panel identity has no detachable Type-C service or EDID transport. */ +void dcp_panel_metadata_replace(struct apple_dcp *dcp, + const struct dcp_display_metadata *metadata) +{ + unsigned long flags; + + if (dcp->fw_compat != DCP_FIRMWARE_V_26_6_DESKTOP || !dcp_has_panel(dcp)) + return; + spin_lock_irqsave(&dcp->metadata.lock, flags); + dcp->desktop->panel_metadata_generation++; + dcp->desktop->panel_metadata_valid = !!metadata; + if (metadata) + dcp->desktop->panel_metadata = *metadata; + spin_unlock_irqrestore(&dcp->metadata.lock, flags); + if (dcp->connector) + dcp_queue_hotplug(dcp->connector); +} + +bool dcp_panel_metadata_current(struct apple_dcp *dcp, u64 generation) +{ + unsigned long flags; + bool valid; + + spin_lock_irqsave(&dcp->metadata.lock, flags); + valid = dcp->desktop->panel_metadata_valid && + generation == dcp->desktop->panel_metadata_generation; + spin_unlock_irqrestore(&dcp->metadata.lock, flags); + return valid; +} + +const struct drm_edid *dcp_panel_edid_snapshot(struct apple_dcp *dcp, u64 *generation) +{ + struct dcp_display_metadata metadata; + const struct drm_edid *edid; + unsigned long flags; + bool valid; + + spin_lock_irqsave(&dcp->metadata.lock, flags); + valid = dcp->desktop->panel_metadata_valid; + if (valid) { + metadata = dcp->desktop->panel_metadata; + *generation = dcp->desktop->panel_metadata_generation; + } + spin_unlock_irqrestore(&dcp->metadata.lock, flags); + if (!valid) + return ERR_PTR(-ENODATA); + edid = dcp_panel_edid(&metadata, dcp->panel.width_mm, dcp->panel.height_mm); + if (!IS_ERR(edid) && !dcp_panel_metadata_current(dcp, *generation)) { + drm_edid_free(edid); + return ERR_PTR(-ESTALE); + } + return edid; +} + void dcp_set_dimensions(struct apple_dcp *dcp, u64 generation) { struct apple_connector *apple_connector = READ_ONCE(dcp->connector); diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index 6cc67676fb9db..2a48c8b8df962 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -1944,8 +1944,11 @@ int dcp_get_modes(struct drm_connector *connector) } } else { u64 generation = 0; + bool panel = dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && + dcp_has_panel(dcp); const struct drm_edid *derived = nr_modes ? - dcp_firmware_edid(dcp, &generation) : ERR_PTR(-ENODATA); + (panel ? dcp_panel_edid_snapshot(dcp, &generation) : + dcp_firmware_edid(dcp, &generation)) : ERR_PTR(-ENODATA); if (!IS_ERR(derived)) { bool log = !apple_connector->drm_edid_firmware_derived || @@ -1956,7 +1959,8 @@ int dcp_get_modes(struct drm_connector *connector) apple_connector->drm_edid_generation = generation; update = drm_edid_connector_update(connector, derived); drm_edid_free(derived); - if (update || !dcp_firmware_metadata_current(dcp, generation)) { + if (update || !(panel ? dcp_panel_metadata_current(dcp, generation) : + dcp_firmware_metadata_current(dcp, generation))) { drm_edid_connector_update(connector, NULL); apple_connector->drm_edid_firmware_derived = false; } else if (log) { diff --git a/drivers/gpu/drm/apple/iomfb_template.c b/drivers/gpu/drm/apple/iomfb_template.c index abb1eb8bd6dac..0a735158ceda8 100644 --- a/drivers/gpu/drm/apple/iomfb_template.c +++ b/drivers/gpu/drm/apple/iomfb_template.c @@ -1320,6 +1320,12 @@ static bool dcpep_process_chunks(struct apple_dcp *dcp, ret = parse_display_metadata(&metadata_ctx, &metadata); dcp_firmware_metadata_attributes(dcp, ret ? NULL : &metadata); + } else if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && + dcp_has_panel(dcp)) { + struct dcp_display_metadata metadata; + + ret = parse_panel_metadata(&metadata_ctx, &metadata); + dcp_panel_metadata_replace(dcp, ret ? NULL : &metadata); } } diff --git a/drivers/gpu/drm/apple/parser-test.c b/drivers/gpu/drm/apple/parser-test.c index 05648a14cf8e6..9a7426d1f7016 100644 --- a/drivers/gpu/drm/apple/parser-test.c +++ b/drivers/gpu/drm/apple/parser-test.c @@ -3,6 +3,7 @@ #include #include #include +#include #include #include "dcp.h" @@ -1308,7 +1309,41 @@ static void parser_route_prepare_scope(struct kunit *test) mutex_destroy(&dcp->modes_lock); } +static void parser_panel_identity(struct kunit *test) +{ + struct parser_blob *blob = kunit_kzalloc(test, sizeof(*blob), GFP_KERNEL); + struct dcp_display_metadata metadata; + struct dcp_parse_ctx ctx; + const struct drm_edid *edid; + const struct edid *raw; + + KUNIT_ASSERT_NOT_NULL(test, blob); + put_unaligned_le32(0xd3, blob->data); + blob->size = 4; + blob_tag(blob, 1, 1); + blob_key(blob, "ProductAttributes"); + blob_tag(blob, 1, 4); + blob_key(blob, "ManufacturerID"); + blob_key(blob, "TST"); + blob_key(blob, "ProductName"); + blob_key(blob, "Synthetic Internal Panel"); + blob_int(blob, "ProductID", 42); + blob_int(blob, "SerialNumber", 0x12345678); + KUNIT_ASSERT_EQ(test, parse(blob->data, blob->size, &ctx), 0); + KUNIT_ASSERT_EQ(test, parse_panel_metadata(&ctx, &metadata), 0); + KUNIT_EXPECT_STREQ(test, metadata.name, "Synthetic Int"); + edid = dcp_panel_edid(&metadata, 345, 223); + KUNIT_ASSERT_FALSE(test, IS_ERR(edid)); + KUNIT_EXPECT_TRUE(test, drm_edid_valid(edid)); + raw = drm_edid_raw(edid); + KUNIT_EXPECT_EQ(test, le16_to_cpu(raw->product_id.product_code), 42); + KUNIT_EXPECT_EQ(test, le32_to_cpu(raw->product_id.serial_number), 0x12345678U); + KUNIT_EXPECT_EQ(test, raw->gamma, 0xff); + drm_edid_free(edid); +} + static struct kunit_case parser_cases[] = { + KUNIT_CASE(parser_panel_identity), KUNIT_CASE(parser_score_presence), KUNIT_CASE(parser_legacy_ties), KUNIT_CASE(parser_color_presence), diff --git a/drivers/gpu/drm/apple/parser.c b/drivers/gpu/drm/apple/parser.c index 25a2562f34807..1de2fa263a865 100644 --- a/drivers/gpu/drm/apple/parser.c +++ b/drivers/gpu/drm/apple/parser.c @@ -1319,7 +1319,33 @@ static int parse_metadata_chromaticity(struct dcp_parse_ctx *handle, return seen == 7 ? 0 : -EINVAL; } -static int parse_metadata_product(struct dcp_parse_ctx *handle, struct dcp_display_metadata *out) +/* Internal panel labels can be longer than an EDID descriptor. */ +static int parse_panel_label(struct dcp_parse_ctx *handle, char out[14]) +{ + const struct dcp_parse_tag *tag = parse_tag_of_type(handle, DCP_TYPE_STRING); + const u8 *value; + unsigned int length; + + if (IS_ERR(tag)) + return PTR_ERR(tag); + if (!tag->size || tag->size > 255) + return -EINVAL; + value = parse_bytes(handle, tag->size); + if (IS_ERR(value)) + return PTR_ERR(value); + for (unsigned int i = 0; i < tag->size; i++) + if (value[i] < 0x20 || value[i] > 0x7e) + return -EINVAL; + length = tag->size; + if (length > 13) + length = 13; + memcpy(out, value, length); + out[length] = 0; + return 0; +} + +static int parse_metadata_product(struct dcp_parse_ctx *handle, + struct dcp_display_metadata *out, bool panel) { struct iterator it; unsigned int seen = 0; @@ -1344,7 +1370,8 @@ static int parse_metadata_product(struct dcp_parse_ctx *handle, struct dcp_displ if (seen & 2) return -EINVAL; seen |= 2; - ret = parse_metadata_string(handle, out->name, sizeof(out->name)); + ret = panel ? parse_panel_label(handle, out->name) : + parse_metadata_string(handle, out->name, sizeof(out->name)); } else if (!strcmp(key, "ProductID")) { if (seen & 4) return -EINVAL; @@ -1369,8 +1396,9 @@ static int parse_metadata_product(struct dcp_parse_ctx *handle, struct dcp_displ if (seen & 128) return -EINVAL; seen |= 128; - ret = parse_metadata_string(handle, out->serial_text, - sizeof(out->serial_text)); + ret = panel ? parse_panel_label(handle, out->serial_text) : + parse_metadata_string(handle, out->serial_text, + sizeof(out->serial_text)); } else if (!strcmp(key, "LegacyManufacturerID")) { if (seen & 32) return -EINVAL; @@ -1382,7 +1410,8 @@ static int parse_metadata_product(struct dcp_parse_ctx *handle, struct dcp_displ if (ret) return ret; } - if ((seen & 31) != 31 || strlen(out->manufacturer) != 3) + if ((seen & (panel ? 7 : 31)) != (panel ? 7 : 31) || + strlen(out->manufacturer) != 3) return -EINVAL; for (unsigned int i = 0; i < 3; i++) if (out->manufacturer[i] < 'A' || out->manufacturer[i] > 'Z') @@ -1433,7 +1462,7 @@ int parse_display_metadata(struct dcp_parse_ctx *handle, struct dcp_display_meta if (seen & 16) return -EINVAL; seen |= 16; - ret = parse_metadata_product(handle, &parsed); + ret = parse_metadata_product(handle, &parsed, false); } else { ret = skip(handle); } @@ -1446,6 +1475,39 @@ int parse_display_metadata(struct dcp_parse_ctx *handle, struct dcp_display_meta return 0; } +/* Identity does not depend on optional colorimetry or EDID-compatible timing. */ +int parse_panel_metadata(struct dcp_parse_ctx *handle, struct dcp_display_metadata *out) +{ + struct dcp_display_metadata parsed = {}; + struct iterator it; + bool product = false; + int ret = iterator_begin(handle, &it, true); + + if (ret) + return ret; + for (; it.idx < it.len; it.idx++) { + char key[64]; + + ret = parse_metadata_string(handle, key, sizeof(key)); + if (ret) + return ret; + if (!strcmp(key, "ProductAttributes")) { + if (product) + return -EINVAL; + product = true; + ret = parse_metadata_product(handle, &parsed, true); + } else { + ret = skip(handle); + } + if (ret) + return ret; + } + if (!product) + return -ENODATA; + *out = parsed; + return 0; +} + int parse_epic_service_init(struct dcp_parse_ctx *handle, const char **name, const char **class, s64 *unit, bool h17p_keys) { diff --git a/drivers/gpu/drm/apple/parser.h b/drivers/gpu/drm/apple/parser.h index 135124680ec6a..c88f4dd21b855 100644 --- a/drivers/gpu/drm/apple/parser.h +++ b/drivers/gpu/drm/apple/parser.h @@ -196,6 +196,7 @@ int replace_modes(struct dcp_parse_ctx *handle, struct dcp_display_mode **modes, unsigned int *count, int width_mm, int height_mm, unsigned int notch_height, bool internal); +int parse_panel_metadata(struct dcp_parse_ctx *handle, struct dcp_display_metadata *out); int parse_display_attributes(struct dcp_parse_ctx *handle, int *width_mm, int *height_mm, bool *backlight_control); int parse_epic_service_init(struct dcp_parse_ctx *handle, const char **name, diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py index 1ccc513a96f35..28507cbba031d 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py @@ -357,3 +357,67 @@ def modeset_unknown(root): assert(!d.crashed && !d.command.depth && !d.channel.depth); ''' return unit.replace(' return 0;\n}', tests + ' return 0;\n}') + + +def panel_identity(root): + import struct + import rate + f = rate.parser_fixture(root) + attributes = {'MaxHorizontalImageSize': 0, 'MaxVerticalImageSize': 0, + 'ProductAttributes': {'ManufacturerID': 'TST', 'ProductID': 42, + 'ProductName': 'Synthetic Internal Panel', + 'SerialNumber': 0x12345678}} + raw = bytearray(struct.pack('has_panel;} +static void dcp_queue_hotplug(void *con) {assert(con);notifications++;} +''' + helpers + '\nstatic const u8 attributes[]={'+','.join(map(str,raw))+'};\n' + r''' +int main(void) +{ + struct dcp_parse_ctx ctx; + struct dcp_display_metadata metadata; + assert(!parse(attributes,sizeof(attributes),&ctx)); + assert(!parse_panel_metadata(&ctx,&metadata)); + assert(!strcmp(metadata.manufacturer,"TST") && metadata.product==42 && metadata.serial==0x12345678); + assert(!strcmp(metadata.name,"Synthetic Int")); + /* No color fields, date or EDID-representable timing is required. */ + struct apple_dcp d={.fw_compat=5,.has_panel=true,.panel={345,223},.connector=&d}; + __typeof__(*d.desktop) desktop={0};d.desktop=&desktop; + u64 generation=99; + assert(IS_ERR(dcp_panel_edid_snapshot(&d,&generation))); + dcp_panel_metadata_replace(&d,&metadata); + assert(notifications==1 && desktop.panel_metadata_valid); + const struct drm_edid *edid=dcp_panel_edid_snapshot(&d,&generation); + assert(!IS_ERR(edid) && drm_edid_valid(edid) && dcp_panel_metadata_current(&d,generation)); + const u8 *bytes=(const void *)&edid->raw; + assert(bytes[8]==0x52 && bytes[9]==0x74 && bytes[10]==42 && !bytes[11]); + assert(bytes[12]==0x78 && bytes[13]==0x56 && bytes[14]==0x34 && bytes[15]==0x12); + assert(bytes[18]==1 && bytes[19]==3 && bytes[21]==35 && bytes[22]==22 && bytes[23]==0xff); + assert(bytes[54+3]==EDID_DETAIL_MONITOR_NAME && bytes[72+3]==EDID_DETAIL_MONITOR_SERIAL); + assert(!memcmp(bytes+72+5,"12345678\n",9)); + drm_edid_free(edid); + dcp_panel_metadata_replace(&d,NULL); + assert(!dcp_panel_metadata_current(&d,generation)); + assert(IS_ERR(dcp_panel_edid_snapshot(&d,&generation))); + /* Another firmware/profile cannot publish a desktop panel identity. */ + d.fw_compat=2;dcp_panel_metadata_replace(&d,&metadata); + assert(!desktop.panel_metadata_valid); + metadata.manufacturer[0]='?';assert(IS_ERR(dcp_panel_edid(&metadata,345,223))); + return 0; +} +''' diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py b/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py index 9176515c38d51..db5930b71215b 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py @@ -638,6 +638,10 @@ def publication(root): } static const struct drm_edid *dcp_firmware_edid(struct apple_dcp *d,u64 *generation) { (void)d; (void)generation; return ERR_PTR(-ENODATA); } static bool dcp_firmware_metadata_current(struct apple_dcp *d,u64 generation) { (void)d; (void)generation; return false; } +static const struct drm_edid *dcp_panel_edid_snapshot(struct apple_dcp *d,u64 *generation) { (void)d;(void)generation;assert(!"panel identity in external fixture");return ERR_PTR(-ENODATA); } +static bool dcp_panel_metadata_current(struct apple_dcp *d,u64 generation) { (void)d;(void)generation;assert(!"panel identity in external fixture");return false; } +static void dcp_panel_metadata_replace(struct apple_dcp *d,const struct dcp_display_metadata *m) { (void)d;(void)m;assert(!"panel identity in external receipt"); } + static struct drm_display_mode *drm_mode_duplicate(struct drm_device *dev,const struct drm_display_mode *m) { assert(dev->mode_config.mutex);struct drm_display_mode *copy=malloc(sizeof(*copy));assert(copy);*copy=*m; if(invalidate_during_duplicate) { invalidate_during_duplicate=false;dcp_180hz_invalidate(probing); } diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/run.py b/tools/testing/selftests/drivers/gpu/apple-dcp/run.py index 1adcfe62035f5..73eef60687984 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/run.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/run.py @@ -60,6 +60,7 @@ def main(): ('display', 'panel-brightness-commit', display.brightness_commit, ((),), ()), ('display', 'panel-brightness-takeover', display.takeover, ((),), ()), ('display', 'panel-mode-catalog', display.modes, ((),), ()), + ('display', 'desktop-panel-identity', desktop.panel_identity, ((),), ()), ('display', 'panel-refresh-range', display.refresh, ((),), ()), ('display', 'panel-adaptive-fallback', display.adaptive, ((),), ()), ('display', 'panel-adaptive-watchdog', display.watchdog, ((),), ()), From 0c76139a04d6f5a374956c156d626e773f4fa992 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 09:04:16 +1100 Subject: [PATCH 109/388] drm/apple: preserve serialized powerlog reports on compact EPIC The compact report dispatcher removed an eight-byte command descriptor from every standard-service report. A bare serialized dictionary loses its header there, so systemep rejects it before it can decode the mNits event or establish a takeover hint. Recognize the bounded serialization header and dictionary tag before removing a descriptor. Keep the existing descriptor-wrapped report path. Do not scan arbitrary offsets or manufacture a brightness sample. The host test sends both report forms through the production compact receiver, systemep handler and dictionary parser, and verifies the decoded hint. It also reproduces the old header-loss failure and rejects truncation. KUnit covers the exact header boundary. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/afk-parser-test.c | 15 ++++ drivers/gpu/drm/apple/afk.c | 12 ++- .../drivers/gpu/apple-dcp/desktop.py | 81 +++++++++++++++++++ .../selftests/drivers/gpu/apple-dcp/run.py | 1 + 4 files changed, 107 insertions(+), 2 deletions(-) diff --git a/drivers/gpu/drm/apple/afk-parser-test.c b/drivers/gpu/drm/apple/afk-parser-test.c index fd8c5cb0c26e5..8b93e67fd3d5b 100644 --- a/drivers/gpu/drm/apple/afk-parser-test.c +++ b/drivers/gpu/drm/apple/afk-parser-test.c @@ -89,7 +89,22 @@ static void afk_reply_index_test(struct kunit *test) KUNIT_EXPECT_EQ(test, pending->retcode, 7U); } +static void afk_serialized_report_test(struct kunit *test) +{ + u8 dictionary[8] = {}; + unsigned int length; + + put_unaligned_le32(0xd3, dictionary); + put_unaligned_le32(1U << 24, dictionary + 4); + for (length = 0; length < sizeof(dictionary); length++) + KUNIT_EXPECT_FALSE(test, afk_compact_report_dictionary(dictionary, length)); + KUNIT_EXPECT_TRUE(test, afk_compact_report_dictionary(dictionary, sizeof(dictionary))); + put_unaligned_le32(0, dictionary); + KUNIT_EXPECT_FALSE(test, afk_compact_report_dictionary(dictionary, sizeof(dictionary))); +} + static struct kunit_case afk_parser_cases[] = { + KUNIT_CASE(afk_serialized_report_test), KUNIT_CASE(afk_compact_header_test), KUNIT_CASE(afk_short_epic_test), KUNIT_CASE(afk_short_reply_test), diff --git a/drivers/gpu/drm/apple/afk.c b/drivers/gpu/drm/apple/afk.c index 248ca42640b40..688b2d2f5c622 100644 --- a/drivers/gpu/drm/apple/afk.c +++ b/drivers/gpu/drm/apple/afk.c @@ -1024,6 +1024,13 @@ static void afk_recv_handle_compact_call(struct apple_dcp_afkep *ep, queue_work(ep->tx_wq, &r->work); } +/* A serialized report starts at its serialization header, without a command descriptor. */ +static bool afk_compact_report_dictionary(const void *payload, size_t size) +{ + return size >= 2 * sizeof(u32) && get_unaligned_le32(payload) == 0xd3 && + ((get_unaligned_le32(payload + sizeof(u32)) >> 24) & 0x1f) == 1; +} + static void afk_recv_compact_message(struct apple_dcp_afkep *ep, u16 intf_id, const void *body, size_t size) { @@ -1046,9 +1053,10 @@ static void afk_recv_compact_message(struct apple_dcp_afkep *ep, u16 intf_id, return; } if (sub->category == 0) { - /* A standard-service report carries a descriptor first. */ + /* Preserve bare dictionaries; command-style reports have a descriptor. */ if (sub->type == EPIC_SUBTYPE_STD_SERVICE && - payload_size >= sizeof(struct epic_compact_desc)) { + payload_size >= sizeof(struct epic_compact_desc) && + !afk_compact_report_dictionary(payload, payload_size)) { payload += sizeof(struct epic_compact_desc); payload_size -= sizeof(struct epic_compact_desc); } diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py index 28507cbba031d..bde59e98e0270 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py @@ -421,3 +421,84 @@ def panel_identity(root): return 0; } ''' + + +def powerlog_framing(root): + import rate + import struct + f = rate.parser_fixture(root) + # Use only the existing integer/string parser; no captured device data. + raw = bytearray(struct.pack('service->channel==id);return e->service;} +static struct apple_epic_service *afk_service_get(struct apple_epic_service *s) {s->users++;return s;} +static void afk_service_put(struct apple_epic_service *s) {assert(s->users);s->users--;} +static void afk_recv_handle_compact_init(struct apple_dcp_afkep *e,u16 id,const void *p,size_t n) +{(void)e;(void)id;(void)p;(void)n;assert(false);} +static void afk_recv_handle_compact_call(struct apple_dcp_afkep *e,u16 id,const void *p,size_t n) +{(void)e;(void)id;(void)p;(void)n;assert(false);} +static void afk_recv_handle_compact_reply(struct apple_dcp_afkep *e,u16 id,u8 t,const void *p,size_t n) +{(void)e;(void)id;(void)t;(void)p;(void)n;assert(false);} +''' + functions = '\n'.join(block(source, name) for source, name in ( + (system, 'static int powerlog_report('), + (afk, 'static bool afk_compact_report_dictionary('), + (afk, 'static void afk_recv_compact_message('))) + return prefix + '\n' + functions + '\nstatic const u8 report[]={'+','.join(map(str,raw))+'};\n' + r''' +int main(void) +{ + struct apple_dcp d={.fw_compat=5}; + struct apple_dcp_afkep ep={.dcp=&d,.endpoint=0x20}; + struct apple_epic_service_ops ops={.report=powerlog_report}; + struct apple_epic_service service={.ep=&ep,.ops=&ops,.channel=3};ep.service=&service; + u8 wire[16+8+sizeof(report)]={0}; + struct epic_compact_sub_hdr *sub=(void *)(wire+8);sub->category=0;sub->type=0xc0; + memcpy(wire+16,report,sizeof(report)); + afk_recv_compact_message(&ep,3,wire,16+sizeof(report)); + assert(hints==1 && hint==123000 && !service.users); + /* Descriptor-wrapped reports keep their existing transport behavior. */ + memset(wire+16,0,8);memcpy(wire+24,report,sizeof(report)); + afk_recv_compact_message(&ep,3,wire,sizeof(wire)); + assert(hints==2 && hint==123000 && !service.users); + /* The old unconditional descriptor removal loses the serialization header. */ + struct dcp_parse_ctx ctx;assert(parse(report+8,sizeof(report)-8,&ctx)==-EINVAL); + /* Truncated data never creates a takeover hint. */ + memcpy(wire+16,report,8); + afk_recv_compact_message(&ep,3,wire,23); + assert(hints==2 && !service.users); + return 0; +} +''' diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/run.py b/tools/testing/selftests/drivers/gpu/apple-dcp/run.py index 73eef60687984..bad70b2fd8f6f 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/run.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/run.py @@ -61,6 +61,7 @@ def main(): ('display', 'panel-brightness-takeover', display.takeover, ((),), ()), ('display', 'panel-mode-catalog', display.modes, ((),), ()), ('display', 'desktop-panel-identity', desktop.panel_identity, ((),), ()), + ('display', 'desktop-powerlog-framing', desktop.powerlog_framing, ((),), ()), ('display', 'panel-refresh-range', display.refresh, ((),), ()), ('display', 'panel-adaptive-fallback', display.adaptive, ((),), ()), ('display', 'panel-adaptive-watchdog', display.watchdog, ((),), ()), From e29ee9a18eb04daee1254d256ecfb508d52ae6c1 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 09:07:04 +1100 Subject: [PATCH 110/388] drm/apple: keep firmware brightness reports separate from user targets Use the first bounded report only to establish an unknown takeover level. Once Linux has a level or a user target, retain firmware observations separately from the target, fade position and accepted present. A delayed or non-monotonic report must not replace a request, mark it dirty, restart a fade or generate a new write. Continue decoding reports after backlight registration so they can confirm output without driving policy. Preserve the backlight target under its existing state lock and synchronize only the initial takeover hint with the registered device. Add a host regression with rapid increasing and decreasing key repeats, interleaved old and new reports, both completion/reply orders and late reports after settling. Assert monotonic output and convergence to the last user target. Add KUnit for report ownership through a fade and retarget. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/dcp_backlight.c | 22 ++++++- drivers/gpu/drm/apple/dcp_backlight.h | 21 ++++++ drivers/gpu/drm/apple/dcp_backlight_test.c | 34 ++++++++++ drivers/gpu/drm/apple/systemep.c | 4 -- .../drivers/gpu/apple-dcp/desktop.py | 64 ++++++++++++++++++- .../drivers/gpu/apple-dcp/display.py | 1 + .../selftests/drivers/gpu/apple-dcp/run.py | 1 + 7 files changed, 139 insertions(+), 8 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp_backlight.c b/drivers/gpu/drm/apple/dcp_backlight.c index 381bdedee65b1..7d884e062f363 100644 --- a/drivers/gpu/drm/apple/dcp_backlight.c +++ b/drivers/gpu/drm/apple/dcp_backlight.c @@ -135,14 +135,30 @@ int dcp_backlight_takeover(struct apple_dcp *dcp, u32 millinits) u32 maximum, nits; int ret; - if (dcp_backlight_active(dcp)) - return 0; /* H17P takes nits up to the panel ceiling, not the older DAC table's. */ maximum = dcp->brightness.maximum; ret = dcp_bl_takeover_nits(maximum, millinits, &nits); if (ret) return ret; + if (dcp_backlight_active(dcp) || + (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && + READ_ONCE(dcp->backlight.state.ready))) { + unsigned long flags; + bool seeded; + + spin_lock_irqsave(&dcp->backlight.lock, flags); + seeded = dcp_bl_report(&dcp->backlight.state, nits); + if (seeded && dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) { + dcp->desktop->inherited_brightness = nits; + dcp->desktop->inherited_brightness_valid = true; + } + spin_unlock_irqrestore(&dcp->backlight.lock, flags); + if (seeded) + dcp_backlight_sync_hint(dcp); + return 0; + } + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) { unsigned long flags; bool seeded = false; @@ -154,7 +170,7 @@ int dcp_backlight_takeover(struct apple_dcp *dcp, u32 millinits) dcp->desktop->inherited_brightness = nits; dcp->desktop->inherited_brightness_valid = true; if (dcp->backlight.state.ready) - seeded = dcp_bl_seed(&dcp->backlight.state, nits); + seeded = dcp_bl_report(&dcp->backlight.state, nits); } spin_unlock_irqrestore(&dcp->backlight.lock, flags); if (seeded) diff --git a/drivers/gpu/drm/apple/dcp_backlight.h b/drivers/gpu/drm/apple/dcp_backlight.h index 704e985f49a78..ad8ca6d5623fc 100644 --- a/drivers/gpu/drm/apple/dcp_backlight.h +++ b/drivers/gpu/drm/apple/dcp_backlight.h @@ -22,6 +22,8 @@ struct dcp_backlight_state { u32 fade_from; u32 actual; u32 sent_nits; + /* Firmware observations confirm output; they never become user requests. */ + u32 reported; u64 sequence; u8 retries; bool ready; @@ -43,6 +45,7 @@ struct dcp_backlight_state { * without a loader report is not known until a present carries a level. */ bool level_known; + bool report_valid; }; struct dcp_backlight_present { @@ -88,8 +91,10 @@ static inline int dcp_bl_init(struct dcp_backlight_state *state, u32 maximum, .target = nits, .level = nits, .actual = nits, + .reported = nits, .ready = true, .level_known = inherited_valid, + .report_valid = inherited_valid, }; return 0; } @@ -109,6 +114,22 @@ static inline bool dcp_bl_seed(struct dcp_backlight_state *state, u32 nits) return true; } +/* + * A report can establish takeover once. After Linux has a level or target, + * keep the observation separate from target, fade progress and completion. + * In particular, delayed reports cannot restart a fade or request a resend. + */ +static inline bool dcp_bl_report(struct dcp_backlight_state *state, u32 nits) +{ + if (!state->ready || nits > state->maximum) + return false; + state->reported = nits; + state->report_valid = true; + if (state->controlled || state->level_known) + return false; + return dcp_bl_seed(state, nits); +} + /* Report a usable target until a hint or completed present gives the level. */ static inline int dcp_bl_actual(const struct dcp_backlight_state *state) { diff --git a/drivers/gpu/drm/apple/dcp_backlight_test.c b/drivers/gpu/drm/apple/dcp_backlight_test.c index 57029cecb2f23..c8a4d31bde2af 100644 --- a/drivers/gpu/drm/apple/dcp_backlight_test.c +++ b/drivers/gpu/drm/apple/dcp_backlight_test.c @@ -600,7 +600,41 @@ static void backlight_desktop_swap_test(struct kunit *test) KUNIT_EXPECT_EQ(test, state.level, 0U); } +static void backlight_reports_keep_target_test(struct kunit *test) +{ + struct dcp_backlight_state state; + struct dcp_backlight_present present; + + KUNIT_ASSERT_EQ(test, dcp_bl_init(&state, 600, false, 0, true, 300), 0); + state.fade = true; + KUNIT_EXPECT_TRUE(test, dcp_bl_report(&state, 100)); + KUNIT_EXPECT_EQ(test, state.actual, 100U); + KUNIT_ASSERT_EQ(test, dcp_bl_request(&state, 600, false, false), 0); + KUNIT_EXPECT_TRUE(test, dcp_bl_fade_step(&state, 125, 250)); + KUNIT_EXPECT_FALSE(test, dcp_bl_report(&state, 50)); + KUNIT_EXPECT_EQ(test, state.target, 600U); + KUNIT_EXPECT_EQ(test, state.level, 350U); + KUNIT_EXPECT_EQ(test, state.actual, 100U); + KUNIT_ASSERT_EQ(test, dcp_bl_prepare(&state, true, &present), 0); + KUNIT_EXPECT_FALSE(test, dcp_bl_report(&state, 600)); + KUNIT_EXPECT_TRUE(test, dcp_bl_complete(&state, present.sequence, true)); + KUNIT_EXPECT_EQ(test, state.actual, 350U); + KUNIT_ASSERT_EQ(test, dcp_bl_request(&state, 500, false, false), 0); + KUNIT_EXPECT_FALSE(test, dcp_bl_report(&state, 100)); + KUNIT_EXPECT_TRUE(test, dcp_bl_fade_step(&state, 125, 250)); + KUNIT_EXPECT_EQ(test, state.level, 425U); + KUNIT_EXPECT_FALSE(test, dcp_bl_fade_step(&state, 250, 250)); + KUNIT_ASSERT_EQ(test, dcp_bl_prepare(&state, true, &present), 0); + KUNIT_EXPECT_TRUE(test, dcp_bl_complete(&state, present.sequence, true)); + KUNIT_EXPECT_FALSE(test, dcp_bl_report(&state, 200)); + KUNIT_EXPECT_EQ(test, state.reported, 200U); + KUNIT_EXPECT_EQ(test, state.target, 500U); + KUNIT_EXPECT_EQ(test, state.actual, 500U); + KUNIT_EXPECT_FALSE(test, state.dirty); +} + static struct kunit_case backlight_cases[] = { + KUNIT_CASE(backlight_reports_keep_target_test), KUNIT_CASE(backlight_millinits_takeover_test), KUNIT_CASE(backlight_takeover_test), KUNIT_CASE(backlight_range_test), diff --git a/drivers/gpu/drm/apple/systemep.c b/drivers/gpu/drm/apple/systemep.c index b8576aec5c6f5..31e4060679bb2 100644 --- a/drivers/gpu/drm/apple/systemep.c +++ b/drivers/gpu/drm/apple/systemep.c @@ -75,10 +75,6 @@ static int powerlog_report(struct apple_epic_service *service, enum epic_subtype struct apple_dcp *dcp = service->ep->dcp; int ret; - /* Later reports must not overwrite a latched hint or Linux's target. */ - if (dcp_backlight_active(dcp)) - return 0; - dev_dbg(dcp->dev, "systemep[ch:%u]: report type:%02x len:%zu\n", service->channel, type, data_size); diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py index bde59e98e0270..ac6798f250e1e 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py @@ -458,7 +458,7 @@ def powerlog_framing(root): struct apple_epic_service_ops {int (*report)(struct apple_epic_service *,enum epic_subtype,const void *,size_t);}; struct apple_epic_service {struct apple_dcp_afkep *ep;struct apple_epic_service_ops *ops;unsigned channel,users;}; static unsigned hints;static u32 hint; -static bool dcp_backlight_active(struct apple_dcp *d) {(void)d;return false;} +static bool __attribute__((unused)) dcp_backlight_active(struct apple_dcp *d) {(void)d;return false;} static bool dcp_has_panel(struct apple_dcp *d) {(void)d;return true;} static int dcp_backlight_takeover(struct apple_dcp *d,u32 mnits) {(void)d;hints++;hint=mnits;return 0;} static u32 get_unaligned_le32(const void *p) {u32 v;memcpy(&v,p,4);return v;} @@ -502,3 +502,65 @@ def powerlog_framing(root): return 0; } ''' + + +def keys_and_reports(root): + import display + prefix = display.backlight(root).split('static void completed(')[0] + prefix = prefix.replace('static inline ', 'static inline __attribute__((unused)) ') + return prefix + r''' +static void report(struct dcp_backlight_state *s,u32 nits) +{ + struct dcp_backlight_state before=*s; + assert(!dcp_bl_report(s,nits)); + assert(s->target==before.target && s->level==before.level && s->actual==before.actual); + assert(s->fade_from==before.fade_from && s->fading==before.fading); + assert(s->dirty==before.dirty && s->in_flight==before.in_flight && s->sequence==before.sequence); + assert(s->report_valid && s->reported==nits); +} +static void step(struct dcp_backlight_state *s,u32 elapsed,u32 *id,u32 *previous,bool up) +{ + struct dcp_backlight_swap p; + dcp_bl_fade_step(s,elapsed,250); + report(s,(*id & 1)?50:550); + if(!s->dirty)return; + assert(!dcp_bl_swap_prepare(s,&p,(*id)++,true)); + report(s,(*id & 1)?0:600); + if(*id & 1) { + assert(!dcp_bl_swap_finish(s,&p,p.id,true,true)); + report(s,250); + assert(dcp_bl_swap_finish(s,&p,p.id,false,true)); + } else { + assert(!dcp_bl_swap_finish(s,&p,p.id,false,true)); + report(s,160); + assert(dcp_bl_swap_finish(s,&p,p.id,true,true)); + } + assert(up?s->actual>=*previous:s->actual<=*previous); + *previous=s->actual; +} +int main(void) +{ + for(unsigned direction=0;direction<2;direction++) { + bool up=!direction;u32 initial=up?100:600,previous=initial,id=1; + struct dcp_backlight_state s; + assert(!dcp_bl_init(&s,600,false,0,true,300)); + s.fade=true;s.preserve_inherited=true; + assert(dcp_bl_report(&s,initial) && s.actual==initial && !s.dirty); + report(&s,up?550:50); /* Only the first hint can establish takeover. */ + for(unsigned key=1;key<=10;key++) { + u32 target=up?100+key*50:600-key*50; + assert(!dcp_bl_request(&s,target,false,false)); + report(&s,up?50:550); + /* Key repeat retargets before any fade can finish. */ + step(&s,16,&id,&previous,up);step(&s,32,&id,&previous,up); + assert(s.target==target); + } + u32 last=up?600:100; + for(u32 time=48;time<=256;time+=16)step(&s,time,&id,&previous,up); + assert(s.target==last && s.actual==last && s.level==last && !s.fading && !s.dirty); + for(unsigned late=0;late<64;late++)report(&s,late&1?0:600); + assert(s.target==last && s.actual==last && !s.dirty); + } + return 0; +} +''' diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/display.py b/tools/testing/selftests/drivers/gpu/apple-dcp/display.py index e64e4f9a16473..5ee71a0b20993 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/display.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/display.py @@ -11,6 +11,7 @@ def backlight(root): header = (root / 'drivers/gpu/drm/apple/dcp_backlight.h').read_text() header = '\n'.join(line for line in header.splitlines() if not line.startswith('#include')) + header = header.replace('static inline ', 'static inline __attribute__((unused)) ') return r''' #include #include diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/run.py b/tools/testing/selftests/drivers/gpu/apple-dcp/run.py index bad70b2fd8f6f..c5a2bed8483c2 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/run.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/run.py @@ -62,6 +62,7 @@ def main(): ('display', 'panel-mode-catalog', display.modes, ((),), ()), ('display', 'desktop-panel-identity', desktop.panel_identity, ((),), ()), ('display', 'desktop-powerlog-framing', desktop.powerlog_framing, ((),), ()), + ('display', 'desktop-brightness-keys-reports', desktop.keys_and_reports, ((),), ()), ('display', 'panel-refresh-range', display.refresh, ((),), ()), ('display', 'panel-adaptive-fallback', display.adaptive, ((),), ()), ('display', 'panel-adaptive-watchdog', display.watchdog, ((),), ()), From ad97d44a15a12901c4b71224b6770ae0989e6e8d Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 09:07:53 +1100 Subject: [PATCH 111/388] selftests: apple-dcp: retain the panel EDID for independent decoding Retain the synthetic panel identity EDID with host artifacts so a userspace parser can independently validate its monitor strings. Document panel identity, report framing and brightness ownership alongside the existing ramp and adaptive-sync policy. Assisted-by: LLM Signed-off-by: Ryan Murray --- Documentation/gpu/apple-dcp-t6041-panel.rst | 28 ++++++++++++++++++- drivers/gpu/drm/apple/dcp-display-metadata.c | 2 +- drivers/gpu/drm/apple/dcp-display-metadata.h | 2 +- .../selftests/drivers/gpu/apple-dcp/README | 9 ++++++ .../drivers/gpu/apple-dcp/desktop.py | 9 ++++-- .../selftests/drivers/gpu/apple-dcp/run.py | 2 +- 6 files changed, 45 insertions(+), 7 deletions(-) diff --git a/Documentation/gpu/apple-dcp-t6041-panel.rst b/Documentation/gpu/apple-dcp-t6041-panel.rst index 5bbe4ef6872ca..8512296f29fd8 100644 --- a/Documentation/gpu/apple-dcp-t6041-panel.rst +++ b/Documentation/gpu/apple-dcp-t6041-panel.rst @@ -9,11 +9,15 @@ by the H17P panel. The backlight interface reports the requested target and ``actual_brightness`` follows accepted, completed presents. A registration default does not change the brightness inherited from the boot loader. Until a loader hint or a completed user request supplies a known level, -``actual_brightness`` returns ``ENODATA``. Echoing the unchanged registration +``actual_brightness`` reports the target. Echoing the unchanged registration value also leaves the panel alone while its level is unknown. A request for a different target takes control. Once a hint or completed request establishes the level, all targets are usable. Framebuffer unblank and core suspend notifications also preserve an unknown inherited level. +The first explicit change fades from a known inherited level, or from the +reporting target when no hint is available. Firmware reports are observations, +not requests. After takeover they cannot replace the target, restart a fade or +cause a brightness write. Actual readback follows accepted, completed presents. Ambient-light policy can use the normal backlight interface, just as a slider does. Requests made while blanked or suspended preserve the restore target. @@ -36,3 +40,25 @@ swap watchdog and re-applies the active CRTC at fixed refresh. Either failure latches adaptive sync off until reboot. A transport that stops replying cannot be recovered by issuing another command on its busy channel; it still requires a reboot. Use the previous kernel entry if a panel fails qualification. + +Panel identity +-------------- + +The desktop internal panel has no external EDID service. Its ProductAttributes +supply the monitor identity, independently of optional colorimetry or timing +fields. The driver publishes an identity-only EDID using that product data and +the panel dimensions from the device tree. The firmware timing list remains +authoritative; the EDID adds no synthetic modes or color capabilities. + +Compact powerlog reports may be bare serialized dictionaries or carry a command +descriptor. The receiver preserves the serialization header before handing the +report to systemep. Only the first valid report before Linux control can seed +takeover; later reports confirm output without changing the requested target. + +Desktop mode changes use the D202 and D454 adapters for this firmware's records. +The callback table also includes physical mapping and the empty resize +notification. An unknown callback during a modeset gets a bounded unsupported +reply. Its enclosing command must ACK before the set fails, so a subsequent +mode change can reuse the drained channel. Accepted atomic modeset errors are +reported through the out-fence. A timed-out command retains its channel until +the firmware replies; another set cannot overwrite its stack. diff --git a/drivers/gpu/drm/apple/dcp-display-metadata.c b/drivers/gpu/drm/apple/dcp-display-metadata.c index 5bc60abd95e4b..fd5e0cd6ccdd2 100644 --- a/drivers/gpu/drm/apple/dcp-display-metadata.c +++ b/drivers/gpu/drm/apple/dcp-display-metadata.c @@ -208,7 +208,7 @@ const struct drm_edid *dcp_metadata_edid(const struct dcp_display_metadata *meta * an EDID timing, chromaticities or gamma when the panel omits those fields. */ const struct drm_edid *dcp_panel_edid(const struct dcp_display_metadata *metadata, - u32 width_mm, u32 height_mm) + u32 width_mm, u32 height_mm) { struct edid block = {}; const struct drm_edid *edid; diff --git a/drivers/gpu/drm/apple/dcp-display-metadata.h b/drivers/gpu/drm/apple/dcp-display-metadata.h index b7ec1505d5728..226b778f40138 100644 --- a/drivers/gpu/drm/apple/dcp-display-metadata.h +++ b/drivers/gpu/drm/apple/dcp-display-metadata.h @@ -22,6 +22,6 @@ const struct drm_edid *dcp_metadata_edid(const struct dcp_display_metadata *meta const struct drm_display_mode *mode); const struct drm_edid *dcp_panel_edid(const struct dcp_display_metadata *metadata, - u32 width_mm, u32 height_mm); + u32 width_mm, u32 height_mm); #endif diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/README b/tools/testing/selftests/drivers/gpu/apple-dcp/README index 8a7b42bb7a4fe..dfbdaaab3f367 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/README +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/README @@ -172,3 +172,12 @@ watchdog test checks fixed-refresh recovery, retained-buffer arming and late completion isolation. These tests do not establish which timings a particular panel reports or prove refresh rate, flicker, brightness smoothness or adaptive-sync on hardware. + +Desktop panel brightness regressions also cover identity-only EDID parsing and +publication from ProductAttributes without optional color data. Both bare and +descriptor-wrapped powerlog reports pass through the production compact receiver, +systemep handler and mNits parser. Rapid key repeats with interleaved stale and +new reports must converge monotonically to the last user target in both +directions. Report observations cannot restart a fade or request another write. +The callback cases exercise the desktop D202/D454 wire adapters, physical resource +mapping and release, and unknown-callback failure followed by a successful set. diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py index ac6798f250e1e..0c1e7c5dd7400 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py @@ -359,7 +359,7 @@ def modeset_unknown(root): return unit.replace(' return 0;\n}', tests + ' return 0;\n}') -def panel_identity(root): +def panel_identity(root, output=None): import struct import rate f = rate.parser_fixture(root) @@ -374,7 +374,7 @@ def panel_identity(root): helpers = '\n'.join(block(source, name) for name in ( 'void dcp_panel_metadata_replace(', 'bool dcp_panel_metadata_current(', 'const struct drm_edid *dcp_panel_edid_snapshot(')) - return code + r''' + unit = code + r''' #define DCP_FIRMWARE_V_26_6_DESKTOP 5 #define spin_lock_irqsave(p,f) do {assert(!*(p));*(p)=true;(f)=0;} while(0) #define spin_unlock_irqrestore(p,f) do {assert(*(p));*(p)=false;(void)(f);} while(0) @@ -387,7 +387,7 @@ def panel_identity(root): static bool dcp_has_panel(struct apple_dcp *d) {return d->has_panel;} static void dcp_queue_hotplug(void *con) {assert(con);notifications++;} ''' + helpers + '\nstatic const u8 attributes[]={'+','.join(map(str,raw))+'};\n' + r''' -int main(void) +int main(int argc, char **argv) { struct dcp_parse_ctx ctx; struct dcp_display_metadata metadata; @@ -410,6 +410,7 @@ def panel_identity(root): assert(bytes[18]==1 && bytes[19]==3 && bytes[21]==35 && bytes[22]==22 && bytes[23]==0xff); assert(bytes[54+3]==EDID_DETAIL_MONITOR_NAME && bytes[72+3]==EDID_DETAIL_MONITOR_SERIAL); assert(!memcmp(bytes+72+5,"12345678\n",9)); + if(argc==2) {FILE *file=fopen(argv[1],"wb");assert(file);assert(fwrite(bytes,128,1,file)==1);fclose(file);} drm_edid_free(edid); dcp_panel_metadata_replace(&d,NULL); assert(!dcp_panel_metadata_current(&d,generation)); @@ -422,6 +423,8 @@ def panel_identity(root): } ''' + return (unit, ((str(output / "desktop-panel-identity.edid"),),)) if output else unit + def powerlog_framing(root): import rate diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/run.py b/tools/testing/selftests/drivers/gpu/apple-dcp/run.py index c5a2bed8483c2..1f6e1d845f44f 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/run.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/run.py @@ -60,7 +60,7 @@ def main(): ('display', 'panel-brightness-commit', display.brightness_commit, ((),), ()), ('display', 'panel-brightness-takeover', display.takeover, ((),), ()), ('display', 'panel-mode-catalog', display.modes, ((),), ()), - ('display', 'desktop-panel-identity', desktop.panel_identity, ((),), ()), + ('display', 'desktop-panel-identity', lambda r: desktop.panel_identity(r, output), ((),), ()), ('display', 'desktop-powerlog-framing', desktop.powerlog_framing, ((),), ()), ('display', 'desktop-brightness-keys-reports', desktop.keys_and_reports, ((),), ()), ('display', 'panel-refresh-range', display.refresh, ((),), ()), From 505d49459e2eed947772c36cff4e29a5528938f8 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 09:19:53 +1100 Subject: [PATCH 112/388] drm/apple: include DCP timing and range limits in the panel EDID Provide a representable preferred timing and catalog limits for a conformant panel EDID. Select a base-block timing from the actual DCP catalog, including when the preferred high-refresh clock exceeds the base EDID format. Round only the EDID clock to its 10 kHz units; the DRM mode catalog stays unchanged. Derive vertical limits from the validated adaptive range or fixed mode rates, and horizontal and clock limits from the same retained catalog being published. Use EDID 1.4 frequency offsets above 255 kHz. This also preserves the panel adaptive-sync range when DRM reads the new EDID. Extend the host and KUnit cases with a high-clock 120 Hz mode, a representable 60 Hz mode with a fractional clock, and 24-120 Hz adaptive limits. The retained synthetic EDID passes edid-decode conformance validation. Assisted-by: LLM Signed-off-by: Ryan Murray --- Documentation/gpu/apple-dcp-t6041-panel.rst | 7 +- drivers/gpu/drm/apple/dcp-display-metadata.c | 97 ++++++++++++++++--- drivers/gpu/drm/apple/dcp-display-metadata.h | 2 + drivers/gpu/drm/apple/dcp-internal.h | 4 +- drivers/gpu/drm/apple/dcp.c | 8 +- drivers/gpu/drm/apple/iomfb.c | 2 +- drivers/gpu/drm/apple/parser-test.c | 19 +++- .../selftests/drivers/gpu/apple-dcp/README | 2 +- .../drivers/gpu/apple-dcp/desktop.py | 27 ++++-- .../selftests/drivers/gpu/apple-dcp/rate.py | 11 ++- 10 files changed, 148 insertions(+), 31 deletions(-) diff --git a/Documentation/gpu/apple-dcp-t6041-panel.rst b/Documentation/gpu/apple-dcp-t6041-panel.rst index 8512296f29fd8..a32407077a1c2 100644 --- a/Documentation/gpu/apple-dcp-t6041-panel.rst +++ b/Documentation/gpu/apple-dcp-t6041-panel.rst @@ -46,9 +46,10 @@ Panel identity The desktop internal panel has no external EDID service. Its ProductAttributes supply the monitor identity, independently of optional colorimetry or timing -fields. The driver publishes an identity-only EDID using that product data and -the panel dimensions from the device tree. The firmware timing list remains -authoritative; the EDID adds no synthetic modes or color capabilities. +fields. The driver publishes an EDID using that product data, the panel dimensions +from the device tree, and a representable DCP timing plus catalog limits. +The EDID clock rounds to its required 10 kHz units. DRM still uses the exact +firmware timing list; the EDID adds no synthetic modes or color capabilities. Compact powerlog reports may be bare serialized dictionaries or carry a command descriptor. The receiver preserves the serialization header before handing the diff --git a/drivers/gpu/drm/apple/dcp-display-metadata.c b/drivers/gpu/drm/apple/dcp-display-metadata.c index fd5e0cd6ccdd2..8819f65cb8655 100644 --- a/drivers/gpu/drm/apple/dcp-display-metadata.c +++ b/drivers/gpu/drm/apple/dcp-display-metadata.c @@ -1,11 +1,13 @@ // SPDX-License-Identifier: GPL-2.0-only OR MIT #include #include +#include #include #include #include #include "dcp-display-metadata.h" +#include "parser.h" static int dcp_metadata_timing(struct detailed_timing *timing, const struct drm_display_mode *mode, @@ -203,51 +205,120 @@ const struct drm_edid *dcp_metadata_edid(const struct dcp_display_metadata *meta return edid; } +/* EDID 1.4 range offsets extend each one-byte limit by 255. */ +static int dcp_panel_range_byte(u32 limit, u8 *out, u8 *offsets, u8 flag) +{ + if (!limit || limit > 510) + return -ERANGE; + if (limit > 255) { + limit -= 255; + *offsets |= flag; + } + *out = limit; + return 0; +} + /* - * Internal modes come from TimingElements. Supply identity without inventing - * an EDID timing, chromaticities or gamma when the panel omits those fields. + * Supply one representable DCP timing and its catalog limits for EDID + * conformity. DRM still enumerates TimingElements, never these rounded bytes. */ const struct drm_edid *dcp_panel_edid(const struct dcp_display_metadata *metadata, + const struct dcp_display_mode *modes, unsigned int count, u32 width_mm, u32 height_mm) { struct edid block = {}; + struct dcp_display_metadata dimensions = *metadata; + struct detailed_non_pixel *limits = &block.detailed_timings[3].data.other_data; + struct detailed_data_monitor_range *range = &limits->data.range; const struct drm_edid *edid; char serial[14]; + u32 min_v = U32_MAX, max_v = 0, min_h = U32_MAX, max_h = 0, clock = 0; u16 manufacturer = 0; u8 sum = 0; + bool timing = false, preferred = false; int ret; - if (strnlen(metadata->manufacturer, 4) != 3 || metadata->product > U16_MAX || - !width_mm || width_mm > 2550 || !height_mm || height_mm > 2550) + if (!modes || !count || strnlen(metadata->manufacturer, 4) != 3 || + metadata->product > U16_MAX || !width_mm || width_mm > 2550 || + !height_mm || height_mm > 2550) return ERR_PTR(-EINVAL); for (unsigned int i = 0; i < 3; i++) { if (metadata->manufacturer[i] < 'A' || metadata->manufacturer[i] > 'Z') return ERR_PTR(-EINVAL); manufacturer = manufacturer << 5 | (metadata->manufacturer[i] - '@'); } + dimensions.width_cm = DIV_ROUND_CLOSEST(width_mm, 10); + dimensions.height_cm = DIV_ROUND_CLOSEST(height_mm, 10); + for (unsigned int i = 0; i < count; i++) { + const struct dcp_display_mode *entry = &modes[i]; + struct drm_display_mode mode = entry->mode; + struct detailed_timing candidate = {}; + u32 low, high; + u32 pixels; + + if (mode.clock <= 0 || mode.clock > 2550000 || mode.htotal <= 0 || + mode.htotal > 65535 || mode.vtotal <= 0 || mode.vtotal > 65535) + return ERR_PTR(-EINVAL); + pixels = (u32)mode.htotal * mode.vtotal; + low = div_u64((u64)mode.clock * 1000, pixels); + high = DIV_ROUND_UP_ULL((u64)mode.clock * 1000, pixels); + if (entry->vrr && entry->min_vrr && entry->min_vrr <= entry->max_vrr) { + low = entry->min_vrr >> 16; + high = DIV_ROUND_UP_ULL(entry->max_vrr, 1U << 16); + } + min_v = min(min_v, low); + max_v = max(max_v, high); + min_h = min(min_h, (u32)mode.clock / mode.htotal); + max_h = max(max_h, DIV_ROUND_UP_ULL((u32)mode.clock, mode.htotal)); + clock = max_t(u32, clock, mode.clock); + /* The EDID base timing clock has units of 10 kHz. */ + mode.clock = DIV_ROUND_CLOSEST(mode.clock, 10) * 10; + ret = dcp_metadata_timing(&candidate, &mode, &dimensions); + if (!ret && (!timing || (!preferred && (mode.type & DRM_MODE_TYPE_PREFERRED)))) { + block.detailed_timings[0] = candidate; + timing = true; + preferred = mode.type & DRM_MODE_TYPE_PREFERRED; + } + } + if (!timing || clock > 2550000) + return ERR_PTR(-ERANGE); + memcpy(block.header, "\x00\xff\xff\xff\xff\xff\xff\x00", 8); block.product_id.manufacturer_name = cpu_to_be16(manufacturer); block.product_id.product_code = cpu_to_le16(metadata->product); block.product_id.serial_number = cpu_to_le32(metadata->serial); block.version = 1; - block.revision = 3; + block.revision = 4; block.input = DRM_EDID_INPUT_DIGITAL; - block.width_cm = DIV_ROUND_CLOSEST(width_mm, 10); - block.height_cm = DIV_ROUND_CLOSEST(height_mm, 10); - block.gamma = 0xff; /* Unspecified. */ + block.width_cm = dimensions.width_cm; + block.height_cm = dimensions.height_cm; + block.gamma = 0xff; + block.features = DRM_EDID_FEATURE_PREFERRED_TIMING; memset(block.standard_timings, 1, sizeof(block.standard_timings)); - for (unsigned int i = 0; i < ARRAY_SIZE(block.detailed_timings); i++) - block.detailed_timings[i].data.other_data.type = 0x10 /* Dummy descriptor. */; - ret = dcp_metadata_text(&block.detailed_timings[0], EDID_DETAIL_MONITOR_NAME, + ret = dcp_metadata_text(&block.detailed_timings[1], EDID_DETAIL_MONITOR_NAME, metadata->name); if (ret) return ERR_PTR(ret); - /* A zero numeric serial is a valid unspecified EDID serial, not NULL. */ snprintf(serial, sizeof(serial), "%08x", metadata->serial); - ret = dcp_metadata_text(&block.detailed_timings[1], EDID_DETAIL_MONITOR_SERIAL, + ret = dcp_metadata_text(&block.detailed_timings[2], EDID_DETAIL_MONITOR_SERIAL, metadata->serial_text[0] ? metadata->serial_text : serial); if (ret) return ERR_PTR(ret); + limits->type = EDID_DETAIL_MONITOR_RANGE; + ret = dcp_panel_range_byte(min_v, &range->min_vfreq, &limits->pad2, + DRM_EDID_RANGE_OFFSET_MIN_VFREQ); + ret = ret ?: dcp_panel_range_byte(max_v, &range->max_vfreq, &limits->pad2, + DRM_EDID_RANGE_OFFSET_MAX_VFREQ); + ret = ret ?: dcp_panel_range_byte(min_h, &range->min_hfreq_khz, &limits->pad2, + DRM_EDID_RANGE_OFFSET_MIN_HFREQ); + ret = ret ?: dcp_panel_range_byte(max_h, &range->max_hfreq_khz, &limits->pad2, + DRM_EDID_RANGE_OFFSET_MAX_HFREQ); + if (ret) + return ERR_PTR(ret); + range->pixel_clock_mhz = DIV_ROUND_UP_ULL(clock, 10000); + range->flags = DRM_EDID_RANGE_LIMITS_ONLY_FLAG; + memset(&range->formula, ' ', sizeof(range->formula)); + *((u8 *)&range->formula) = '\n'; for (unsigned int i = 0; i < sizeof(block) - 1; i++) sum += ((const u8 *)&block)[i]; block.checksum = -sum; diff --git a/drivers/gpu/drm/apple/dcp-display-metadata.h b/drivers/gpu/drm/apple/dcp-display-metadata.h index 226b778f40138..d97b090af699d 100644 --- a/drivers/gpu/drm/apple/dcp-display-metadata.h +++ b/drivers/gpu/drm/apple/dcp-display-metadata.h @@ -18,10 +18,12 @@ struct dcp_display_metadata { struct drm_edid; struct drm_display_mode; +struct dcp_display_mode; const struct drm_edid *dcp_metadata_edid(const struct dcp_display_metadata *metadata, const struct drm_display_mode *mode); const struct drm_edid *dcp_panel_edid(const struct dcp_display_metadata *metadata, + const struct dcp_display_mode *modes, unsigned int count, u32 width_mm, u32 height_mm); #endif diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index ef8cc5b093573..f8cbb94b6951e 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -307,7 +307,9 @@ struct dcp_hdmi; bool dcp_desktop_present_complete(struct apple_dcp *dcp, u32 id); void dcp_panel_metadata_replace(struct apple_dcp *dcp, const struct dcp_display_metadata *metadata); -const struct drm_edid *dcp_panel_edid_snapshot(struct apple_dcp *dcp, u64 *generation); +const struct drm_edid *dcp_panel_edid_snapshot(struct apple_dcp *dcp, + const struct dcp_display_mode *modes, + unsigned int count, u64 *generation); bool dcp_panel_metadata_current(struct apple_dcp *dcp, u64 generation); bool dcp_desktop_vrr_supported(struct apple_dcp *dcp); int dcp_desktop_vrr_check(struct apple_dcp *dcp, struct drm_crtc_state *state); diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index dbfb03fa2a040..70c52f2015574 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -806,7 +806,9 @@ bool dcp_panel_metadata_current(struct apple_dcp *dcp, u64 generation) return valid; } -const struct drm_edid *dcp_panel_edid_snapshot(struct apple_dcp *dcp, u64 *generation) +const struct drm_edid *dcp_panel_edid_snapshot(struct apple_dcp *dcp, + const struct dcp_display_mode *modes, + unsigned int count, u64 *generation) { struct dcp_display_metadata metadata; const struct drm_edid *edid; @@ -822,7 +824,9 @@ const struct drm_edid *dcp_panel_edid_snapshot(struct apple_dcp *dcp, u64 *gener spin_unlock_irqrestore(&dcp->metadata.lock, flags); if (!valid) return ERR_PTR(-ENODATA); - edid = dcp_panel_edid(&metadata, dcp->panel.width_mm, dcp->panel.height_mm); + /* The caller supplies the same retained catalog it is publishing. */ + edid = dcp_panel_edid(&metadata, modes, count, + dcp->panel.width_mm, dcp->panel.height_mm); if (!IS_ERR(edid) && !dcp_panel_metadata_current(dcp, *generation)) { drm_edid_free(edid); return ERR_PTR(-ESTALE); diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index 2a48c8b8df962..510ef60e8e592 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -1947,7 +1947,7 @@ int dcp_get_modes(struct drm_connector *connector) bool panel = dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && dcp_has_panel(dcp); const struct drm_edid *derived = nr_modes ? - (panel ? dcp_panel_edid_snapshot(dcp, &generation) : + (panel ? dcp_panel_edid_snapshot(dcp, modes, nr_modes, &generation) : dcp_firmware_edid(dcp, &generation)) : ERR_PTR(-ENODATA); if (!IS_ERR(derived)) { diff --git a/drivers/gpu/drm/apple/parser-test.c b/drivers/gpu/drm/apple/parser-test.c index 9a7426d1f7016..511ac4e1f66ef 100644 --- a/drivers/gpu/drm/apple/parser-test.c +++ b/drivers/gpu/drm/apple/parser-test.c @@ -1316,6 +1316,19 @@ static void parser_panel_identity(struct kunit *test) struct dcp_parse_ctx ctx; const struct drm_edid *edid; const struct edid *raw; + struct dcp_display_mode modes[2] = {}; + + modes[0].mode = (struct drm_display_mode) { + .clock = 540005, .hdisplay = 3456, .hsync_start = 3500, + .hsync_end = 3550, .htotal = 4000, .vdisplay = 2160, + .vsync_start = 2165, .vsync_end = 2170, .vtotal = 2250, + .flags = DRM_MODE_FLAG_PHSYNC | DRM_MODE_FLAG_PVSYNC, + }; + modes[1] = modes[0]; + modes[1].mode.clock = 1080005; + modes[1].vrr = true; + modes[1].min_vrr = 24U << 16; + modes[1].max_vrr = 120U << 16; KUNIT_ASSERT_NOT_NULL(test, blob); put_unaligned_le32(0xd3, blob->data); @@ -1332,13 +1345,17 @@ static void parser_panel_identity(struct kunit *test) KUNIT_ASSERT_EQ(test, parse(blob->data, blob->size, &ctx), 0); KUNIT_ASSERT_EQ(test, parse_panel_metadata(&ctx, &metadata), 0); KUNIT_EXPECT_STREQ(test, metadata.name, "Synthetic Int"); - edid = dcp_panel_edid(&metadata, 345, 223); + edid = dcp_panel_edid(&metadata, modes, ARRAY_SIZE(modes), 345, 223); KUNIT_ASSERT_FALSE(test, IS_ERR(edid)); KUNIT_EXPECT_TRUE(test, drm_edid_valid(edid)); raw = drm_edid_raw(edid); KUNIT_EXPECT_EQ(test, le16_to_cpu(raw->product_id.product_code), 42); KUNIT_EXPECT_EQ(test, le32_to_cpu(raw->product_id.serial_number), 0x12345678U); KUNIT_EXPECT_EQ(test, raw->gamma, 0xff); + KUNIT_EXPECT_EQ(test, le16_to_cpu(raw->detailed_timings[0].pixel_clock), 54001); + KUNIT_EXPECT_EQ(test, raw->detailed_timings[3].data.other_data.data.range.min_vfreq, 24); + KUNIT_EXPECT_EQ(test, raw->detailed_timings[3].data.other_data.data.range.max_vfreq, 120); + KUNIT_EXPECT_EQ(test, modes[0].mode.clock, 540005); drm_edid_free(edid); } diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/README b/tools/testing/selftests/drivers/gpu/apple-dcp/README index dfbdaaab3f367..4f4cc2880068b 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/README +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/README @@ -173,7 +173,7 @@ completion isolation. These tests do not establish which timings a particular panel reports or prove refresh rate, flicker, brightness smoothness or adaptive-sync on hardware. -Desktop panel brightness regressions also cover identity-only EDID parsing and +Desktop panel brightness regressions also cover panel EDID parsing and publication from ProductAttributes without optional color data. Both bare and descriptor-wrapped powerlog reports pass through the production compact receiver, systemep handler and mNits parser. Rapid key repeats with interleaved stale and diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py index 0c1e7c5dd7400..2d9501f3eaa98 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py @@ -395,30 +395,41 @@ def panel_identity(root, output=None): assert(!parse_panel_metadata(&ctx,&metadata)); assert(!strcmp(metadata.manufacturer,"TST") && metadata.product==42 && metadata.serial==0x12345678); assert(!strcmp(metadata.name,"Synthetic Int")); - /* No color fields, date or EDID-representable timing is required. */ + /* Optional color/date fields are absent; timings come from the catalog. */ struct apple_dcp d={.fw_compat=5,.has_panel=true,.panel={345,223},.connector=&d}; __typeof__(*d.desktop) desktop={0};d.desktop=&desktop; + struct dcp_display_mode modes[3]={0}; + for(unsigned i=0;i<3;i++)modes[i].mode=(struct drm_display_mode){.clock=i==0?1080005:i==1?540005:432000, + .hdisplay=3456,.hsync_start=3500,.hsync_end=3550,.htotal=4000, + .vdisplay=2160,.vsync_start=2165,.vsync_end=2170,.vtotal=2250, + .flags=DRM_MODE_FLAG_PHSYNC|DRM_MODE_FLAG_PVSYNC,.type=i==0?DRM_MODE_TYPE_PREFERRED:0}; + modes[0].vrr=true;modes[0].min_vrr=24U<<16;modes[0].max_vrr=120U<<16; u64 generation=99; - assert(IS_ERR(dcp_panel_edid_snapshot(&d,&generation))); + assert(IS_ERR(dcp_panel_edid_snapshot(&d,modes,3,&generation))); dcp_panel_metadata_replace(&d,&metadata); assert(notifications==1 && desktop.panel_metadata_valid); - const struct drm_edid *edid=dcp_panel_edid_snapshot(&d,&generation); + const struct drm_edid *edid=dcp_panel_edid_snapshot(&d,modes,3,&generation); assert(!IS_ERR(edid) && drm_edid_valid(edid) && dcp_panel_metadata_current(&d,generation)); const u8 *bytes=(const void *)&edid->raw; assert(bytes[8]==0x52 && bytes[9]==0x74 && bytes[10]==42 && !bytes[11]); assert(bytes[12]==0x78 && bytes[13]==0x56 && bytes[14]==0x34 && bytes[15]==0x12); - assert(bytes[18]==1 && bytes[19]==3 && bytes[21]==35 && bytes[22]==22 && bytes[23]==0xff); - assert(bytes[54+3]==EDID_DETAIL_MONITOR_NAME && bytes[72+3]==EDID_DETAIL_MONITOR_SERIAL); - assert(!memcmp(bytes+72+5,"12345678\n",9)); + assert(bytes[18]==1 && bytes[19]==4 && bytes[21]==35 && bytes[22]==22 && bytes[23]==0xff); + assert(bytes[72+3]==EDID_DETAIL_MONITOR_NAME && bytes[90+3]==EDID_DETAIL_MONITOR_SERIAL); + assert(!memcmp(bytes+90+5,"12345678\n",9)); if(argc==2) {FILE *file=fopen(argv[1],"wb");assert(file);assert(fwrite(bytes,128,1,file)==1);fclose(file);} + assert(((unsigned)bytes[54]|(unsigned)bytes[55]<<8)==54001); + assert(bytes[108+3]==EDID_DETAIL_MONITOR_RANGE && bytes[108+5]==24 && bytes[108+6]==120); + assert(bytes[108+4] & DRM_EDID_RANGE_OFFSET_MAX_HFREQ); + /* The encoded timing clock rounds to 10 kHz; the DCP catalog is unchanged. */ + assert(modes[1].mode.clock==540005 && modes[0].mode.clock==1080005); drm_edid_free(edid); dcp_panel_metadata_replace(&d,NULL); assert(!dcp_panel_metadata_current(&d,generation)); - assert(IS_ERR(dcp_panel_edid_snapshot(&d,&generation))); + assert(IS_ERR(dcp_panel_edid_snapshot(&d,modes,3,&generation))); /* Another firmware/profile cannot publish a desktop panel identity. */ d.fw_compat=2;dcp_panel_metadata_replace(&d,&metadata); assert(!desktop.panel_metadata_valid); - metadata.manufacturer[0]='?';assert(IS_ERR(dcp_panel_edid(&metadata,345,223))); + metadata.manufacturer[0]='?';assert(IS_ERR(dcp_panel_edid(&metadata,modes,3,345,223))); return 0; } ''' diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py b/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py index db5930b71215b..4194e8d39ad20 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py @@ -144,6 +144,15 @@ def wire(value, out): #define mul_u32_u32(a, b) ((u64)(u32)(a) * (u32)(b)) #define DIV_ROUND_CLOSEST_ULL(n, d) (((n) + (d) / 2) / (d)) #define DIV_ROUND_CLOSEST(n, d) (((n) + (d) / 2) / (d)) +#ifndef min +#define min(a,b) ((a)<(b)?(a):(b)) +#endif +#ifndef max +#define max(a,b) ((a)>(b)?(a):(b)) +#endif +#define max_t(t,a,b) max((t)(a),(t)(b)) +#define div_u64(n,d) ((n)/(d)) +#define DIV_ROUND_UP_ULL(n,d) (((u64)(n)+(d)-1)/(d)) #define trace_iomfb_color_mode(...) ((void)0) #define trace_iomfb_parse_mode_fail(...) ((void)0) #define trace_iomfb_parse_mode_success(...) ((void)0) @@ -638,7 +647,7 @@ def publication(root): } static const struct drm_edid *dcp_firmware_edid(struct apple_dcp *d,u64 *generation) { (void)d; (void)generation; return ERR_PTR(-ENODATA); } static bool dcp_firmware_metadata_current(struct apple_dcp *d,u64 generation) { (void)d; (void)generation; return false; } -static const struct drm_edid *dcp_panel_edid_snapshot(struct apple_dcp *d,u64 *generation) { (void)d;(void)generation;assert(!"panel identity in external fixture");return ERR_PTR(-ENODATA); } +static const struct drm_edid *dcp_panel_edid_snapshot(struct apple_dcp *d,const struct dcp_display_mode *m,unsigned n,u64 *generation) { (void)d;(void)m;(void)n;(void)generation;assert(!"panel identity in external fixture");return ERR_PTR(-ENODATA); } static bool dcp_panel_metadata_current(struct apple_dcp *d,u64 generation) { (void)d;(void)generation;assert(!"panel identity in external fixture");return false; } static void dcp_panel_metadata_replace(struct apple_dcp *d,const struct dcp_display_metadata *m) { (void)d;(void)m;assert(!"panel identity in external receipt"); } From 4b0269870f7ac7f2dadce1fd820e6dce929e630e Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 09:26:57 +1100 Subject: [PATCH 113/388] drm/apple: keep brightness report observations scoped to desktop Preserve the established active-report filter for the Neo and other non-desktop backends. The desktop backlight uses its separate ready state, so T6041 reports still reach the confirmation-only policy after registration. This avoids exposing unrelated backends to newly parsed report traffic. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/systemep.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/drivers/gpu/drm/apple/systemep.c b/drivers/gpu/drm/apple/systemep.c index 31e4060679bb2..44e239c6016b0 100644 --- a/drivers/gpu/drm/apple/systemep.c +++ b/drivers/gpu/drm/apple/systemep.c @@ -75,6 +75,10 @@ static int powerlog_report(struct apple_epic_service *service, enum epic_subtype struct apple_dcp *dcp = service->ep->dcp; int ret; + /* Keep the existing filtering on active non-desktop backends. */ + if (dcp_backlight_active(dcp)) + return 0; + dev_dbg(dcp->dev, "systemep[ch:%u]: report type:%02x len:%zu\n", service->channel, type, data_size); From 40c53dd27c73bfdfbf237998253f4ce00dc0bcd3 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 09:43:59 +1100 Subject: [PATCH 114/388] drm/apple: retain the fixed panel identity across attribute repeats A repeated or partial DisplayAttributes dictionary must not revoke a known internal panel identity or emit another hotplug event. Retain the validated identity when a dictionary omits product data, and advance its generation only when valid identity fields change. This keeps unrelated attribute updates from making userspace reprobe the monitor and restore brightness again. Extend the host case with repeated and partial dictionaries and an actual identity replacement, and add KUnit for the fixed-panel cache lifecycle. Assisted-by: LLM Signed-off-by: Ryan Murray --- Documentation/gpu/apple-dcp-t6041-panel.rst | 3 ++ drivers/gpu/drm/apple/dcp.c | 16 +++++++---- drivers/gpu/drm/apple/parser-test.c | 28 +++++++++++++++++++ .../selftests/drivers/gpu/apple-dcp/README | 3 ++ .../drivers/gpu/apple-dcp/desktop.py | 17 ++++++++--- 5 files changed, 58 insertions(+), 9 deletions(-) diff --git a/Documentation/gpu/apple-dcp-t6041-panel.rst b/Documentation/gpu/apple-dcp-t6041-panel.rst index a32407077a1c2..94ddee52d02aa 100644 --- a/Documentation/gpu/apple-dcp-t6041-panel.rst +++ b/Documentation/gpu/apple-dcp-t6041-panel.rst @@ -63,3 +63,6 @@ reply. Its enclosing command must ACK before the set fails, so a subsequent mode change can reuse the drained channel. Accepted atomic modeset errors are reported through the out-fence. A timed-out command retains its channel until the firmware replies; another set cannot overwrite its stack. + +Repeated or partial DisplayAttributes retain a validated fixed-panel identity. +They do not emit another identity hotplug event or revoke the EDID. diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 70c52f2015574..5f3901acba4de 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -781,16 +781,22 @@ void dcp_panel_metadata_replace(struct apple_dcp *dcp, const struct dcp_display_metadata *metadata) { unsigned long flags; + bool changed = false; - if (dcp->fw_compat != DCP_FIRMWARE_V_26_6_DESKTOP || !dcp_has_panel(dcp)) + if (!metadata || dcp->fw_compat != DCP_FIRMWARE_V_26_6_DESKTOP || + !dcp_has_panel(dcp)) return; spin_lock_irqsave(&dcp->metadata.lock, flags); - dcp->desktop->panel_metadata_generation++; - dcp->desktop->panel_metadata_valid = !!metadata; - if (metadata) + /* Partial or repeated attributes cannot revoke this fixed panel's identity. */ + if (!dcp->desktop->panel_metadata_valid || + memcmp(&dcp->desktop->panel_metadata, metadata, sizeof(*metadata))) { dcp->desktop->panel_metadata = *metadata; + dcp->desktop->panel_metadata_valid = true; + dcp->desktop->panel_metadata_generation++; + changed = true; + } spin_unlock_irqrestore(&dcp->metadata.lock, flags); - if (dcp->connector) + if (changed && dcp->connector) dcp_queue_hotplug(dcp->connector); } diff --git a/drivers/gpu/drm/apple/parser-test.c b/drivers/gpu/drm/apple/parser-test.c index 511ac4e1f66ef..aeea21b07e5eb 100644 --- a/drivers/gpu/drm/apple/parser-test.c +++ b/drivers/gpu/drm/apple/parser-test.c @@ -1359,7 +1359,35 @@ static void parser_panel_identity(struct kunit *test) drm_edid_free(edid); } +static void parser_panel_identity_stable(struct kunit *test) +{ + struct apple_dcp *dcp = kunit_kzalloc(test, sizeof(*dcp), GFP_KERNEL); + struct dcp_desktop_state *desktop = kunit_kzalloc(test, sizeof(*desktop), GFP_KERNEL); + struct dcp_display_metadata metadata = { + .manufacturer = "TST", .name = "Synthetic", .product = 42, .serial = 1, + }; + u64 generation; + + KUNIT_ASSERT_NOT_NULL(test, dcp); + KUNIT_ASSERT_NOT_NULL(test, desktop); + dcp->desktop = desktop; + dcp->fw_compat = DCP_FIRMWARE_V_26_6_DESKTOP; + dcp->panel.width_mm = 345; + spin_lock_init(&dcp->metadata.lock); + dcp_panel_metadata_replace(dcp, &metadata); + generation = desktop->panel_metadata_generation; + KUNIT_EXPECT_TRUE(test, dcp_panel_metadata_current(dcp, generation)); + dcp_panel_metadata_replace(dcp, NULL); + dcp_panel_metadata_replace(dcp, &metadata); + KUNIT_EXPECT_TRUE(test, dcp_panel_metadata_current(dcp, generation)); + metadata.serial++; + dcp_panel_metadata_replace(dcp, &metadata); + KUNIT_EXPECT_FALSE(test, dcp_panel_metadata_current(dcp, generation)); + KUNIT_EXPECT_EQ(test, desktop->panel_metadata.serial, 2U); +} + static struct kunit_case parser_cases[] = { + KUNIT_CASE(parser_panel_identity_stable), KUNIT_CASE(parser_panel_identity), KUNIT_CASE(parser_score_presence), KUNIT_CASE(parser_legacy_ties), diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/README b/tools/testing/selftests/drivers/gpu/apple-dcp/README index 4f4cc2880068b..2938f3f00b9ce 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/README +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/README @@ -181,3 +181,6 @@ new reports must converge monotonically to the last user target in both directions. Report observations cannot restart a fade or request another write. The callback cases exercise the desktop D202/D454 wire adapters, physical resource mapping and release, and unknown-callback failure followed by a successful set. + +Panel identity tests also retain the EDID and generation across repeated and +partial dictionaries, and notify only when valid identity data actually changes. diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py index 2d9501f3eaa98..a89bbf9804a85 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py @@ -423,11 +423,20 @@ def panel_identity(root, output=None): /* The encoded timing clock rounds to 10 kHz; the DCP catalog is unchanged. */ assert(modes[1].mode.clock==540005 && modes[0].mode.clock==1080005); drm_edid_free(edid); - dcp_panel_metadata_replace(&d,NULL); - assert(!dcp_panel_metadata_current(&d,generation)); - assert(IS_ERR(dcp_panel_edid_snapshot(&d,modes,3,&generation))); + /* Repeated and partial dictionaries cannot trigger monitor/brightness restore loops. */ + for(unsigned repeat=0;repeat<32;repeat++) { + dcp_panel_metadata_replace(&d,NULL); + dcp_panel_metadata_replace(&d,&metadata); + assert(dcp_panel_metadata_current(&d,generation) && notifications==1); + } + metadata.serial++; + dcp_panel_metadata_replace(&d,&metadata); + assert(!dcp_panel_metadata_current(&d,generation) && notifications==2); + edid=dcp_panel_edid_snapshot(&d,modes,3,&generation); + assert(!IS_ERR(edid) && dcp_panel_metadata_current(&d,generation)); + drm_edid_free(edid); /* Another firmware/profile cannot publish a desktop panel identity. */ - d.fw_compat=2;dcp_panel_metadata_replace(&d,&metadata); + desktop=(__typeof__(desktop)){0};d.fw_compat=2;dcp_panel_metadata_replace(&d,&metadata); assert(!desktop.panel_metadata_valid); metadata.manufacturer[0]='?';assert(IS_ERR(dcp_panel_edid(&metadata,modes,3,345,223))); return 0; From f840284506bf557e52009a137ef1e04f95537b73 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 10:17:38 +1100 Subject: [PATCH 115/388] drm/apple: receive panel identity through dictionary publications The desktop dictionary setters previously acknowledged every property without retaining its data. Decode DisplayAttributes and direct ProductAttributes on the fixed panel, alongside the existing chunked path. Bound their input records and preserve the existing boolean acknowledgment, including byte and padded results, for null or unrelated properties. Accept a legacy PNP manufacturer code and derive a missing model label from the actual vendor/product IDs. Keep unspecified serial and label fields from erasing known data for the same fixed panel. Optional manufacture fields do not control identity parsing. Extend the host case through the actual D552/D561 bindings, with both dictionary forms, minimal identity, null and unrelated setters, reply canaries and partial updates. External metadata parsing and the contributor DMA callbacks remain unchanged. Assisted-by: LLM Signed-off-by: Ryan Murray --- Documentation/gpu/apple-dcp-t6041-panel.rst | 6 +++ drivers/gpu/drm/apple/dcp-display-metadata.c | 5 +- drivers/gpu/drm/apple/dcp.c | 25 ++++++++-- drivers/gpu/drm/apple/iomfb_v26_6_desktop.c | 38 +++++++++++++- drivers/gpu/drm/apple/parser-test.c | 6 +++ drivers/gpu/drm/apple/parser.c | 26 ++++++++-- drivers/gpu/drm/apple/parser.h | 2 + .../selftests/drivers/gpu/apple-dcp/README | 4 ++ .../drivers/gpu/apple-dcp/desktop.py | 50 ++++++++++++++++++- 9 files changed, 150 insertions(+), 12 deletions(-) diff --git a/Documentation/gpu/apple-dcp-t6041-panel.rst b/Documentation/gpu/apple-dcp-t6041-panel.rst index 94ddee52d02aa..01a03c0f2777d 100644 --- a/Documentation/gpu/apple-dcp-t6041-panel.rst +++ b/Documentation/gpu/apple-dcp-t6041-panel.rst @@ -66,3 +66,9 @@ the firmware replies; another set cannot overwrite its stack. Repeated or partial DisplayAttributes retain a validated fixed-panel identity. They do not emit another identity hotplug event or revoke the EDID. + +Identity accepts both chunked DisplayAttributes and the desktop dictionary +setters. A legacy PNP manufacturer code is decoded when the text code is absent. +Missing optional labels are represented by the firmware vendor/product code and +unspecified numeric serial, without inventing a per-device identity. Partial +updates for the same vendor/product retain known optional identity fields. diff --git a/drivers/gpu/drm/apple/dcp-display-metadata.c b/drivers/gpu/drm/apple/dcp-display-metadata.c index 8819f65cb8655..94cc5ec03417f 100644 --- a/drivers/gpu/drm/apple/dcp-display-metadata.c +++ b/drivers/gpu/drm/apple/dcp-display-metadata.c @@ -231,7 +231,7 @@ const struct drm_edid *dcp_panel_edid(const struct dcp_display_metadata *metadat struct detailed_non_pixel *limits = &block.detailed_timings[3].data.other_data; struct detailed_data_monitor_range *range = &limits->data.range; const struct drm_edid *edid; - char serial[14]; + char serial[14], name[14]; u32 min_v = U32_MAX, max_v = 0, min_h = U32_MAX, max_h = 0, clock = 0; u16 manufacturer = 0; u8 sum = 0; @@ -295,8 +295,9 @@ const struct drm_edid *dcp_panel_edid(const struct dcp_display_metadata *metadat block.gamma = 0xff; block.features = DRM_EDID_FEATURE_PREFERRED_TIMING; memset(block.standard_timings, 1, sizeof(block.standard_timings)); + snprintf(name, sizeof(name), "%.3s %04x", metadata->manufacturer, metadata->product); ret = dcp_metadata_text(&block.detailed_timings[1], EDID_DETAIL_MONITOR_NAME, - metadata->name); + metadata->name[0] ? metadata->name : name); if (ret) return ERR_PTR(ret); snprintf(serial, sizeof(serial), "%08x", metadata->serial); diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 5f3901acba4de..239d7506183b8 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -780,6 +780,7 @@ const struct drm_edid *dcp_firmware_edid(struct apple_dcp *dcp, u64 *generation) void dcp_panel_metadata_replace(struct apple_dcp *dcp, const struct dcp_display_metadata *metadata) { + struct dcp_display_metadata merged; unsigned long flags; bool changed = false; @@ -787,10 +788,28 @@ void dcp_panel_metadata_replace(struct apple_dcp *dcp, !dcp_has_panel(dcp)) return; spin_lock_irqsave(&dcp->metadata.lock, flags); - /* Partial or repeated attributes cannot revoke this fixed panel's identity. */ + merged = *metadata; + if (dcp->desktop->panel_metadata_valid && + merged.product == dcp->desktop->panel_metadata.product && + !memcmp(merged.manufacturer, dcp->desktop->panel_metadata.manufacturer, 4)) { + const struct dcp_display_metadata *old = &dcp->desktop->panel_metadata; + bool same_serial = !merged.serial || merged.serial == old->serial; + + /* Zero/empty optional fields mean unspecified, not a new panel. */ + if (!merged.name[0]) + memcpy(merged.name, old->name, sizeof(merged.name)); + if (!merged.serial) + merged.serial = old->serial; + if (same_serial && !merged.serial_text[0]) + memcpy(merged.serial_text, old->serial_text, sizeof(merged.serial_text)); + if (!merged.year) + merged.year = old->year; + if (!merged.week) + merged.week = old->week; + } if (!dcp->desktop->panel_metadata_valid || - memcmp(&dcp->desktop->panel_metadata, metadata, sizeof(*metadata))) { - dcp->desktop->panel_metadata = *metadata; + memcmp(&dcp->desktop->panel_metadata, &merged, sizeof(merged))) { + dcp->desktop->panel_metadata = merged; dcp->desktop->panel_metadata_valid = true; dcp->desktop->panel_metadata_generation++; changed = true; diff --git a/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c b/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c index ed7f5faadf0f6..033f915ff9f5f 100644 --- a/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c +++ b/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c @@ -91,6 +91,37 @@ static bool trampoline_passthrough_dict_v26_6(struct apple_dcp *dcp, int tag, return true; } +/* The firmware can publish panel identity through dictionary setters too. */ +static bool trampoline_panel_dictionary_v26_6(struct apple_dcp *dcp, int tag, + void *out, void *in) +{ + const struct dcp_dict_passthrough_req_v26_6 *req = in; + const char *key = (const char *)req->key; + struct dcp_display_metadata metadata; + struct dcp_parse_ctx ctx; + int ret; + + trace_iomfb_callback(dcp, tag, __func__); + if (!dcp_has_panel(dcp) || req->value_null || + !memchr(key, 0, sizeof(req->key))) + goto done; + if (strcmp(key, "DisplayAttributes") && strcmp(key, "ProductAttributes")) + goto done; + ret = parse(req->value, sizeof(req->value), &ctx); + if (ret) + goto done; + if (!strcmp(key, "ProductAttributes")) + ret = parse_panel_product_metadata(&ctx, &metadata); + else + ret = parse_panel_metadata(&ctx, &metadata); + if (!ret) + dcp_panel_metadata_replace(dcp, &metadata); +done: + /* Preserve the existing setter acknowledgment for unrelated properties. */ + *(u8 *)out = true; + return true; +} + bool iomfb_check_callback_v26_6_desktop(int tag, u32 in_len, u32 out_len) { switch (tag) { @@ -104,6 +135,9 @@ bool iomfb_check_callback_v26_6_desktop(int tag, u32 in_len, u32 out_len) return in_len == 24 && out_len == 20; case 454: return in_len == 4 && out_len == 4; + case 552: + case 561: + return in_len == 4164 && (out_len == 1 || out_len == 4); default: return true; } @@ -198,8 +232,8 @@ static const iomfb_cb_handler cb_handlers[IOMFB_MAX_CB] = { [451] = trampoline_allocate_buffer, [452] = trampoline_map_physical, [454] = trampoline_release_mem_desc_v26_6, - [552] = trampoline_true, /* set_property_dict_0 */ - [561] = trampoline_true, /* set_property_dict */ + [552] = trampoline_panel_dictionary_v26_6, + [561] = trampoline_panel_dictionary_v26_6, [563] = trampoline_true, /* set_property_int */ [565] = trampoline_true, /* set_property_bool */ [567] = trampoline_true, /* set_property_str */ diff --git a/drivers/gpu/drm/apple/parser-test.c b/drivers/gpu/drm/apple/parser-test.c index aeea21b07e5eb..459de4aaecda9 100644 --- a/drivers/gpu/drm/apple/parser-test.c +++ b/drivers/gpu/drm/apple/parser-test.c @@ -1379,7 +1379,13 @@ static void parser_panel_identity_stable(struct kunit *test) KUNIT_EXPECT_TRUE(test, dcp_panel_metadata_current(dcp, generation)); dcp_panel_metadata_replace(dcp, NULL); dcp_panel_metadata_replace(dcp, &metadata); + { + struct dcp_display_metadata partial = { .manufacturer = "TST", .product = 42 }; + + dcp_panel_metadata_replace(dcp, &partial); + } KUNIT_EXPECT_TRUE(test, dcp_panel_metadata_current(dcp, generation)); + KUNIT_EXPECT_EQ(test, desktop->panel_metadata.serial, 1U); metadata.serial++; dcp_panel_metadata_replace(dcp, &metadata); KUNIT_EXPECT_FALSE(test, dcp_panel_metadata_current(dcp, generation)); diff --git a/drivers/gpu/drm/apple/parser.c b/drivers/gpu/drm/apple/parser.c index 1de2fa263a865..fb16cee1cad22 100644 --- a/drivers/gpu/drm/apple/parser.c +++ b/drivers/gpu/drm/apple/parser.c @@ -1328,7 +1328,7 @@ static int parse_panel_label(struct dcp_parse_ctx *handle, char out[14]) if (IS_ERR(tag)) return PTR_ERR(tag); - if (!tag->size || tag->size > 255) + if (tag->size > 255) return -EINVAL; value = parse_bytes(handle, tag->size); if (IS_ERR(value)) @@ -1381,12 +1381,14 @@ static int parse_metadata_product(struct dcp_parse_ctx *handle, if (seen & 8) return -EINVAL; seen |= 8; - ret = parse_metadata_number(handle, &out->week, 0, 53); + ret = panel ? skip(handle) : + parse_metadata_number(handle, &out->week, 0, 53); } else if (!strcmp(key, "YearOfManufacture")) { if (seen & 16) return -EINVAL; seen |= 16; - ret = parse_metadata_number(handle, &out->year, 1990, 2245); + ret = panel ? skip(handle) : + parse_metadata_number(handle, &out->year, 1990, 2245); } else if (!strcmp(key, "SerialNumber")) { if (seen & 64) return -EINVAL; @@ -1410,7 +1412,12 @@ static int parse_metadata_product(struct dcp_parse_ctx *handle, if (ret) return ret; } - if ((seen & (panel ? 7 : 31)) != (panel ? 7 : 31) || + if (panel && !(seen & 1) && (seen & 32)) { + for (unsigned int i = 0; i < 3; i++) + out->manufacturer[i] = '@' + ((legacy >> (10 - 5 * i)) & 31); + seen |= 1; + } + if ((seen & (panel ? 5 : 31)) != (panel ? 5 : 31) || strlen(out->manufacturer) != 3) return -EINVAL; for (unsigned int i = 0; i < 3; i++) @@ -1476,6 +1483,17 @@ int parse_display_metadata(struct dcp_parse_ctx *handle, struct dcp_display_meta } /* Identity does not depend on optional colorimetry or EDID-compatible timing. */ +int parse_panel_product_metadata(struct dcp_parse_ctx *handle, + struct dcp_display_metadata *out) +{ + struct dcp_display_metadata parsed = {}; + int ret = parse_metadata_product(handle, &parsed, true); + + if (!ret) + *out = parsed; + return ret; +} + int parse_panel_metadata(struct dcp_parse_ctx *handle, struct dcp_display_metadata *out) { struct dcp_display_metadata parsed = {}; diff --git a/drivers/gpu/drm/apple/parser.h b/drivers/gpu/drm/apple/parser.h index c88f4dd21b855..fecec2656201e 100644 --- a/drivers/gpu/drm/apple/parser.h +++ b/drivers/gpu/drm/apple/parser.h @@ -197,6 +197,8 @@ int replace_modes(struct dcp_parse_ctx *handle, int width_mm, int height_mm, unsigned int notch_height, bool internal); int parse_panel_metadata(struct dcp_parse_ctx *handle, struct dcp_display_metadata *out); +int parse_panel_product_metadata(struct dcp_parse_ctx *handle, + struct dcp_display_metadata *out); int parse_display_attributes(struct dcp_parse_ctx *handle, int *width_mm, int *height_mm, bool *backlight_control); int parse_epic_service_init(struct dcp_parse_ctx *handle, const char **name, diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/README b/tools/testing/selftests/drivers/gpu/apple-dcp/README index 2938f3f00b9ce..51c7c0db9f83f 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/README +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/README @@ -184,3 +184,7 @@ mapping and release, and unknown-callback failure followed by a successful set. Panel identity tests also retain the EDID and generation across repeated and partial dictionaries, and notify only when valid identity data actually changes. + +The identity case also exercises the real D552/D561 table bindings, nested +DisplayAttributes and direct ProductAttributes, legacy manufacturer codes, +minimal identity data, and unchanged byte/padded boolean acknowledgments. diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py index a89bbf9804a85..6216f713f15b1 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py @@ -368,14 +368,23 @@ def panel_identity(root, output=None): 'ProductName': 'Synthetic Internal Panel', 'SerialNumber': 0x12345678}} raw = bytearray(struct.pack('has_panel;} static void dcp_queue_hotplug(void *con) {assert(con);notifications++;} -''' + helpers + '\nstatic const u8 attributes[]={'+','.join(map(str,raw))+'};\n' + r''' +''' + helpers + dictionary + '\nstatic const u8 attributes[]={'+','.join(map(str,raw))+'};\n' + 'static const u8 product[]={'+','.join(map(str,product))+'};\n' + 'static const u8 minimal[]={'+','.join(map(str,minimal))+'};\n' + r''' int main(int argc, char **argv) { struct dcp_parse_ctx ctx; @@ -406,6 +415,41 @@ def panel_identity(root, output=None): modes[0].vrr=true;modes[0].min_vrr=24U<<16;modes[0].max_vrr=120U<<16; u64 generation=99; assert(IS_ERR(dcp_panel_edid_snapshot(&d,modes,3,&generation))); + bool (*handlers[600])(struct apple_dcp *,int,void *,void *)={ +''' + bindings + r''' + }; + for(unsigned form=0;form<3;form++) { + desktop=(__typeof__(desktop)){0};notifications=0; + struct dcp_dict_passthrough_req_v26_6 wire={0};u32 result=0; + strcpy((char *)wire.key,form?"ProductAttributes":"DisplayAttributes"); + const u8 *data=form==2?minimal:form?product:attributes; + size_t size=form==2?sizeof(minimal):form?sizeof(product):sizeof(attributes); + memcpy(wire.value,data,size); + unsigned tag=form?552:561; + assert(iomfb_check_callback_v26_6_desktop(tag,sizeof(wire),4)); + assert(iomfb_check_callback_v26_6_desktop(tag,sizeof(wire),1)); + assert(!iomfb_check_callback_v26_6_desktop(tag,sizeof(wire)-1,4)); + u8 short_reply[2]={0,0xa5}; + assert(handlers[tag](&d,tag,short_reply,&wire)); + assert(short_reply[0]==1 && short_reply[1]==0xa5); + assert(handlers[tag](&d,tag,&result,&wire) && result==1); + assert(desktop.panel_metadata_valid && notifications==1); + assert(desktop.panel_metadata.serial==(form==2?0:metadata.serial)); + assert(!strcmp(desktop.panel_metadata.manufacturer,"TST")); + if(form==2) { + const struct drm_edid *minimal_edid=dcp_panel_edid_snapshot(&d,modes,3,&generation); + assert(!IS_ERR(minimal_edid) && drm_edid_valid(minimal_edid)); + const u8 *m=(const void *)&minimal_edid->raw; + assert(!memcmp(m+72+5,"TST 002a",8) && !memcmp(m+90+5,"00000000",8)); + drm_edid_free(minimal_edid); + } + /* Null and unrelated setters preserve the existing ACK and identity. */ + wire.value_null=1;result=0; + assert(handlers[tag](&d,tag,&result,&wire) && result==1 && notifications==1); + wire.value_null=0;strcpy((char *)wire.key,"Unrelated");result=0; + assert(handlers[tag](&d,tag,&result,&wire) && result==1 && notifications==1); + } + desktop=(__typeof__(desktop)){0};notifications=0; dcp_panel_metadata_replace(&d,&metadata); assert(notifications==1 && desktop.panel_metadata_valid); const struct drm_edid *edid=dcp_panel_edid_snapshot(&d,modes,3,&generation); @@ -429,6 +473,10 @@ def panel_identity(root, output=None): dcp_panel_metadata_replace(&d,&metadata); assert(dcp_panel_metadata_current(&d,generation) && notifications==1); } + struct dcp_display_metadata partial={.manufacturer="TST",.product=42}; + dcp_panel_metadata_replace(&d,&partial); + assert(dcp_panel_metadata_current(&d,generation) && notifications==1); + assert(desktop.panel_metadata.serial==metadata.serial); metadata.serial++; dcp_panel_metadata_replace(&d,&metadata); assert(!dcp_panel_metadata_current(&d,generation) && notifications==2); From c19d5c120633410a287c875f6d0352120d92b763 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 11:42:19 +1100 Subject: [PATCH 116/388] drm/apple: inspect desktop panel property keys behind a debug switch The desktop dictionary setters run without supplying the identity keys we expected. Add opt-in structural inspection to D552, D561, D114 and chunked properties. Bound the traversal by depth and entries and the output per DCP, and emit keys and types only. The default stays silent. Exercise malformed, truncated and oversized dictionaries on the host and in KUnit. No captured firmware values are included. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/dcp-internal.h | 5 ++ drivers/gpu/drm/apple/iomfb.c | 41 +++++++++++++ drivers/gpu/drm/apple/iomfb_template.c | 3 + drivers/gpu/drm/apple/iomfb_v26_6_desktop.c | 5 ++ drivers/gpu/drm/apple/parser-test.c | 22 +++++++ drivers/gpu/drm/apple/parser.c | 58 +++++++++++++++++++ drivers/gpu/drm/apple/parser.h | 3 + .../drivers/gpu/apple-dcp/desktop.py | 31 ++++++++++ .../selftests/drivers/gpu/apple-dcp/run.py | 1 + 9 files changed, 169 insertions(+) diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index f8cbb94b6951e..37f6228dd7af9 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -247,6 +247,7 @@ struct dcp_desktop_state { struct dcp_display_metadata panel_metadata; u64 panel_metadata_generation; bool panel_metadata_valid; + u32 identity_log_count; /* RX records an unsupported callback until the modeset RPC has drained. */ bool modeset_active; bool modeset_callback_error; @@ -892,4 +893,8 @@ static inline bool dcp_ext_dptx_607(const struct apple_dcp *dcp) #define DCP_AUDIO_MAX_CHANS 15 + +void dcp_trace_panel_property(struct apple_dcp *dcp, int tag, const char *key, + size_t key_len, const void *blob, size_t len); + #endif /* __APPLE_DCP_INTERNAL_H__ */ diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index 510ef60e8e592..5a67d1801af4e 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -40,6 +40,47 @@ #include "parser.h" #include "trace.h" +/* Enable for one diagnostic boot with apple_drm.panel_identity_debug=1. */ +static bool panel_identity_debug; +module_param(panel_identity_debug, bool, 0600); +MODULE_PARM_DESC(panel_identity_debug, "Bounded desktop panel property keys and types (no values)"); + +static void dcp_identity_key_log(void *data, const char *key, u32 key_len, + u32 type, u32 size, u32 depth) +{ + struct apple_dcp *dcp = data; + char safe[65]; + u32 i; + + if (dcp->desktop->identity_log_count >= 256) + return; + for (i = 0; i < min_t(u32, key_len, sizeof(safe) - 1); i++) + safe[i] = key[i] >= 0x20 && key[i] <= 0x7e ? key[i] : '?'; + safe[i] = 0; + dcp->desktop->identity_log_count++; + dev_info(dcp->dev, "panel identity depth=%u key=%s type=%u size=%u\n", + depth, safe, type, size); +} + +void dcp_trace_panel_property(struct apple_dcp *dcp, int tag, const char *key, + size_t key_len, const void *blob, size_t len) +{ + int ret; + + if (!READ_ONCE(panel_identity_debug) || + dcp->fw_compat != DCP_FIRMWARE_V_26_6_DESKTOP || + !dcp_has_panel(dcp) || dcp->desktop->identity_log_count >= 256) + return; + dev_info(dcp->dev, "panel identity callback=D%03d bytes=%zu null=%d\n", + tag, len, !blob); + dcp_identity_key_log(dcp, key, key_len, 0, len, 0); + if (!blob) + return; + ret = parse_identity_keys(blob, len, dcp_identity_key_log, dcp); + if (ret) + dev_info(dcp->dev, "panel identity inspection stopped: %d\n", ret); +} + static int dcp_tx_offset(enum dcp_context_id id) { switch (id) { diff --git a/drivers/gpu/drm/apple/iomfb_template.c b/drivers/gpu/drm/apple/iomfb_template.c index 0a735158ceda8..e34af2a47e868 100644 --- a/drivers/gpu/drm/apple/iomfb_template.c +++ b/drivers/gpu/drm/apple/iomfb_template.c @@ -1264,6 +1264,9 @@ static bool dcpep_process_chunks(struct apple_dcp *dcp, dcp_property_complete(dcp->chunks.data, dcp->chunks.received, dcp->chunks.length); + dcp_trace_panel_property(dcp, 0, req->key, + strnlen(req->key, sizeof(req->key)), dcp->chunks.data, dcp->chunks.length); + ret = parse(dcp->chunks.data, dcp->chunks.length, &ctx); if (ret) { diff --git a/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c b/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c index 033f915ff9f5f..bd8eae4ee6716 100644 --- a/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c +++ b/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c @@ -81,6 +81,9 @@ static bool trampoline_passthrough_dict_v26_6(struct apple_dcp *dcp, int tag, struct dcp_dict_passthrough_resp_v26_6 *resp = out; trace_iomfb_callback(dcp, tag, "dcpep_cb_passthrough_dict"); + dcp_trace_panel_property(dcp, tag, (const char *)req->key, + strnlen((const char *)req->key, sizeof(req->key)), + req->value_null ? NULL : req->value, sizeof(req->value)); if (!req->value_null) memcpy(resp->value, req->value, sizeof(resp->value)); @@ -102,6 +105,8 @@ static bool trampoline_panel_dictionary_v26_6(struct apple_dcp *dcp, int tag, int ret; trace_iomfb_callback(dcp, tag, __func__); + dcp_trace_panel_property(dcp, tag, key, strnlen(key, sizeof(req->key)), + req->value_null ? NULL : req->value, sizeof(req->value)); if (!dcp_has_panel(dcp) || req->value_null || !memchr(key, 0, sizeof(req->key))) goto done; diff --git a/drivers/gpu/drm/apple/parser-test.c b/drivers/gpu/drm/apple/parser-test.c index 459de4aaecda9..108a7ca67b2ef 100644 --- a/drivers/gpu/drm/apple/parser-test.c +++ b/drivers/gpu/drm/apple/parser-test.c @@ -1392,7 +1392,29 @@ static void parser_panel_identity_stable(struct kunit *test) KUNIT_EXPECT_EQ(test, desktop->panel_metadata.serial, 2U); } +static void parser_identity_emit(void *data, const char *key, u32 len, + u32 type, u32 size, u32 depth) +{ + u32 *seen = data; + + (*seen)++; +} + +static void parser_identity_bounds(struct kunit *test) +{ + u32 seen = 0; + __le32 huge[] = { cpu_to_le32(0xd3), cpu_to_le32(0x01ffffff) }; + + KUNIT_EXPECT_EQ(test, parse_identity_keys(huge, sizeof(huge), + parser_identity_emit, &seen), -E2BIG); + KUNIT_EXPECT_EQ(test, seen, 0U); + for (u32 len = 0; len < sizeof(huge); len++) + KUNIT_EXPECT_LT(test, parse_identity_keys(huge, len, + parser_identity_emit, &seen), 0); +} + static struct kunit_case parser_cases[] = { + KUNIT_CASE(parser_identity_bounds), KUNIT_CASE(parser_panel_identity_stable), KUNIT_CASE(parser_panel_identity), KUNIT_CASE(parser_score_presence), diff --git a/drivers/gpu/drm/apple/parser.c b/drivers/gpu/drm/apple/parser.c index fb16cee1cad22..54b9606a4f632 100644 --- a/drivers/gpu/drm/apple/parser.c +++ b/drivers/gpu/drm/apple/parser.c @@ -2003,3 +2003,61 @@ int parse_system_log_mnits(struct dcp_parse_ctx *handle, struct dcp_system_ev_mn return 0; } + +/* Inspect structure only. Never emit a firmware value or allocate from its size. */ +static int parse_identity_tree(struct dcp_parse_ctx *ctx, dcp_identity_key_fn emit, + void *data, u32 depth, u32 *budget) +{ + const struct dcp_parse_tag *tag; + u32 count; + + if (!*budget || depth > 4) + return -E2BIG; + --*budget; + tag = parse_tag(ctx); + if (IS_ERR(tag)) + return PTR_ERR(tag); + if (tag->type != DCP_TYPE_DICTIONARY && tag->type != DCP_TYPE_ARRAY) { + /* Reuse the bounded scalar decoder from the original position. */ + ctx->pos -= sizeof(*tag); + return skip(ctx); + } + count = tag->size; + if (count > *budget) + return -E2BIG; + for (u32 i = 0; i < count; i++) { + struct dcp_parse_ctx value; + const struct dcp_parse_tag *key_tag, *value_tag; + const char *key = "[]"; + u32 length = 2; + int ret; + + if (tag->type == DCP_TYPE_DICTIONARY) { + key_tag = parse_tag_of_type(ctx, DCP_TYPE_STRING); + if (IS_ERR(key_tag)) + return PTR_ERR(key_tag); + length = key_tag->size; + key = parse_bytes(ctx, length); + if (IS_ERR(key)) + return PTR_ERR(key); + } + value = *ctx; + value_tag = parse_tag(&value); + if (IS_ERR(value_tag)) + return PTR_ERR(value_tag); + emit(data, key, length, value_tag->type, value_tag->size, depth); + ret = parse_identity_tree(ctx, emit, data, depth + 1, budget); + if (ret) + return ret; + } + return 0; +} + +int parse_identity_keys(const void *blob, size_t len, dcp_identity_key_fn emit, void *data) +{ + struct dcp_parse_ctx ctx; + u32 budget = 64; + int ret = parse(blob, len, &ctx); + + return ret ?: parse_identity_tree(&ctx, emit, data, 0, &budget); +} diff --git a/drivers/gpu/drm/apple/parser.h b/drivers/gpu/drm/apple/parser.h index fecec2656201e..9a81c0d15ac1b 100644 --- a/drivers/gpu/drm/apple/parser.h +++ b/drivers/gpu/drm/apple/parser.h @@ -11,6 +11,9 @@ #include "dcp-metadata-epoch.h" struct apple_dcp; +typedef void (*dcp_identity_key_fn)(void *data, const char *key, u32 key_len, + u32 type, u32 size, u32 depth); +int parse_identity_keys(const void *blob, size_t len, dcp_identity_key_fn emit, void *data); struct apple_connector; struct apple_dcp_typec_route; diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py index 6216f713f15b1..9ae0c1808a53a 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py @@ -635,3 +635,34 @@ def keys_and_reports(root): return 0; } ''' + + +def identity_keys(root): + import rate, struct + f = rate.parser_fixture(root) + data = bytearray(struct.pack(' Date: Sun, 25 Oct 2026 20:43:03 -0400 Subject: [PATCH 117/388] cpufreq: apple: settle T8140 lazy requests before suspend A fast-switch request returns its frequency once committed, and the core records it in policy->cur, but on a lazy_verify SoC it may still be pending behind a busy transition, or be dropped when the verify timer finds that transition failed. cpufreq_generic_suspend() skips a suspend frequency equal to policy->cur, so suspend could leave the cluster at an older state, or miss a failure, without calling the driver. Give the driver its own ->suspend: on lazy_verify SoCs it cancels the verify timer, reports a failure the timer had yet to report, writes the pending request and waits for it, verifying it and the transition before it, and only then does the generic suspend. A latched failure fails the driver suspend, which the core logs. Waited-for requests share the draining step. They never run alongside fast-switch requests, so ->target_index no longer holds the raw spinlock, with interrupts off, across its two polls; a failure report the timer lost to the cancel is now printed; ->exit drains as well. Measured on T8140 against the previous change's base in the same boot, alternating, three rounds (us per submit, medians): compute.light queue d1 102.25 -> 102.12 compute.light queue d2 36.93 -> 36.47 render.light queue d2 48.24 -> 48.45 A pm_test=devices suspend cycle ran without DVFS or driver suspend errors. With a diagnostic build, not part of this change: a request left pending for the lowest frequency, equal to policy->cur and suspend_freq, was written and verified at suspend; a stuck BUSY that dropped the pending request, with the timer's report withheld, was reported at suspend and failed the driver suspend. --- drivers/cpufreq/apple-soc-cpufreq.c | 118 +++++++++++++++++++++------- 1 file changed, 90 insertions(+), 28 deletions(-) diff --git a/drivers/cpufreq/apple-soc-cpufreq.c b/drivers/cpufreq/apple-soc-cpufreq.c index a634be0b6f6a7..a50628797fb4f 100644 --- a/drivers/cpufreq/apple-soc-cpufreq.c +++ b/drivers/cpufreq/apple-soc-cpufreq.c @@ -228,8 +228,9 @@ struct apple_cpu_priv { /* * Lazy verification, see apple_soc_cpufreq_lazy_request(): the lock * serialises the command register, expected_pstate and - * transition_failed between the scheduler's requests, the timer that - * verifies them and ->target_index. + * transition_failed between the scheduler's requests and the timer + * that verifies them. Waited-for requests cancel the timer first and + * do not take it. */ raw_spinlock_t lock; struct hrtimer verify_timer; @@ -427,19 +428,14 @@ static void apple_soc_cpufreq_write(struct apple_cpu_priv *priv, u64 reg, } /* - * Requests the frequency table entry @index and, on a SoC that verifies - * transitions, waits for the cluster to accept it. + * Requests @pstate and, on a SoC that verifies transitions, waits for the + * cluster to accept it, having waited for and verified the previous request. */ -static int apple_soc_cpufreq_request(struct cpufreq_policy *policy, - unsigned int index) +static int apple_soc_cpufreq_set_pstate(struct apple_cpu_priv *priv, + unsigned int pstate) { - struct apple_cpu_priv *priv = policy->driver_data; - unsigned int pstate = policy->freq_table[index].driver_data; u64 reg; - /* OPP level is a hardware ID, not the frequency-table array index. */ - if (pstate < priv->info->min_pstate || pstate > priv->info->max_pstate) - return -EINVAL; if (priv->transition_failed) return -EIO; @@ -487,6 +483,20 @@ static int apple_soc_cpufreq_request(struct cpufreq_policy *policy, return 0; } +/* Requests the frequency table entry @index, see apple_soc_cpufreq_set_pstate(). */ +static int apple_soc_cpufreq_request(struct cpufreq_policy *policy, + unsigned int index) +{ + struct apple_cpu_priv *priv = policy->driver_data; + unsigned int pstate = policy->freq_table[index].driver_data; + + /* OPP level is a hardware ID, not the frequency-table array index. */ + if (pstate < priv->info->min_pstate || pstate > priv->info->max_pstate) + return -EINVAL; + + return apple_soc_cpufreq_set_pstate(priv, pstate); +} + /* * Lazy verification (lazy_verify SoCs) * @@ -508,6 +518,13 @@ static int apple_soc_cpufreq_request(struct cpufreq_policy *policy, * A failure stops all further requests, as for a request that is waited for, * and the timer reports it. * + * Waited-for requests (the initial frequency check, the static governors, + * suspend) never run alongside fast-switch requests: the core stops a + * governor that switches from the scheduler first. They cancel the timer, + * report a failure it had yet to report and replace a pending request, see + * apple_soc_cpufreq_drain(); suspend first writes the pending request, see + * apple_soc_cpufreq_suspend(). + * * Called with priv->lock held. */ static void apple_soc_cpufreq_fail(struct apple_cpu_priv *priv, u64 reg) @@ -670,27 +687,69 @@ static int apple_soc_cpufreq_lazy_request(struct cpufreq_policy *policy, return ret; } -static int apple_soc_cpufreq_set_target(struct cpufreq_policy *policy, - unsigned int index) +/* + * Stops lazy verification ahead of a waited-for request: cancels the verify + * timer, reports a failure it had yet to report, and drops the pending + * request, returning its P-state or -1. No fast-switch request runs + * concurrently to queue the timer again. + */ +static int apple_soc_cpufreq_drain(struct apple_cpu_priv *priv) { - struct apple_cpu_priv *priv = policy->driver_data; + unsigned int expected; unsigned long flags; - int ret; - - if (!priv->info->lazy_verify) - return apple_soc_cpufreq_request(policy, index); + bool report; + int pstate; + u64 reg; - /* - * Waited-for requests (suspend, the static governors) replace any - * pending one; the lock keeps a late fast-switch request out until - * this one is accepted. - */ hrtimer_cancel(&priv->verify_timer); + raw_spin_lock_irqsave(&priv->lock, flags); + pstate = priv->pending_pstate; priv->pending_pstate = -1; - ret = apple_soc_cpufreq_request(policy, index); + report = priv->failure_unreported; + priv->failure_unreported = false; + reg = priv->failure_command; + expected = priv->expected_pstate; raw_spin_unlock_irqrestore(&priv->lock, flags); - return ret; + + if (report) + apple_soc_cpufreq_report(priv, reg, expected); + return pstate; +} + +static int apple_soc_cpufreq_set_target(struct cpufreq_policy *policy, + unsigned int index) +{ + struct apple_cpu_priv *priv = policy->driver_data; + + /* A waited-for request replaces a pending one, see "Lazy verification". */ + if (priv->info->lazy_verify) + apple_soc_cpufreq_drain(priv); + + return apple_soc_cpufreq_request(policy, index); +} + +/* + * policy->cur holds the last fast-switch request, which on a lazy_verify SoC + * may still be pending, or lost to a failure that stopped requests. The core + * skips a suspend frequency equal to policy->cur, so settle the cluster + * first: write and verify the pending request, or report the failure. + */ +static int apple_soc_cpufreq_suspend(struct cpufreq_policy *policy) +{ + struct apple_cpu_priv *priv = policy->driver_data; + + if (priv->info->lazy_verify) { + int pstate = apple_soc_cpufreq_drain(priv); + int ret; + + ret = apple_soc_cpufreq_set_pstate(priv, pstate >= 0 ? pstate : + priv->expected_pstate); + if (ret) + return ret; + } + + return cpufreq_generic_suspend(policy); } static unsigned int apple_soc_cpufreq_fast_switch(struct cpufreq_policy *policy, @@ -1307,8 +1366,11 @@ static void apple_soc_cpufreq_exit(struct cpufreq_policy *policy) { struct apple_cpu_priv *priv = policy->driver_data; - /* The governor has stopped: no request re-arms the verify timer. */ - hrtimer_cancel(&priv->verify_timer); + /* + * The governor has stopped and the policy goes away: stop the verify + * timer, report a failure it had yet to report, drop a pending request. + */ + apple_soc_cpufreq_drain(priv); /* Drain firmware-limit updates before releasing the OPP tables. */ apple_fast_die_exit(priv); @@ -1356,7 +1418,7 @@ static struct cpufreq_driver apple_soc_cpufreq_driver = { .fast_switch = apple_soc_cpufreq_fast_switch, .register_em = cpufreq_register_em_with_opp, .set_boost = cpufreq_boost_set_sw, - .suspend = cpufreq_generic_suspend, + .suspend = apple_soc_cpufreq_suspend, }; static int __init apple_soc_cpufreq_module_init(void) From 9dadb83a1ea2663d04ca6a5c001f1416875b7d6d Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 11:44:03 +1100 Subject: [PATCH 118/388] drm/apple: give the desktop panel an ADT identity fallback Retain a validated real EDID from dictionary or chunked EDID setters when provided. Otherwise use panel identity from the reserved ADT, with the OF panel dimensions and a generic Apple panel identity if fields are absent. This removes the dependency on ProductAttributes arriving before an EDID can be published. Keep exact DRM modes from the DCP catalog. Read only bounded panel properties from the bootloader's reserved ADT. Hash panel serial bytes into the EDID numeric serial and never use the machine serial. Restrict the reader and fallback to desktop 26.6, leaving T8140 on its existing path. Cover malformed ADT, missing firmware identity, real EDID priority and invalid replacement retention in host tests/KUnit. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/Makefile | 2 +- drivers/gpu/drm/apple/dcp-internal.h | 5 + drivers/gpu/drm/apple/dcp-panel-adt.c | 143 ++++++++++++++++++ drivers/gpu/drm/apple/dcp-panel-adt.h | 15 ++ drivers/gpu/drm/apple/dcp.c | 110 ++++++++++++-- drivers/gpu/drm/apple/iomfb_template.c | 5 + drivers/gpu/drm/apple/iomfb_v26_6_desktop.c | 5 + drivers/gpu/drm/apple/parser-test.c | 13 ++ drivers/gpu/drm/apple/parser.c | 34 +++++ drivers/gpu/drm/apple/parser.h | 1 + .../drivers/gpu/apple-dcp/desktop.py | 84 +++++++++- .../selftests/drivers/gpu/apple-dcp/run.py | 1 + 12 files changed, 401 insertions(+), 17 deletions(-) create mode 100644 drivers/gpu/drm/apple/dcp-panel-adt.c create mode 100644 drivers/gpu/drm/apple/dcp-panel-adt.h diff --git a/drivers/gpu/drm/apple/Makefile b/drivers/gpu/drm/apple/Makefile index a592e776e99ab..48a3bb9009bc7 100644 --- a/drivers/gpu/drm/apple/Makefile +++ b/drivers/gpu/drm/apple/Makefile @@ -7,7 +7,7 @@ appledrm-y += afk.o dcp.o dcp-fabric.o dcp_backlight.o dptxep.o iomfb.o parser.o appledrm-y += dcp-hdmi.o appledrm-$(CONFIG_DRM_APPLE_AUDIO) += audio.o appledrm-$(CONFIG_DRM_APPLE_AUDIO) += av.o -appledrm-y += connector.o dcp-display-metadata.o dcp-180hz.o +appledrm-y += connector.o dcp-panel-adt.o dcp-display-metadata.o dcp-180hz.o appledrm-y += ibootep.o appledrm-y += dcpext_scanout.o appledrm-y += dcpext_drm.o diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index 37f6228dd7af9..7cdec3dbf0e52 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -245,9 +245,12 @@ struct dcp_desktop_state { bool mode_live; /* Identity supplied by this fixed panel, independent of optional color data. */ struct dcp_display_metadata panel_metadata; + struct dcp_display_metadata panel_adt_identity; u64 panel_metadata_generation; bool panel_metadata_valid; u32 identity_log_count; + u8 panel_raw_edid[4096]; + size_t panel_raw_edid_size; /* RX records an unsupported callback until the modeset RPC has drained. */ bool modeset_active; bool modeset_callback_error; @@ -897,4 +900,6 @@ static inline bool dcp_ext_dptx_607(const struct apple_dcp *dcp) void dcp_trace_panel_property(struct apple_dcp *dcp, int tag, const char *key, size_t key_len, const void *blob, size_t len); +void dcp_panel_edid_receive(struct apple_dcp *dcp, const void *blob, size_t len); + #endif /* __APPLE_DCP_INTERNAL_H__ */ diff --git a/drivers/gpu/drm/apple/dcp-panel-adt.c b/drivers/gpu/drm/apple/dcp-panel-adt.c new file mode 100644 index 0000000000000..9dbe34e0c2f2c --- /dev/null +++ b/drivers/gpu/drm/apple/dcp-panel-adt.c @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +/* + * Read-only Apple device tree decoding. The wire layout is documented by + * m1n1's public src/adt.h (GPL-2.0-or-later OR BSD-2-Clause). No firmware + * implementation or captured device identifiers are used here. + */ +#include +#include +#include +#include +#include + +#include "dcp-panel-adt.h" + +struct dcp_adt_cursor { + const u8 *data; + size_t pos, size; + u32 nodes; +}; + +static const u8 *dcp_adt_bytes(struct dcp_adt_cursor *c, size_t size) +{ + const u8 *p; + + if (c->pos > c->size || size > c->size - c->pos) + return ERR_PTR(-EINVAL); + p = c->data + c->pos; + c->pos += size; + return p; +} + +/* Hash panel serial bytes into EDID's numeric serial; never log the input. */ +static u32 dcp_adt_serial(const u8 *data, size_t size) +{ + u32 serial = 2166136261U; + + for (size_t i = 0; i < size; i++) + serial = (serial ^ data[i]) * 16777619U; + return serial ?: 1; +} + +static void dcp_adt_panel_property(struct dcp_panel_adt *panel, const char *name, + const u8 *value, u32 size) +{ + struct dcp_display_metadata *id = &panel->identity; + u32 number = size == 4 ? get_unaligned_le32(value) : 0; + + if ((!strcmp(name, "vendor-id") || !strcmp(name, "display-vendor-id")) && + size == 4 && number <= 0x7fff) { + char manufacturer[4] = { (number >> 10 & 31) + '@', + (number >> 5 & 31) + '@', (number & 31) + '@', 0 }; + + if (manufacturer[0] >= 'A' && manufacturer[0] <= 'Z' && + manufacturer[1] >= 'A' && manufacturer[1] <= 'Z' && + manufacturer[2] >= 'A' && manufacturer[2] <= 'Z') + memcpy(id->manufacturer, manufacturer, sizeof(manufacturer)); + } else if ((!strcmp(name, "product-id") || !strcmp(name, "display-product-id") || + !strcmp(name, "panel-id")) && size == 4) { + id->product = number & U16_MAX; + } else if ((!strcmp(name, "serial-number") || + !strcmp(name, "raw-panel-serial-number")) && size && size <= 256) { + id->serial = dcp_adt_serial(value, size); + } else if (!strcmp(name, "display-serial-number") && size == 4) { + id->serial = number; + } else if (!strcmp(name, "width-mm") && size == 4 && number && number <= 2550) { + panel->width_mm = number; + } else if (!strcmp(name, "height-mm") && size == 4 && number && number <= 2550) { + panel->height_mm = number; + } +} + +static int dcp_adt_node(struct dcp_adt_cursor *c, struct dcp_panel_adt *panel, + u32 depth, bool product_parent) +{ + const u8 *header = dcp_adt_bytes(c, 8); + size_t props_start, props_end; + u32 props, children; + char node_name[33] = {}; + bool product, selected; + + if (IS_ERR(header)) + return PTR_ERR(header); + if (depth > 32 || ++c->nodes > 4096) + return -E2BIG; + props = get_unaligned_le32(header); + children = get_unaligned_le32(header + 4); + if (props > 4096 || children > 4096) + return -E2BIG; + props_start = c->pos; + /* Names need not be the first property. Validate every extent first. */ + for (u32 i = 0; i < props; i++) { + const u8 *prop = dcp_adt_bytes(c, 36), *value; + u32 size; + + if (IS_ERR(prop) || !memchr(prop, 0, 32)) + return -EINVAL; + size = get_unaligned_le32(prop + 32) & 0x7fffffff; + value = dcp_adt_bytes(c, ALIGN((size_t)size, 4)); + if (IS_ERR(value)) + return PTR_ERR(value); + if (!strcmp((const char *)prop, "name") && size && size <= 32 && + memchr(value, 0, size)) + memcpy(node_name, value, size); + } + props_end = c->pos; + product = depth == 1 && !strcmp(node_name, "product"); + selected = (depth == 1 && !strcmp(node_name, "panel")) || + (product_parent && !strcmp(node_name, "panel")); + /* /product carries raw-panel-serial-number on machines without /panel. */ + c->pos = props_start; + for (u32 i = 0; i < props; i++) { + const u8 *prop = c->data + c->pos; + u32 size = get_unaligned_le32(prop + 32) & 0x7fffffff; + const char *name = (const char *)prop; + + if (selected || (product && !strcmp(name, "raw-panel-serial-number"))) + dcp_adt_panel_property(panel, name, prop + 36, size); + c->pos += 36 + ALIGN((size_t)size, 4); + } + panel->found |= selected; + c->pos = props_end; + for (u32 i = 0; i < children; i++) { + int ret = dcp_adt_node(c, panel, depth + 1, product); + + if (ret) + return ret; + } + return 0; +} + +int dcp_panel_adt_parse(const void *data, size_t size, struct dcp_panel_adt *panel) +{ + struct dcp_adt_cursor cursor = { .data = data, .size = size }; + struct dcp_panel_adt parsed = *panel; + int ret; + + if (!data || size > SZ_16M) + return -EINVAL; + ret = dcp_adt_node(&cursor, &parsed, 0, false); + if (!ret) + *panel = parsed; + return ret; +} diff --git a/drivers/gpu/drm/apple/dcp-panel-adt.h b/drivers/gpu/drm/apple/dcp-panel-adt.h new file mode 100644 index 0000000000000..2d724adb7d0e1 --- /dev/null +++ b/drivers/gpu/drm/apple/dcp-panel-adt.h @@ -0,0 +1,15 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ +#ifndef APPLE_DCP_PANEL_ADT_H +#define APPLE_DCP_PANEL_ADT_H + +#include "dcp-display-metadata.h" + +/* Only panel properties, never the machine or account identity. */ +struct dcp_panel_adt { + struct dcp_display_metadata identity; + u32 width_mm, height_mm; + bool found; +}; + +int dcp_panel_adt_parse(const void *data, size_t size, struct dcp_panel_adt *panel); +#endif diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 239d7506183b8..b4eb0511e82ce 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -51,6 +51,10 @@ #include "afk.h" #include "av.h" #include "dcp.h" +#include "dcp-panel-adt.h" +#include +#include +#include #include "dcp-fabric-effects.h" #include "dcp-fabric.h" #include "dcp-hdmi.h" @@ -776,6 +780,48 @@ const struct drm_edid *dcp_firmware_edid(struct apple_dcp *dcp, u64 *generation) return edid; } +/* + * m1n1 reserves the original ADT as a phram node labelled "adt". Read it + * without an MTD dependency. Only the desktop panel calls this; other DCP + * profiles retain their established identity and brightness paths. + */ +static void dcp_panel_adt_identity(struct apple_dcp *dcp) +{ + struct dcp_panel_adt panel = { + .identity = { .manufacturer = "APP", .product = 0x0616 }, + .width_mm = dcp->panel.width_mm, + .height_mm = dcp->panel.height_mm, + }; + struct device_node *np; + const char *label; + struct resource res; + void *data; + int ret = -ENODATA; + + /* Explicit generic label when no serial or product is provided. */ + strscpy(panel.identity.name, "Apple Panel", sizeof(panel.identity.name)); + for_each_compatible_node(np, NULL, "phram") { + if (of_property_read_string(np, "label", &label) || strcmp(label, "adt")) + continue; + if (of_address_to_resource(np, 0, &res) || + !resource_size(&res) || resource_size(&res) > SZ_16M) + break; + data = memremap(res.start, resource_size(&res), MEMREMAP_WB); + if (!data) + break; + ret = dcp_panel_adt_parse(data, resource_size(&res), &panel); + memunmap(data); + break; + } + of_node_put(np); + /* The board's OF panel dimensions remain valid if ADT has none. */ + dcp->panel.width_mm = panel.width_mm; + dcp->panel.height_mm = panel.height_mm; + dcp->desktop->panel_adt_identity = panel.identity; + dev_info(dcp->dev, "panel identity fallback: ADT %s, OF dimensions %ux%u mm\n", + ret ? "unavailable" : "read", panel.width_mm, panel.height_mm); +} + /* Internal panel identity has no detachable Type-C service or EDID transport. */ void dcp_panel_metadata_replace(struct apple_dcp *dcp, const struct dcp_display_metadata *metadata) @@ -819,14 +865,46 @@ void dcp_panel_metadata_replace(struct apple_dcp *dcp, dcp_queue_hotplug(dcp->connector); } +/* Preserve a validated real EDID ahead of any metadata-derived fallback. */ +void dcp_panel_edid_receive(struct apple_dcp *dcp, const void *blob, size_t len) +{ + const struct drm_edid *edid; + const u8 *raw; + size_t size; + unsigned long flags; + bool changed; + + if (dcp->fw_compat != DCP_FIRMWARE_V_26_6_DESKTOP || !dcp_has_panel(dcp) || + parse_panel_edid_data(blob, len, &raw, &size)) + return; + edid = drm_edid_alloc(raw, size); + if (!edid) + return; + if (!drm_edid_valid(edid)) { + drm_edid_free(edid); + return; + } + spin_lock_irqsave(&dcp->metadata.lock, flags); + changed = size != dcp->desktop->panel_raw_edid_size || + memcmp(raw, dcp->desktop->panel_raw_edid, size); + if (changed) { + memcpy(dcp->desktop->panel_raw_edid, raw, size); + dcp->desktop->panel_raw_edid_size = size; + dcp->desktop->panel_metadata_generation++; + } + spin_unlock_irqrestore(&dcp->metadata.lock, flags); + drm_edid_free(edid); + if (changed && dcp->connector) + dcp_queue_hotplug(dcp->connector); +} + bool dcp_panel_metadata_current(struct apple_dcp *dcp, u64 generation) { unsigned long flags; bool valid; spin_lock_irqsave(&dcp->metadata.lock, flags); - valid = dcp->desktop->panel_metadata_valid && - generation == dcp->desktop->panel_metadata_generation; + valid = generation == dcp->desktop->panel_metadata_generation; spin_unlock_irqrestore(&dcp->metadata.lock, flags); return valid; } @@ -837,21 +915,27 @@ const struct drm_edid *dcp_panel_edid_snapshot(struct apple_dcp *dcp, { struct dcp_display_metadata metadata; const struct drm_edid *edid; + u8 *raw; + size_t size; unsigned long flags; - bool valid; + raw = kmalloc(sizeof(dcp->desktop->panel_raw_edid), GFP_KERNEL); + if (!raw) + return ERR_PTR(-ENOMEM); spin_lock_irqsave(&dcp->metadata.lock, flags); - valid = dcp->desktop->panel_metadata_valid; - if (valid) { - metadata = dcp->desktop->panel_metadata; - *generation = dcp->desktop->panel_metadata_generation; - } + size = dcp->desktop->panel_raw_edid_size; + memcpy(raw, dcp->desktop->panel_raw_edid, size); + metadata = dcp->desktop->panel_metadata_valid ? + dcp->desktop->panel_metadata : dcp->desktop->panel_adt_identity; + *generation = dcp->desktop->panel_metadata_generation; spin_unlock_irqrestore(&dcp->metadata.lock, flags); - if (!valid) - return ERR_PTR(-ENODATA); /* The caller supplies the same retained catalog it is publishing. */ - edid = dcp_panel_edid(&metadata, modes, count, - dcp->panel.width_mm, dcp->panel.height_mm); + edid = size ? drm_edid_alloc(raw, size) : + dcp_panel_edid(&metadata, modes, count, + dcp->panel.width_mm, dcp->panel.height_mm); + kfree(raw); + if (!edid) + return ERR_PTR(-ENOMEM); if (!IS_ERR(edid) && !dcp_panel_metadata_current(dcp, *generation)) { drm_edid_free(edid); return ERR_PTR(-ESTALE); @@ -4823,6 +4907,8 @@ static int dcp_comp_bind_pipe(struct device *dev, struct device *main, void *dat of_property_read_u32(panel_np, height_prop[!dcp->notch_height], &dcp->panel.height_mm); + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) + dcp_panel_adt_identity(dcp); of_node_put(panel_np); dcp->fixed_connector_type = DRM_MODE_CONNECTOR_eDP; dcp->connector_type = DRM_MODE_CONNECTOR_eDP; diff --git a/drivers/gpu/drm/apple/iomfb_template.c b/drivers/gpu/drm/apple/iomfb_template.c index e34af2a47e868..ce638d142171e 100644 --- a/drivers/gpu/drm/apple/iomfb_template.c +++ b/drivers/gpu/drm/apple/iomfb_template.c @@ -1267,6 +1267,11 @@ static bool dcpep_process_chunks(struct apple_dcp *dcp, dcp_trace_panel_property(dcp, 0, req->key, strnlen(req->key, sizeof(req->key)), dcp->chunks.data, dcp->chunks.length); + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && + (!strcmp(req->key, "EDID") || !strcmp(req->key, "edid"))) { + dcp_panel_edid_receive(dcp, dcp->chunks.data, dcp->chunks.length); + return true; + } ret = parse(dcp->chunks.data, dcp->chunks.length, &ctx); if (ret) { diff --git a/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c b/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c index bd8eae4ee6716..8068e0c25e395 100644 --- a/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c +++ b/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c @@ -110,6 +110,11 @@ static bool trampoline_panel_dictionary_v26_6(struct apple_dcp *dcp, int tag, if (!dcp_has_panel(dcp) || req->value_null || !memchr(key, 0, sizeof(req->key))) goto done; + /* A real EDID blob, when supplied, takes precedence over product data. */ + if (!strcmp(key, "EDID") || !strcmp(key, "edid")) { + dcp_panel_edid_receive(dcp, req->value, sizeof(req->value)); + goto done; + } if (strcmp(key, "DisplayAttributes") && strcmp(key, "ProductAttributes")) goto done; ret = parse(req->value, sizeof(req->value), &ctx); diff --git a/drivers/gpu/drm/apple/parser-test.c b/drivers/gpu/drm/apple/parser-test.c index 108a7ca67b2ef..d96b29c43ea0a 100644 --- a/drivers/gpu/drm/apple/parser-test.c +++ b/drivers/gpu/drm/apple/parser-test.c @@ -14,6 +14,7 @@ #include "dcp-property.h" #include "iomfb_internal.h" #include "parser.h" +#include "dcp-panel-adt.h" static struct device parser_device = { .init_name = "apple-display-parser-test", @@ -1413,7 +1414,19 @@ static void parser_identity_bounds(struct kunit *test) parser_identity_emit, &seen), 0); } +static void parser_adt_bounds(struct kunit *test) +{ + struct dcp_panel_adt panel = { .identity = { .manufacturer = "APP" } }; + struct dcp_panel_adt before = panel; + __le32 huge[] = { cpu_to_le32(4097), 0 }; + + KUNIT_EXPECT_EQ(test, dcp_panel_adt_parse(huge, sizeof(huge), &panel), -E2BIG); + KUNIT_EXPECT_MEMEQ(test, &panel, &before, sizeof(panel)); + KUNIT_EXPECT_EQ(test, dcp_panel_adt_parse(NULL, 0, &panel), -EINVAL); +} + static struct kunit_case parser_cases[] = { + KUNIT_CASE(parser_adt_bounds), KUNIT_CASE(parser_identity_bounds), KUNIT_CASE(parser_panel_identity_stable), KUNIT_CASE(parser_panel_identity), diff --git a/drivers/gpu/drm/apple/parser.c b/drivers/gpu/drm/apple/parser.c index 54b9606a4f632..0d4e85363e7d2 100644 --- a/drivers/gpu/drm/apple/parser.c +++ b/drivers/gpu/drm/apple/parser.c @@ -2061,3 +2061,37 @@ int parse_identity_keys(const void *blob, size_t len, dcp_identity_key_fn emit, return ret ?: parse_identity_tree(&ctx, emit, data, 0, &budget); } + +/* A setter may deliver the EDID bytes directly or as an OSSerialize blob. */ +int parse_panel_edid_data(const void *blob, size_t len, const u8 **edid, size_t *size) +{ + struct dcp_parse_ctx ctx; + const struct dcp_parse_tag *tag; + const u8 *data = blob; + u32 bytes; + int ret; + + if (len >= 128 && !memcmp(data, "\x00\xff\xff\xff\xff\xff\xff\x00", 8)) { + bytes = (1U + data[126]) * 128; + if (bytes > len || bytes > 4096) + return -EINVAL; + *edid = data; + *size = bytes; + return 0; + } + ret = parse(blob, len, &ctx); + if (ret) + return ret; + tag = parse_tag_of_type(&ctx, DCP_TYPE_BLOB); + if (IS_ERR(tag) || tag->size < 128 || tag->size > 4096 || tag->size % 128) + return -EINVAL; + data = parse_bytes(&ctx, tag->size); + if (IS_ERR(data)) + return PTR_ERR(data); + if (memcmp(data, "\x00\xff\xff\xff\xff\xff\xff\x00", 8) || + (1U + data[126]) * 128 != tag->size) + return -EINVAL; + *edid = data; + *size = tag->size; + return 0; +} diff --git a/drivers/gpu/drm/apple/parser.h b/drivers/gpu/drm/apple/parser.h index 9a81c0d15ac1b..9b419ce83621b 100644 --- a/drivers/gpu/drm/apple/parser.h +++ b/drivers/gpu/drm/apple/parser.h @@ -11,6 +11,7 @@ #include "dcp-metadata-epoch.h" struct apple_dcp; +int parse_panel_edid_data(const void *blob, size_t len, const u8 **edid, size_t *size); typedef void (*dcp_identity_key_fn)(void *data, const char *key, u32 key_len, u32 type, u32 size, u32 depth); int parse_identity_keys(const void *blob, size_t len, dcp_identity_key_fn emit, void *data); diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py index 9ae0c1808a53a..f45784339437d 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py @@ -48,6 +48,9 @@ def modeset_unmap(root): #undef static_assert #define static_assert(c) _Static_assert(c,#c) #define trace_iomfb_callback(...) ((void)0) +#define dcp_trace_panel_property(...) ((void)0) +#define GFP_KERNEL 0 +#define kmalloc(n,f) malloc(n) #define dev_warn_dma(...) ((void)0) struct dcp_map_physical_req {u64 paddr,size;u32 flags;u8 dva_null,dva_size_null,padding[2];} __attribute__((packed)); struct dcp_map_physical_resp {u64 dva,dva_size;u32 mem_desc_id;} __attribute__((packed)); @@ -374,10 +377,11 @@ def panel_identity(root, output=None): f['edid_header'] + f['parser'] + f['stubs'] + f['builder']) source = (root / 'drivers/gpu/drm/apple/dcp.c').read_text() helpers = '\n'.join(block(source, name) for name in ( - 'void dcp_panel_metadata_replace(', 'bool dcp_panel_metadata_current(', + 'void dcp_panel_metadata_replace(', 'void dcp_panel_edid_receive(', 'bool dcp_panel_metadata_current(', 'const struct drm_edid *dcp_panel_edid_snapshot(')) backend = (root / 'drivers/gpu/drm/apple/iomfb_v26_6_desktop.c').read_text() - dictionary = block(backend, 'struct dcp_dict_passthrough_req_v26_6 {')+';\n' + dictionary = block((root/'drivers/gpu/drm/apple/parser.c').read_text(), 'int parse_panel_edid_data(') + dictionary += block(backend, 'struct dcp_dict_passthrough_req_v26_6 {')+';\n' dictionary += block(backend, 'static bool trampoline_panel_dictionary_v26_6(') dictionary += block(backend, 'bool iomfb_check_callback_v26_6_desktop(') bindings = '\n'.join(line for line in block(backend, 'static const iomfb_cb_handler cb_handlers[').splitlines() @@ -385,11 +389,14 @@ def panel_identity(root, output=None): unit = code + r''' #define DCP_FIRMWARE_V_26_6_DESKTOP 5 #define trace_iomfb_callback(...) ((void)0) +#define dcp_trace_panel_property(...) ((void)0) +#define GFP_KERNEL 0 +#define kmalloc(n,f) malloc(n) #define spin_lock_irqsave(p,f) do {assert(!*(p));*(p)=true;(f)=0;} while(0) #define spin_unlock_irqrestore(p,f) do {assert(*(p));*(p)=false;(void)(f);} while(0) struct apple_dcp { int fw_compat;bool has_panel;struct {bool lock;} metadata; - struct {struct dcp_display_metadata panel_metadata;u64 panel_metadata_generation;bool panel_metadata_valid;} *desktop; + struct {struct dcp_display_metadata panel_metadata, panel_adt_identity;u64 panel_metadata_generation;bool panel_metadata_valid;u8 panel_raw_edid[4096];size_t panel_raw_edid_size;} *desktop; struct {u32 width_mm,height_mm;} panel;void *connector; }; static unsigned notifications; @@ -414,7 +421,9 @@ def panel_identity(root, output=None): .flags=DRM_MODE_FLAG_PHSYNC|DRM_MODE_FLAG_PVSYNC,.type=i==0?DRM_MODE_TYPE_PREFERRED:0}; modes[0].vrr=true;modes[0].min_vrr=24U<<16;modes[0].max_vrr=120U<<16; u64 generation=99; - assert(IS_ERR(dcp_panel_edid_snapshot(&d,modes,3,&generation))); + desktop.panel_adt_identity=(struct dcp_display_metadata){.manufacturer="APP",.product=0x616}; + const struct drm_edid *fallback=dcp_panel_edid_snapshot(&d,modes,3,&generation); + assert(!IS_ERR(fallback) && drm_edid_valid(fallback));drm_edid_free(fallback); bool (*handlers[600])(struct apple_dcp *,int,void *,void *)={ ''' + bindings + r''' }; @@ -466,6 +475,17 @@ def panel_identity(root, output=None): assert(bytes[108+4] & DRM_EDID_RANGE_OFFSET_MAX_HFREQ); /* The encoded timing clock rounds to 10 kHz; the DCP catalog is unchanged. */ assert(modes[1].mode.clock==540005 && modes[0].mode.clock==1080005); + /* A real EDID replaces the fallback; invalid replacement preserves it. */ + u8 real[128];memcpy(real,bytes,128);real[12]^=1;real[127]^=1; + dcp_panel_edid_receive(&d,real,sizeof(real)); + assert(desktop.panel_raw_edid_size==128); + generation=desktop.panel_metadata_generation; + const struct drm_edid *copied=dcp_panel_edid_snapshot(&d,modes,3,&generation); + assert(!IS_ERR(copied) && !memcmp(&copied->raw,real,128));drm_edid_free(copied); + unsigned previous=notifications; + real[127]^=1;dcp_panel_edid_receive(&d,real,sizeof(real)); + assert(notifications==previous); + desktop.panel_raw_edid_size=0;notifications=1; drm_edid_free(edid); /* Repeated and partial dictionaries cannot trigger monitor/brightness restore loops. */ for(unsigned repeat=0;repeat<32;repeat++) { @@ -666,3 +686,59 @@ def identity_keys(root): return 0; } ''' + + +def adt_identity(root): + import struct + def node(props, children=()): + out=bytearray(struct.pack(' +#include +#include +#include +#include +#include +typedef uint8_t u8;typedef uint32_t u32; +#define U16_MAX UINT16_MAX +#define SZ_16M (16U<<20) +#define ALIGN(x,n) (((x)+(n)-1)&~((n)-1)) +#define ERR_PTR(x) ((void *)(intptr_t)(x)) +#define PTR_ERR(x) ((int)(intptr_t)(x)) +#define IS_ERR(x) ((uintptr_t)(x)>(uintptr_t)-4096) +static u32 get_unaligned_le32(const void *p) {u32 n;memcpy(&n,p,4);return n;} +''' + code+=block((base/'dcp-display-metadata.h').read_text(),'struct dcp_display_metadata {')+';\n' + code+=block(header,'struct dcp_panel_adt {')+';\n'+source + code+='static const u8 fixture[]={'+','.join(map(str,data))+'};\n' + return code+r''' +int main(void) +{ + struct dcp_panel_adt panel={.identity={.manufacturer="APP",.product=0x616}}; + assert(!dcp_panel_adt_parse(fixture,sizeof(fixture),&panel)); + assert(panel.found && panel.width_mm==345 && panel.height_mm==223); + assert(!strcmp(panel.identity.manufacturer,"TST") && panel.identity.product==42); + assert(panel.identity.serial && panel.identity.serial==dcp_adt_serial((const u8 *)"SYNTHETIC-PANEL",15)); + for(unsigned n=0;n Date: Mon, 26 Oct 2026 11:44:32 +1100 Subject: [PATCH 119/388] drm/apple: notify userspace after desktop brightness changes Emit the backlight core's change uevent and notify brightness and actual_brightness after a completed desktop present changes the level, or when takeover updates the inherited level. Defer events outside the receive path's locks and drain notification work during shutdown. Do not use backlight_force_update for a fade: it would overwrite the requested target with the intermediate actual level. Keep the T8140 path unchanged. Host tests exercise accepted, rejected, duplicate and out-of-order completions, teardown gating and preservation of the target. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/dcp-internal.h | 1 + drivers/gpu/drm/apple/dcp_backlight.c | 34 +++++++++- .../drivers/gpu/apple-dcp/desktop.py | 66 +++++++++++++++++++ .../selftests/drivers/gpu/apple-dcp/run.py | 1 + 4 files changed, 101 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index 7cdec3dbf0e52..f20ed0ce3d14a 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -700,6 +700,7 @@ struct apple_dcp { void (*kick)(struct apple_dcp *dcp); /* Steps a running fade; fade_start is its start in jiffies. */ struct delayed_work fade_work; + struct work_struct notify_work; struct delayed_work retry_work; unsigned long fade_start; /* Rate limit for firmware re-send requests. */ diff --git a/drivers/gpu/drm/apple/dcp_backlight.c b/drivers/gpu/drm/apple/dcp_backlight.c index 7d884e062f363..180a97b685fd3 100644 --- a/drivers/gpu/drm/apple/dcp_backlight.c +++ b/drivers/gpu/drm/apple/dcp_backlight.c @@ -113,6 +113,30 @@ static void dcp_backlight_desktop_kick(struct apple_dcp *dcp) schedule_work(&dcp->bl_update_wq); } +/* + * Match the backlight core's change event without force_update(), which + * would replace a user's target with an intermediate fade level. + */ +static void dcp_backlight_notify_work(struct work_struct *work) +{ + struct apple_dcp *dcp = container_of(work, struct apple_dcp, backlight.notify_work); + struct backlight_device *bd = READ_ONCE(dcp->brightness.bl_dev); + char *envp[] = { "SOURCE=unknown", NULL }; + + if (!bd || READ_ONCE(dcp->quiescing)) + return; + kobject_uevent_env(&bd->dev.kobj, KOBJ_CHANGE, envp); + sysfs_notify(&bd->dev.kobj, NULL, "brightness"); + sysfs_notify(&bd->dev.kobj, NULL, "actual_brightness"); +} + +static void dcp_backlight_notify(struct apple_dcp *dcp) +{ + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && + !READ_ONCE(dcp->quiescing) && READ_ONCE(dcp->brightness.bl_dev)) + schedule_work(&dcp->backlight.notify_work); +} + /* A loader hint describes the inherited level, not a new brightness request. */ static void dcp_backlight_sync_hint(struct apple_dcp *dcp) { @@ -128,6 +152,7 @@ static void dcp_backlight_sync_hint(struct apple_dcp *dcp) bd->props.brightness = dcp->backlight.state.target; spin_unlock_irqrestore(&dcp->backlight.lock, flags); mutex_unlock(&bd->ops_lock); + dcp_backlight_notify(dcp); } int dcp_backlight_takeover(struct apple_dcp *dcp, u32 millinits) @@ -304,6 +329,7 @@ void dcp_backlight_stop(struct apple_dcp *dcp) { cancel_delayed_work_sync(&dcp->backlight.fade_work); cancel_delayed_work_sync(&dcp->backlight.retry_work); + cancel_work_sync(&dcp->backlight.notify_work); } static void dcp_backlight_release(void *data) @@ -320,6 +346,7 @@ int dcp_backlight_init(struct apple_dcp *dcp, struct device *dev) { spin_lock_init(&dcp->backlight.lock); INIT_DELAYED_WORK(&dcp->backlight.fade_work, dcp_backlight_fade_work); + INIT_WORK(&dcp->backlight.notify_work, dcp_backlight_notify_work); INIT_DELAYED_WORK(&dcp->backlight.retry_work, dcp_backlight_retry_work); return devm_add_action_or_reset(dev, dcp_backlight_release, dcp); } @@ -464,16 +491,21 @@ void dcp_backlight_desktop_finish(struct apple_dcp *dcp, u32 id, { unsigned long flags; unsigned int delay; - bool finished, pending; + bool finished, pending, changed; + int previous; if (!dcp_backlight_desktop(dcp)) return; spin_lock_irqsave(&dcp->backlight.lock, flags); + previous = dcp_bl_actual(&dcp->backlight.state); finished = dcp_bl_swap_finish(&dcp->backlight.state, &dcp->desktop->backlight, id, reply, accepted); + changed = previous != dcp_bl_actual(&dcp->backlight.state); delay = dcp_bl_retry_delay(&dcp->backlight.state); pending = dcp->backlight.state.dirty; spin_unlock_irqrestore(&dcp->backlight.lock, flags); + if (changed) + dcp_backlight_notify(dcp); if (!finished || READ_ONCE(dcp->quiescing) || READ_ONCE(dcp->crashed)) return; if (delay) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py index f45784339437d..bccf57dcdb209 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py @@ -208,6 +208,7 @@ def brightness_takeover(root): 'static bool dcp_backlight_controls_device(')) return prefix + r''' #include +#include #define READ_ONCE(x) (x) #define DCP_FIRMWARE_V_26_6_DESKTOP 5 #define DCP_FIRMWARE_H17P 1 @@ -256,6 +257,8 @@ def brightness_takeover(root): static void *bl_get_data(struct backlight_device *bd) {return bd->data;} static bool iomfb_is_m4(struct apple_dcp *d) {(void)d;return false;} bool dcp_backlight_active(struct apple_dcp *d) {(void)d;return false;} +static unsigned notify_count; +static void dcp_backlight_notify(struct apple_dcp *d) {(void)d;notify_count++;} static void dcp_backlight_desktop_kick(struct apple_dcp *d) {(void)d;} static void dcp_backlight_kick(struct apple_dcp *d) {(void)d;} static unsigned fades; @@ -742,3 +745,66 @@ def node(props, children=()): return 0; } ''' + + +def brightness_notifications(root): + import display + prefix=display.backlight(root).split('static void completed(')[0] + prefix=prefix.replace('static inline ', 'static inline __attribute__((unused)) ') + source=(root/'drivers/gpu/drm/apple/dcp_backlight.c').read_text() + functions='\n'.join(block(source,name) for name in ( + 'static void dcp_backlight_notify_work(', 'static void dcp_backlight_notify(', + 'static bool dcp_backlight_desktop(', 'void dcp_backlight_desktop_finish(')) + return prefix+r''' +#include +#include +#define READ_ONCE(x) (x) +#define DCP_FIRMWARE_V_26_6_DESKTOP 5 +#define spin_lock_irqsave(p,f) do {assert(!*(p));*(p)=true;(f)=0;} while(0) +#define spin_unlock_irqrestore(p,f) do {assert(*(p));*(p)=false;(void)(f);} while(0) +#define container_of(p,t,m) ((t *)((char *)(p)-offsetof(t,m))) +#define KOBJ_CHANGE 1 +#define system_dfl_wq NULL +#define msecs_to_jiffies(x) (x) +struct work_struct {int unused;}; +struct backlight_device {struct {int kobj;} dev;struct {u32 brightness;} props;}; +struct apple_dcp { + int fw_compat;bool quiescing,crashed; + struct {struct backlight_device *bl_dev;} brightness; + struct {bool lock;struct dcp_backlight_state state;struct work_struct notify_work;int retry_work;} backlight; + struct {struct dcp_backlight_swap backlight;} *desktop; +}; +static unsigned events,brightness,actual,kicks; +static void kobject_uevent_env(int *k,int action,char **env) +{(void)k;assert(action==1 && !strcmp(env[0],"SOURCE=unknown") && !env[1]);events++;} +static void sysfs_notify(int *k,void *dir,const char *name) +{(void)k;assert(!dir);if(!strcmp(name,"brightness"))brightness++;else {assert(!strcmp(name,"actual_brightness"));actual++;}} +static void dcp_backlight_notify_work(struct work_struct *); +static void schedule_work(struct work_struct *w) {dcp_backlight_notify_work(w);} +static void dcp_backlight_kick(struct apple_dcp *d) {assert(!d->backlight.lock);kicks++;} +static void mod_delayed_work(void *q,int *w,unsigned delay) {(void)q;(void)w;assert(delay);} +'''+functions+r''' +int main(void) +{ + struct backlight_device bd={.props={500}}; + struct apple_dcp d={.fw_compat=5,.brightness={&bd}}; + __typeof__(*d.desktop) desktop={0};d.desktop=&desktop; + assert(!dcp_bl_init(&d.backlight.state,600,true,100,false,0)); + assert(!dcp_bl_request(&d.backlight.state,500,false,false)); + u32 target=d.backlight.state.target; + /* Accepted/completed presents update userspace once, duplicate callbacks do not. */ + assert(!dcp_bl_swap_prepare(&d.backlight.state,&desktop.backlight,7,true)); + dcp_backlight_desktop_finish(&d,8,false,true);assert(!events); + dcp_backlight_desktop_finish(&d,7,true,true);assert(!events); + dcp_backlight_desktop_finish(&d,7,false,true);assert(events==1 && brightness==1 && actual==1); + dcp_backlight_desktop_finish(&d,7,false,true);assert(events==1); + assert(d.backlight.state.target==target && bd.props.brightness==500); + assert(!dcp_bl_request(&d.backlight.state,250,false,false)); + assert(!dcp_bl_swap_prepare(&d.backlight.state,&desktop.backlight,9,true)); + dcp_backlight_desktop_finish(&d,9,true,false);assert(events==1); + d.quiescing=true;dcp_backlight_notify(&d);assert(events==1); + d.quiescing=false;d.fw_compat=1;dcp_backlight_notify(&d);assert(events==1); + d.fw_compat=5;d.brightness.bl_dev=NULL;dcp_backlight_notify(&d);assert(events==1); + return 0; +} +''' diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/run.py b/tools/testing/selftests/drivers/gpu/apple-dcp/run.py index cb9f79f41c939..3c26c17ad916f 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/run.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/run.py @@ -60,6 +60,7 @@ def main(): ('display', 'panel-brightness-commit', display.brightness_commit, ((),), ()), ('display', 'panel-brightness-takeover', display.takeover, ((),), ()), ('display', 'panel-mode-catalog', display.modes, ((),), ()), + ('display', 'desktop-brightness-notifications', desktop.brightness_notifications, ((),), ()), ('display', 'desktop-adt-identity', desktop.adt_identity, ((),), ()), ('display', 'desktop-identity-keys', desktop.identity_keys, ((),), ('-Wno-unused-function',)), ('display', 'desktop-panel-identity', lambda r: desktop.panel_identity(r, output), ((),), ()), From 7842bc73ff2a5b7e002a9e7ec0f186fb394873c5 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 11:46:28 +1100 Subject: [PATCH 120/388] docs: describe desktop panel identity sources and change events Document the bounded diagnostics, exact ADT property mappings, generic fallback and target-preserving backlight notifications. Assisted-by: LLM Signed-off-by: Ryan Murray --- Documentation/gpu/apple-dcp-t6041-panel.rst | 50 +++++++++++++++++++ drivers/gpu/drm/apple/iomfb.c | 2 +- .../selftests/drivers/gpu/apple-dcp/README | 8 +++ 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/Documentation/gpu/apple-dcp-t6041-panel.rst b/Documentation/gpu/apple-dcp-t6041-panel.rst index 01a03c0f2777d..bc7bd3ac4305e 100644 --- a/Documentation/gpu/apple-dcp-t6041-panel.rst +++ b/Documentation/gpu/apple-dcp-t6041-panel.rst @@ -72,3 +72,53 @@ setters. A legacy PNP manufacturer code is decoded when the text code is absent. Missing optional labels are represented by the firmware vendor/product code and unspecified numeric serial, without inventing a per-device identity. Partial updates for the same vendor/product retain known optional identity fields. + +Desktop identity diagnostics and fallback +----------------------------------------- + +Enable ``appledrm.panel_identity_debug=1`` for a diagnostic boot (or set the +module's ``panel_identity_debug`` parameter before a modeset). D552, D561, +D114 and chunked property delivery report the outer key, serialized key names, +value type numbers and lengths. D000 identifies chunked delivery. Type numbers +are dictionary 1, array 2, integer 4, string 9, blob 10 and boolean 11. Values +are never printed. Each property walk stops at depth 4 or 64 objects, and the +DCP stops after 256 key records, with bounded framing/status lines in addition. +The switch defaults off. Malformed serialization only stops inspection. + +A valid real EDID delivered under ``EDID`` or ``edid`` has priority. Without +one, ProductAttributes remain usable, but are no longer a prerequisite for +publishing an identity. The fallback reads the original ADT from m1n1's +reserved-memory ``phram`` node with ``label = "adt"``. The reader accepts at +most 16 MiB, 4096 nodes, 4096 properties per node and 32 levels, and validates +all extents before committing any identity data. + +The supported ADT identity properties are as follows. These are property +mappings, not a claim that a particular firmware supplies every property. + +* ``/panel`` or ``/product/panel`` ``vendor-id`` or ``display-vendor-id`` is a + little-endian 32-bit packed PNP manufacturer code. +* ``product-id``, ``display-product-id`` or ``panel-id`` supplies the low 16 + bits of the EDID product code. +* ``display-serial-number`` supplies a little-endian numeric serial. + ``serial-number`` supplies panel serial bytes, hashed with FNV-1a into the + numeric EDID serial. ``raw-panel-serial-number`` on ``/product`` or a panel + node is handled the same way. Machine serials from other nodes are ignored. +* ``width-mm`` and ``height-mm`` supply bounded physical dimensions. Missing + dimensions retain the OF panel node's ``width-mm`` and ``height-mm`` (or + ``adj-height-mm`` when the notch is hidden). + +If the ADT or its identity fields are absent, the desktop J616 fallback is +manufacturer APP, product 0616, name Apple Panel and unspecified numeric +serial zero, represented by the non-empty serial descriptor ``00000000``. +This generic identity does not claim to identify an individual panel. The +preferred representable timing and range limits come from the retained DCP +TimingElements catalog. Exact mode enumeration continues to use that catalog. +No new mode, color capability or machine serial is invented. The ADT reader, +EDID fallback and real EDID setters are restricted to desktop 26.6; T8140 +continues to use its existing path. + +Completed desktop brightness changes emit a backlight change uevent and notify +both ``brightness`` and ``actual_brightness``. The work runs outside the receive +path's locks and is drained during shutdown. Takeover sends the same event when +it updates the inherited level. Requested brightness remains the user's target +through the fade; notifications do not replace it with an intermediate level. diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index 5a67d1801af4e..850decb52cde9 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -40,7 +40,7 @@ #include "parser.h" #include "trace.h" -/* Enable for one diagnostic boot with apple_drm.panel_identity_debug=1. */ +/* Enable for one diagnostic boot with appledrm.panel_identity_debug=1. */ static bool panel_identity_debug; module_param(panel_identity_debug, bool, 0600); MODULE_PARM_DESC(panel_identity_debug, "Bounded desktop panel property keys and types (no values)"); diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/README b/tools/testing/selftests/drivers/gpu/apple-dcp/README index 51c7c0db9f83f..9471075925ef4 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/README +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/README @@ -188,3 +188,11 @@ partial dictionaries, and notify only when valid identity data actually changes. The identity case also exercises the real D552/D561 table bindings, nested DisplayAttributes and direct ProductAttributes, legacy manufacturer codes, minimal identity data, and unchanged byte/padded boolean acknowledgments. + +Desktop identity diagnostics cover bounded traversal of key names and types, +including truncated and oversized dictionaries. ADT identity tests use only +synthetic nodes and panel serials, reject truncated trees transactionally and +ensure the machine's serial cannot override the panel's. Panel EDID tests cover +a missing firmware identity, real EDID priority and invalid replacement retention. +Desktop backlight notification tests exercise accepted, rejected, duplicate and +out-of-order completions and preserve the requested target during notification. From d626b45468908d809347f46e52d50dd11f71dd6d Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 11:48:01 +1100 Subject: [PATCH 121/388] selftests: apple-dcp: retain the ADT fallback EDID for consumer checks Keep a second EDID fixture for the no-ProductAttributes path so the actual libdisplay-info make/model/serial getters and edid-decode can validate it. Avoid redundant metadata hotplugs once a real EDID has priority. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/dcp.c | 6 ++++-- tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py | 7 ++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index b4eb0511e82ce..4e220a80496ce 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -857,8 +857,10 @@ void dcp_panel_metadata_replace(struct apple_dcp *dcp, memcmp(&dcp->desktop->panel_metadata, &merged, sizeof(merged))) { dcp->desktop->panel_metadata = merged; dcp->desktop->panel_metadata_valid = true; - dcp->desktop->panel_metadata_generation++; - changed = true; + if (!dcp->desktop->panel_raw_edid_size) { + dcp->desktop->panel_metadata_generation++; + changed = true; + } } spin_unlock_irqrestore(&dcp->metadata.lock, flags); if (changed && dcp->connector) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py index bccf57dcdb209..3998537deb004 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py @@ -426,7 +426,12 @@ def panel_identity(root, output=None): u64 generation=99; desktop.panel_adt_identity=(struct dcp_display_metadata){.manufacturer="APP",.product=0x616}; const struct drm_edid *fallback=dcp_panel_edid_snapshot(&d,modes,3,&generation); - assert(!IS_ERR(fallback) && drm_edid_valid(fallback));drm_edid_free(fallback); + assert(!IS_ERR(fallback) && drm_edid_valid(fallback)); + if(argc==2) { + char path[4096];snprintf(path,sizeof(path),"%s.adt-fallback",argv[1]); + FILE *file=fopen(path,"wb");assert(file);assert(fwrite(&fallback->raw,128,1,file)==1);fclose(file); + } + drm_edid_free(fallback); bool (*handlers[600])(struct apple_dcp *,int,void *,void *)={ ''' + bindings + r''' }; From 18a99665e5403fbcf0a9736c5aa7ec7cacd466ad Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 11:48:46 +1100 Subject: [PATCH 122/388] drm/apple: include root value types in panel identity diagnostics Scalar setters have no dictionary members. Emit the root type and size as well so one boot identifies their actual representation too. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/parser-test.c | 2 +- drivers/gpu/drm/apple/parser.c | 12 ++++++++++-- .../selftests/drivers/gpu/apple-dcp/desktop.py | 4 ++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/drivers/gpu/drm/apple/parser-test.c b/drivers/gpu/drm/apple/parser-test.c index d96b29c43ea0a..2627faab3d5fe 100644 --- a/drivers/gpu/drm/apple/parser-test.c +++ b/drivers/gpu/drm/apple/parser-test.c @@ -1408,7 +1408,7 @@ static void parser_identity_bounds(struct kunit *test) KUNIT_EXPECT_EQ(test, parse_identity_keys(huge, sizeof(huge), parser_identity_emit, &seen), -E2BIG); - KUNIT_EXPECT_EQ(test, seen, 0U); + KUNIT_EXPECT_EQ(test, seen, 1U); for (u32 len = 0; len < sizeof(huge); len++) KUNIT_EXPECT_LT(test, parse_identity_keys(huge, len, parser_identity_emit, &seen), 0); diff --git a/drivers/gpu/drm/apple/parser.c b/drivers/gpu/drm/apple/parser.c index 0d4e85363e7d2..f12b9157c89fa 100644 --- a/drivers/gpu/drm/apple/parser.c +++ b/drivers/gpu/drm/apple/parser.c @@ -2055,11 +2055,19 @@ static int parse_identity_tree(struct dcp_parse_ctx *ctx, dcp_identity_key_fn em int parse_identity_keys(const void *blob, size_t len, dcp_identity_key_fn emit, void *data) { - struct dcp_parse_ctx ctx; + struct dcp_parse_ctx ctx, root; + const struct dcp_parse_tag *tag; u32 budget = 64; int ret = parse(blob, len, &ctx); - return ret ?: parse_identity_tree(&ctx, emit, data, 0, &budget); + if (ret) + return ret; + root = ctx; + tag = parse_tag(&root); + if (IS_ERR(tag)) + return PTR_ERR(tag); + emit(data, "", 6, tag->type, tag->size, 0); + return parse_identity_tree(&ctx, emit, data, 0, &budget); } /* A setter may deliver the EDID bytes directly or as an OSSerialize blob. */ diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py index 3998537deb004..658043764d59b 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py @@ -685,9 +685,9 @@ def identity_keys(root): } int main(void) { - assert(!parse_identity_keys(blob,sizeof(blob),emit,NULL) && seen==6); + assert(!parse_identity_keys(blob,sizeof(blob),emit,NULL) && seen==7); for(unsigned i=0;i Date: Mon, 26 Oct 2026 11:50:08 +1100 Subject: [PATCH 123/388] drm/apple: accept nested panel EDID blobs with usable identity Look for real EDID data in bounded serialized dictionaries as well as raw and direct blob setters, including the in/out dictionary callback. Keep the existing pass-through reply unchanged. Require identity fields usable by userspace in addition to the EDID checksum, so incomplete real blocks cannot reintroduce null monitor strings. Cover nested blobs and a valid checksum with a missing serial in the host fixture. Assisted-by: LLM Signed-off-by: Ryan Murray --- Documentation/gpu/apple-dcp-t6041-panel.rst | 5 +- drivers/gpu/drm/apple/dcp.c | 33 +++++++- drivers/gpu/drm/apple/iomfb_template.c | 6 +- drivers/gpu/drm/apple/iomfb_v26_6_desktop.c | 11 ++- drivers/gpu/drm/apple/parser.c | 75 ++++++++++++++----- .../drivers/gpu/apple-dcp/desktop.py | 18 ++++- 6 files changed, 118 insertions(+), 30 deletions(-) diff --git a/Documentation/gpu/apple-dcp-t6041-panel.rst b/Documentation/gpu/apple-dcp-t6041-panel.rst index bc7bd3ac4305e..479e1c785f2ec 100644 --- a/Documentation/gpu/apple-dcp-t6041-panel.rst +++ b/Documentation/gpu/apple-dcp-t6041-panel.rst @@ -85,7 +85,10 @@ are never printed. Each property walk stops at depth 4 or 64 objects, and the DCP stops after 256 key records, with bounded framing/status lines in addition. The switch defaults off. Malformed serialization only stops inspection. -A valid real EDID delivered under ``EDID`` or ``edid`` has priority. Without +A real EDID with a usable identity takes priority. It may arrive as raw bytes, +a serialized blob, or an ``EDID``, ``edid`` or ``IODisplayEDID`` blob nested +in a dictionary. Checksum-valid blocks without a usable manufacturer, model +and serial are rejected so they cannot reintroduce null userspace strings. Without one, ProductAttributes remain usable, but are no longer a prerequisite for publishing an identity. The fallback reads the original ADT from m1n1's reserved-memory ``phram`` node with ``label = "adt"``. The reader accepts at diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 4e220a80496ce..65041a66405ff 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -867,6 +867,37 @@ void dcp_panel_metadata_replace(struct apple_dcp *dcp, dcp_queue_hotplug(dcp->connector); } +/* A checksum alone does not guarantee usable userspace identity strings. */ +static bool dcp_panel_edid_has_identity(const u8 *raw) +{ + u16 vendor = (u16)raw[8] << 8 | raw[9]; + bool model = raw[10] || raw[11]; + bool serial = raw[12] || raw[13] || raw[14] || raw[15]; + + for (u32 shift = 0; shift <= 10; shift += 5) + if (!((vendor >> shift) & 31) || ((vendor >> shift) & 31) > 26) + return false; + for (u32 offset = 54; offset <= 108; offset += 18) { + const u8 *desc = raw + offset; + bool text = false; + + if (desc[0] || desc[1] || desc[2] || desc[4]) + continue; + for (u32 i = 5; i < 18 && desc[i] != '\n'; i++) { + if (desc[i] < 0x20 || desc[i] > 0x7e) { + text = false; + break; + } + text |= desc[i] != ' '; + } + if (desc[3] == EDID_DETAIL_MONITOR_NAME) + model |= text; + if (desc[3] == EDID_DETAIL_MONITOR_SERIAL) + serial |= text; + } + return model && serial; +} + /* Preserve a validated real EDID ahead of any metadata-derived fallback. */ void dcp_panel_edid_receive(struct apple_dcp *dcp, const void *blob, size_t len) { @@ -882,7 +913,7 @@ void dcp_panel_edid_receive(struct apple_dcp *dcp, const void *blob, size_t len) edid = drm_edid_alloc(raw, size); if (!edid) return; - if (!drm_edid_valid(edid)) { + if (!drm_edid_valid(edid) || !dcp_panel_edid_has_identity(raw)) { drm_edid_free(edid); return; } diff --git a/drivers/gpu/drm/apple/iomfb_template.c b/drivers/gpu/drm/apple/iomfb_template.c index ce638d142171e..9c3e105636fc3 100644 --- a/drivers/gpu/drm/apple/iomfb_template.c +++ b/drivers/gpu/drm/apple/iomfb_template.c @@ -1267,11 +1267,11 @@ static bool dcpep_process_chunks(struct apple_dcp *dcp, dcp_trace_panel_property(dcp, 0, req->key, strnlen(req->key, sizeof(req->key)), dcp->chunks.data, dcp->chunks.length); - if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && - (!strcmp(req->key, "EDID") || !strcmp(req->key, "edid"))) { + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) dcp_panel_edid_receive(dcp, dcp->chunks.data, dcp->chunks.length); + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && + (!strcmp(req->key, "EDID") || !strcmp(req->key, "edid"))) return true; - } ret = parse(dcp->chunks.data, dcp->chunks.length, &ctx); if (ret) { diff --git a/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c b/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c index 8068e0c25e395..d98a51882b6e8 100644 --- a/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c +++ b/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c @@ -85,8 +85,10 @@ static bool trampoline_passthrough_dict_v26_6(struct apple_dcp *dcp, int tag, strnlen((const char *)req->key, sizeof(req->key)), req->value_null ? NULL : req->value, sizeof(req->value)); - if (!req->value_null) + if (!req->value_null) { + dcp_panel_edid_receive(dcp, req->value, sizeof(req->value)); memcpy(resp->value, req->value, sizeof(resp->value)); + } resp->ret = false; memset(resp->padding, 0, sizeof(resp->padding)); @@ -110,11 +112,8 @@ static bool trampoline_panel_dictionary_v26_6(struct apple_dcp *dcp, int tag, if (!dcp_has_panel(dcp) || req->value_null || !memchr(key, 0, sizeof(req->key))) goto done; - /* A real EDID blob, when supplied, takes precedence over product data. */ - if (!strcmp(key, "EDID") || !strcmp(key, "edid")) { - dcp_panel_edid_receive(dcp, req->value, sizeof(req->value)); - goto done; - } + /* A real EDID may be a blob or nested in a property dictionary. */ + dcp_panel_edid_receive(dcp, req->value, sizeof(req->value)); if (strcmp(key, "DisplayAttributes") && strcmp(key, "ProductAttributes")) goto done; ret = parse(req->value, sizeof(req->value), &ctx); diff --git a/drivers/gpu/drm/apple/parser.c b/drivers/gpu/drm/apple/parser.c index f12b9157c89fa..1c6da363af453 100644 --- a/drivers/gpu/drm/apple/parser.c +++ b/drivers/gpu/drm/apple/parser.c @@ -2070,13 +2070,67 @@ int parse_identity_keys(const void *blob, size_t len, dcp_identity_key_fn emit, return parse_identity_tree(&ctx, emit, data, 0, &budget); } -/* A setter may deliver the EDID bytes directly or as an OSSerialize blob. */ +/* Look for named EDID blobs inside bounded property dictionaries. */ +static int parse_panel_edid_value(struct dcp_parse_ctx *ctx, u32 depth, u32 *budget, + bool edid_key, const u8 **edid, size_t *size) +{ + const struct dcp_parse_tag *tag; + struct dcp_parse_ctx start = *ctx; + + if (depth > 4 || !*budget) + return -E2BIG; + --*budget; + tag = parse_tag(ctx); + if (IS_ERR(tag)) + return PTR_ERR(tag); + if (tag->type == DCP_TYPE_BLOB && edid_key) { + const u8 *data; + + if (tag->size < 128 || tag->size > 4096 || tag->size % 128) + return -EINVAL; + data = parse_bytes(ctx, tag->size); + if (IS_ERR(data)) + return PTR_ERR(data); + if (memcmp(data, "\x00\xff\xff\xff\xff\xff\xff\x00", 8) || + (1U + data[126]) * 128 != tag->size) + return -EINVAL; + *edid = data; + *size = tag->size; + return 0; + } + if (tag->type == DCP_TYPE_DICTIONARY) { + if (tag->size > *budget) + return -E2BIG; + for (u32 i = 0; i < tag->size; i++) { + const struct dcp_parse_tag *key_tag = parse_tag_of_type(ctx, DCP_TYPE_STRING); + const char *key; + bool match; + int ret; + + if (IS_ERR(key_tag)) + return PTR_ERR(key_tag); + key = parse_bytes(ctx, key_tag->size); + if (IS_ERR(key)) + return PTR_ERR(key); + match = (key_tag->size == 4 && (!memcmp(key, "EDID", 4) || + !memcmp(key, "edid", 4))) || + (key_tag->size == 13 && !memcmp(key, "IODisplayEDID", 13)); + ret = parse_panel_edid_value(ctx, depth + 1, budget, match, edid, size); + if (ret != -ENOENT) + return ret; + } + return -ENOENT; + } + *ctx = start; + return skip(ctx) ?: -ENOENT; +} + +/* A setter may deliver raw bytes, a blob, or a dictionary containing a blob. */ int parse_panel_edid_data(const void *blob, size_t len, const u8 **edid, size_t *size) { struct dcp_parse_ctx ctx; - const struct dcp_parse_tag *tag; const u8 *data = blob; - u32 bytes; + u32 bytes, budget = 64; int ret; if (len >= 128 && !memcmp(data, "\x00\xff\xff\xff\xff\xff\xff\x00", 8)) { @@ -2088,18 +2142,5 @@ int parse_panel_edid_data(const void *blob, size_t len, const u8 **edid, size_t return 0; } ret = parse(blob, len, &ctx); - if (ret) - return ret; - tag = parse_tag_of_type(&ctx, DCP_TYPE_BLOB); - if (IS_ERR(tag) || tag->size < 128 || tag->size > 4096 || tag->size % 128) - return -EINVAL; - data = parse_bytes(&ctx, tag->size); - if (IS_ERR(data)) - return PTR_ERR(data); - if (memcmp(data, "\x00\xff\xff\xff\xff\xff\xff\x00", 8) || - (1U + data[126]) * 128 != tag->size) - return -EINVAL; - *edid = data; - *size = tag->size; - return 0; + return ret ?: parse_panel_edid_value(&ctx, 0, &budget, true, edid, size); } diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py index 658043764d59b..064f215fd96e6 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py @@ -380,10 +380,11 @@ def panel_identity(root, output=None): f['edid_header'] + f['parser'] + f['stubs'] + f['builder']) source = (root / 'drivers/gpu/drm/apple/dcp.c').read_text() helpers = '\n'.join(block(source, name) for name in ( - 'void dcp_panel_metadata_replace(', 'void dcp_panel_edid_receive(', 'bool dcp_panel_metadata_current(', + 'void dcp_panel_metadata_replace(', 'static bool dcp_panel_edid_has_identity(', 'void dcp_panel_edid_receive(', 'bool dcp_panel_metadata_current(', 'const struct drm_edid *dcp_panel_edid_snapshot(')) backend = (root / 'drivers/gpu/drm/apple/iomfb_v26_6_desktop.c').read_text() - dictionary = block((root/'drivers/gpu/drm/apple/parser.c').read_text(), 'int parse_panel_edid_data(') + dictionary = block((root/'drivers/gpu/drm/apple/parser.c').read_text(), 'static int parse_panel_edid_value(') + dictionary += block((root/'drivers/gpu/drm/apple/parser.c').read_text(), 'int parse_panel_edid_data(') dictionary += block(backend, 'struct dcp_dict_passthrough_req_v26_6 {')+';\n' dictionary += block(backend, 'static bool trampoline_panel_dictionary_v26_6(') dictionary += block(backend, 'bool iomfb_check_callback_v26_6_desktop(') @@ -487,12 +488,25 @@ def panel_identity(root, output=None): u8 real[128];memcpy(real,bytes,128);real[12]^=1;real[127]^=1; dcp_panel_edid_receive(&d,real,sizeof(real)); assert(desktop.panel_raw_edid_size==128); + u8 nested[148]={0};u32 word; + word=0xd3;memcpy(nested,&word,4);word=0x01000001;memcpy(nested+4,&word,4); + word=0x09000004;memcpy(nested+8,&word,4);memcpy(nested+12,"EDID",4); + word=0x0a000080;memcpy(nested+16,&word,4);memcpy(nested+20,real,128); + const u8 *nested_raw=NULL;size_t nested_size=0; + assert(!parse_panel_edid_data(nested,sizeof(nested),&nested_raw,&nested_size)); + assert(nested_size==128 && !memcmp(nested_raw,real,128)); + dcp_panel_edid_receive(&d,nested,sizeof(nested)); generation=desktop.panel_metadata_generation; const struct drm_edid *copied=dcp_panel_edid_snapshot(&d,modes,3,&generation); assert(!IS_ERR(copied) && !memcmp(&copied->raw,real,128));drm_edid_free(copied); unsigned previous=notifications; real[127]^=1;dcp_panel_edid_receive(&d,real,sizeof(real)); assert(notifications==previous); + /* A checksum-valid block with no serial must not reintroduce null strings. */ + memset(real+12,0,4);memset(real+95,' ',13);real[127]=0; + for(unsigned j=0;j<127;j++)real[127]-=real[j]; + assert(!dcp_panel_edid_has_identity(real)); + dcp_panel_edid_receive(&d,real,sizeof(real));assert(notifications==previous); desktop.panel_raw_edid_size=0;notifications=1; drm_edid_free(edid); /* Repeated and partial dictionaries cannot trigger monitor/brightness restore loops. */ From 075e86fab5f6ddf1a9a842074d9529b8888c5cba Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 11:52:51 +1100 Subject: [PATCH 124/388] drm/apple: drain desktop brightness events before backlight release Register a desktop-only devres action after the backlight resource so notification work is drained before the device is unregistered, including probe unwinds. Clear its published pointer first so a late queue attempt cannot reach a freed device. Exercise this lifetime boundary on the host. Treat blank panel labels as absent so EDID descriptors use non-empty fallbacks. Match current allocation and indentation conventions, and model the new panel-only ingress in the external ownership fixture. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/dcp-internal.h | 1 - drivers/gpu/drm/apple/dcp-panel-adt.c | 2 +- drivers/gpu/drm/apple/dcp.c | 4 +++- drivers/gpu/drm/apple/dcp_backlight.c | 22 +++++++++++++++++-- drivers/gpu/drm/apple/iomfb.c | 4 ++-- drivers/gpu/drm/apple/iomfb_template.c | 3 ++- drivers/gpu/drm/apple/iomfb_v26_6_desktop.c | 6 ++--- drivers/gpu/drm/apple/parser-test.c | 14 +++++++----- drivers/gpu/drm/apple/parser.c | 12 +++++++--- .../drivers/gpu/apple-dcp/desktop.py | 18 ++++++++++----- .../selftests/drivers/gpu/apple-dcp/rate.py | 2 ++ 11 files changed, 62 insertions(+), 26 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index f20ed0ce3d14a..170e583d645c4 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -897,7 +897,6 @@ static inline bool dcp_ext_dptx_607(const struct apple_dcp *dcp) #define DCP_AUDIO_MAX_CHANS 15 - void dcp_trace_panel_property(struct apple_dcp *dcp, int tag, const char *key, size_t key_len, const void *blob, size_t len); diff --git a/drivers/gpu/drm/apple/dcp-panel-adt.c b/drivers/gpu/drm/apple/dcp-panel-adt.c index 9dbe34e0c2f2c..e3a9b5e4f238e 100644 --- a/drivers/gpu/drm/apple/dcp-panel-adt.c +++ b/drivers/gpu/drm/apple/dcp-panel-adt.c @@ -40,7 +40,7 @@ static u32 dcp_adt_serial(const u8 *data, size_t size) } static void dcp_adt_panel_property(struct dcp_panel_adt *panel, const char *name, - const u8 *value, u32 size) + const u8 *value, u32 size) { struct dcp_display_metadata *id = &panel->identity; u32 number = size == 4 ? get_unaligned_le32(value) : 0; diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 65041a66405ff..6a2eebc1cf057 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -874,6 +874,8 @@ static bool dcp_panel_edid_has_identity(const u8 *raw) bool model = raw[10] || raw[11]; bool serial = raw[12] || raw[13] || raw[14] || raw[15]; + if (vendor & 0x8000) + return false; for (u32 shift = 0; shift <= 10; shift += 5) if (!((vendor >> shift) & 31) || ((vendor >> shift) & 31) > 26) return false; @@ -952,7 +954,7 @@ const struct drm_edid *dcp_panel_edid_snapshot(struct apple_dcp *dcp, size_t size; unsigned long flags; - raw = kmalloc(sizeof(dcp->desktop->panel_raw_edid), GFP_KERNEL); + raw = kmalloc_objs(u8, sizeof(dcp->desktop->panel_raw_edid), GFP_KERNEL); if (!raw) return ERR_PTR(-ENOMEM); spin_lock_irqsave(&dcp->metadata.lock, flags); diff --git a/drivers/gpu/drm/apple/dcp_backlight.c b/drivers/gpu/drm/apple/dcp_backlight.c index 180a97b685fd3..3aeabc1982737 100644 --- a/drivers/gpu/drm/apple/dcp_backlight.c +++ b/drivers/gpu/drm/apple/dcp_backlight.c @@ -121,15 +121,26 @@ static void dcp_backlight_notify_work(struct work_struct *work) { struct apple_dcp *dcp = container_of(work, struct apple_dcp, backlight.notify_work); struct backlight_device *bd = READ_ONCE(dcp->brightness.bl_dev); - char *envp[] = { "SOURCE=unknown", NULL }; + char *envp[2]; if (!bd || READ_ONCE(dcp->quiescing)) return; + envp[0] = "SOURCE=unknown"; + envp[1] = NULL; kobject_uevent_env(&bd->dev.kobj, KOBJ_CHANGE, envp); sysfs_notify(&bd->dev.kobj, NULL, "brightness"); sysfs_notify(&bd->dev.kobj, NULL, "actual_brightness"); } +/* Run before the later-registered backlight devres unregisters the device. */ +static void dcp_backlight_notify_release(void *data) +{ + struct apple_dcp *dcp = data; + + WRITE_ONCE(dcp->brightness.bl_dev, NULL); + cancel_work_sync(&dcp->backlight.notify_work); +} + static void dcp_backlight_notify(struct apple_dcp *dcp) { if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && @@ -722,6 +733,7 @@ int dcp_backlight_register(struct apple_dcp *dcp) { struct device *dev = dcp->dev; struct backlight_device *bl_dev; + int ret; unsigned long flags; struct backlight_properties props = { .type = BACKLIGHT_PLATFORM, @@ -781,7 +793,13 @@ int dcp_backlight_register(struct apple_dcp *dcp) if (IS_ERR(bl_dev)) return PTR_ERR(bl_dev); - dcp->brightness.bl_dev = bl_dev; + /* LIFO devres order drains event work before unregistering the backlight. */ + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) { + ret = devm_add_action_or_reset(dev, dcp_backlight_notify_release, dcp); + if (ret) + return ret; + } + WRITE_ONCE(dcp->brightness.bl_dev, bl_dev); /* A hint may have arrived between the property snapshot and registration. */ if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) dcp_backlight_sync_hint(dcp); diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index 850decb52cde9..e27ac426aeef3 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -46,7 +46,7 @@ module_param(panel_identity_debug, bool, 0600); MODULE_PARM_DESC(panel_identity_debug, "Bounded desktop panel property keys and types (no values)"); static void dcp_identity_key_log(void *data, const char *key, u32 key_len, - u32 type, u32 size, u32 depth) + u32 type, u32 size, u32 depth) { struct apple_dcp *dcp = data; char safe[65]; @@ -63,7 +63,7 @@ static void dcp_identity_key_log(void *data, const char *key, u32 key_len, } void dcp_trace_panel_property(struct apple_dcp *dcp, int tag, const char *key, - size_t key_len, const void *blob, size_t len) + size_t key_len, const void *blob, size_t len) { int ret; diff --git a/drivers/gpu/drm/apple/iomfb_template.c b/drivers/gpu/drm/apple/iomfb_template.c index 9c3e105636fc3..b1145f733c9bb 100644 --- a/drivers/gpu/drm/apple/iomfb_template.c +++ b/drivers/gpu/drm/apple/iomfb_template.c @@ -1265,7 +1265,8 @@ static bool dcpep_process_chunks(struct apple_dcp *dcp, dcp->chunks.length); dcp_trace_panel_property(dcp, 0, req->key, - strnlen(req->key, sizeof(req->key)), dcp->chunks.data, dcp->chunks.length); + strnlen(req->key, sizeof(req->key)), + dcp->chunks.data, dcp->chunks.length); if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) dcp_panel_edid_receive(dcp, dcp->chunks.data, dcp->chunks.length); diff --git a/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c b/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c index d98a51882b6e8..b9fba6f5656d1 100644 --- a/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c +++ b/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c @@ -82,8 +82,8 @@ static bool trampoline_passthrough_dict_v26_6(struct apple_dcp *dcp, int tag, trace_iomfb_callback(dcp, tag, "dcpep_cb_passthrough_dict"); dcp_trace_panel_property(dcp, tag, (const char *)req->key, - strnlen((const char *)req->key, sizeof(req->key)), - req->value_null ? NULL : req->value, sizeof(req->value)); + strnlen((const char *)req->key, sizeof(req->key)), + req->value_null ? NULL : req->value, sizeof(req->value)); if (!req->value_null) { dcp_panel_edid_receive(dcp, req->value, sizeof(req->value)); @@ -108,7 +108,7 @@ static bool trampoline_panel_dictionary_v26_6(struct apple_dcp *dcp, int tag, trace_iomfb_callback(dcp, tag, __func__); dcp_trace_panel_property(dcp, tag, key, strnlen(key, sizeof(req->key)), - req->value_null ? NULL : req->value, sizeof(req->value)); + req->value_null ? NULL : req->value, sizeof(req->value)); if (!dcp_has_panel(dcp) || req->value_null || !memchr(key, 0, sizeof(req->key))) goto done; diff --git a/drivers/gpu/drm/apple/parser-test.c b/drivers/gpu/drm/apple/parser-test.c index 2627faab3d5fe..85b6c0df99753 100644 --- a/drivers/gpu/drm/apple/parser-test.c +++ b/drivers/gpu/drm/apple/parser-test.c @@ -1394,7 +1394,7 @@ static void parser_panel_identity_stable(struct kunit *test) } static void parser_identity_emit(void *data, const char *key, u32 len, - u32 type, u32 size, u32 depth) + u32 type, u32 size, u32 depth) { u32 *seen = data; @@ -1405,13 +1405,15 @@ static void parser_identity_bounds(struct kunit *test) { u32 seen = 0; __le32 huge[] = { cpu_to_le32(0xd3), cpu_to_le32(0x01ffffff) }; + int ret; - KUNIT_EXPECT_EQ(test, parse_identity_keys(huge, sizeof(huge), - parser_identity_emit, &seen), -E2BIG); + ret = parse_identity_keys(huge, sizeof(huge), parser_identity_emit, &seen); + KUNIT_EXPECT_EQ(test, ret, -E2BIG); KUNIT_EXPECT_EQ(test, seen, 1U); - for (u32 len = 0; len < sizeof(huge); len++) - KUNIT_EXPECT_LT(test, parse_identity_keys(huge, len, - parser_identity_emit, &seen), 0); + for (u32 len = 0; len < sizeof(huge); len++) { + ret = parse_identity_keys(huge, len, parser_identity_emit, &seen); + KUNIT_EXPECT_LT(test, ret, 0); + } } static void parser_adt_bounds(struct kunit *test) diff --git a/drivers/gpu/drm/apple/parser.c b/drivers/gpu/drm/apple/parser.c index 1c6da363af453..33344a5f8b10b 100644 --- a/drivers/gpu/drm/apple/parser.c +++ b/drivers/gpu/drm/apple/parser.c @@ -1341,6 +1341,11 @@ static int parse_panel_label(struct dcp_parse_ctx *handle, char out[14]) length = 13; memcpy(out, value, length); out[length] = 0; + /* A blank descriptor is absent to libdisplay-info; use the fallback. */ + for (unsigned int i = 0; i < length; i++) + if (out[i] != ' ') + return 0; + out[0] = 0; return 0; } @@ -2006,7 +2011,7 @@ int parse_system_log_mnits(struct dcp_parse_ctx *handle, struct dcp_system_ev_mn /* Inspect structure only. Never emit a firmware value or allocate from its size. */ static int parse_identity_tree(struct dcp_parse_ctx *ctx, dcp_identity_key_fn emit, - void *data, u32 depth, u32 *budget) + void *data, u32 depth, u32 *budget) { const struct dcp_parse_tag *tag; u32 count; @@ -2072,7 +2077,7 @@ int parse_identity_keys(const void *blob, size_t len, dcp_identity_key_fn emit, /* Look for named EDID blobs inside bounded property dictionaries. */ static int parse_panel_edid_value(struct dcp_parse_ctx *ctx, u32 depth, u32 *budget, - bool edid_key, const u8 **edid, size_t *size) + bool edid_key, const u8 **edid, size_t *size) { const struct dcp_parse_tag *tag; struct dcp_parse_ctx start = *ctx; @@ -2102,11 +2107,12 @@ static int parse_panel_edid_value(struct dcp_parse_ctx *ctx, u32 depth, u32 *bud if (tag->size > *budget) return -E2BIG; for (u32 i = 0; i < tag->size; i++) { - const struct dcp_parse_tag *key_tag = parse_tag_of_type(ctx, DCP_TYPE_STRING); + const struct dcp_parse_tag *key_tag; const char *key; bool match; int ret; + key_tag = parse_tag_of_type(ctx, DCP_TYPE_STRING); if (IS_ERR(key_tag)) return PTR_ERR(key_tag); key = parse_bytes(ctx, key_tag->size); diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py index 064f215fd96e6..218b14a6742ca 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py @@ -50,7 +50,7 @@ def modeset_unmap(root): #define trace_iomfb_callback(...) ((void)0) #define dcp_trace_panel_property(...) ((void)0) #define GFP_KERNEL 0 -#define kmalloc(n,f) malloc(n) +#define kmalloc_objs(t,n,f) malloc(sizeof(t)*(n)) #define dev_warn_dma(...) ((void)0) struct dcp_map_physical_req {u64 paddr,size;u32 flags;u8 dva_null,dva_size_null,padding[2];} __attribute__((packed)); struct dcp_map_physical_resp {u64 dva,dva_size;u32 mem_desc_id;} __attribute__((packed)); @@ -210,6 +210,7 @@ def brightness_takeover(root): #include #include #define READ_ONCE(x) (x) +#define WRITE_ONCE(x,v) ((x)=(v)) #define DCP_FIRMWARE_V_26_6_DESKTOP 5 #define DCP_FIRMWARE_H17P 1 #define DCP_IOMFB_METHODS_H17G 7 @@ -218,6 +219,7 @@ def brightness_takeover(root): #define BACKLIGHT_SCALE_LINEAR 1 #define BL_CORE_SUSPENDED 1 #define BL_CORE_FBBLANK 2 +#define devm_add_action_or_reset(...) (0) #define DCP_BL_FADE_STEP_MS 16 #define system_dfl_wq NULL #define IS_ERR(p) false @@ -375,7 +377,7 @@ def panel_identity(root, output=None): 'SerialNumber': 0x12345678}} raw = bytearray(struct.pack(' #include #define READ_ONCE(x) (x) +#define WRITE_ONCE(x,v) ((x)=(v)) #define DCP_FIRMWARE_V_26_6_DESKTOP 5 #define spin_lock_irqsave(p,f) do {assert(!*(p));*(p)=true;(f)=0;} while(0) #define spin_unlock_irqrestore(p,f) do {assert(*(p));*(p)=false;(void)(f);} while(0) @@ -793,7 +797,8 @@ def brightness_notifications(root): struct {bool lock;struct dcp_backlight_state state;struct work_struct notify_work;int retry_work;} backlight; struct {struct dcp_backlight_swap backlight;} *desktop; }; -static unsigned events,brightness,actual,kicks; +static unsigned events,brightness,actual,kicks,cancelled; +static void cancel_work_sync(struct work_struct *w) {(void)w;cancelled++;} static void kobject_uevent_env(int *k,int action,char **env) {(void)k;assert(action==1 && !strcmp(env[0],"SOURCE=unknown") && !env[1]);events++;} static void sysfs_notify(int *k,void *dir,const char *name) @@ -823,7 +828,8 @@ def brightness_notifications(root): dcp_backlight_desktop_finish(&d,9,true,false);assert(events==1); d.quiescing=true;dcp_backlight_notify(&d);assert(events==1); d.quiescing=false;d.fw_compat=1;dcp_backlight_notify(&d);assert(events==1); - d.fw_compat=5;d.brightness.bl_dev=NULL;dcp_backlight_notify(&d);assert(events==1); + d.fw_compat=5;dcp_backlight_notify_release(&d); + assert(!d.brightness.bl_dev && cancelled==1);dcp_backlight_notify(&d);assert(events==1); return 0; } ''' diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py b/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py index 4194e8d39ad20..02e23247f50b2 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py @@ -650,6 +650,8 @@ def publication(root): static const struct drm_edid *dcp_panel_edid_snapshot(struct apple_dcp *d,const struct dcp_display_mode *m,unsigned n,u64 *generation) { (void)d;(void)m;(void)n;(void)generation;assert(!"panel identity in external fixture");return ERR_PTR(-ENODATA); } static bool dcp_panel_metadata_current(struct apple_dcp *d,u64 generation) { (void)d;(void)generation;assert(!"panel identity in external fixture");return false; } static void dcp_panel_metadata_replace(struct apple_dcp *d,const struct dcp_display_metadata *m) { (void)d;(void)m;assert(!"panel identity in external receipt"); } +static void dcp_panel_edid_receive(struct apple_dcp *d,const void *p,size_t n) { (void)d;(void)p;(void)n;assert(!"panel EDID in external receipt"); } +#define dcp_trace_panel_property(...) ((void)0) static struct drm_display_mode *drm_mode_duplicate(struct drm_device *dev,const struct drm_display_mode *m) { assert(dev->mode_config.mutex);struct drm_display_mode *copy=malloc(sizeof(*copy));assert(copy);*copy=*m; From 94909ec2fe98097481256799602fbb1c1215ada3 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 11:53:21 +1100 Subject: [PATCH 125/388] drm/apple: preserve exact panel dimensions in the EDID timing The EDID base size rounds to centimetres, but its detailed timing can retain the panel's millimetres. Use the actual panel dimensions there, including the ADT fallback, and check both fields in the host fixture. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/dcp-display-metadata.c | 6 ++++++ tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py | 2 ++ 2 files changed, 8 insertions(+) diff --git a/drivers/gpu/drm/apple/dcp-display-metadata.c b/drivers/gpu/drm/apple/dcp-display-metadata.c index 94cc5ec03417f..6355e697a653e 100644 --- a/drivers/gpu/drm/apple/dcp-display-metadata.c +++ b/drivers/gpu/drm/apple/dcp-display-metadata.c @@ -275,6 +275,12 @@ const struct drm_edid *dcp_panel_edid(const struct dcp_display_metadata *metadat mode.clock = DIV_ROUND_CLOSEST(mode.clock, 10) * 10; ret = dcp_metadata_timing(&candidate, &mode, &dimensions); if (!ret && (!timing || (!preferred && (mode.type & DRM_MODE_TYPE_PREFERRED)))) { + struct detailed_pixel_timing *pixel = &candidate.data.pixel_data; + + /* The base size is in cm, but the DTD preserves exact panel mm. */ + pixel->width_mm_lo = width_mm; + pixel->height_mm_lo = height_mm; + pixel->width_height_mm_hi = (width_mm >> 8) << 4 | height_mm >> 8; block.detailed_timings[0] = candidate; timing = true; preferred = mode.type & DRM_MODE_TYPE_PREFERRED; diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py index 218b14a6742ca..f28c13d238322 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py @@ -478,6 +478,8 @@ def panel_identity(root, output=None): assert(bytes[8]==0x52 && bytes[9]==0x74 && bytes[10]==42 && !bytes[11]); assert(bytes[12]==0x78 && bytes[13]==0x56 && bytes[14]==0x34 && bytes[15]==0x12); assert(bytes[18]==1 && bytes[19]==4 && bytes[21]==35 && bytes[22]==22 && bytes[23]==0xff); + assert(((unsigned)bytes[66] | (unsigned)(bytes[68] >> 4) << 8)==345); + assert(((unsigned)bytes[67] | (unsigned)(bytes[68] & 15) << 8)==223); assert(bytes[72+3]==EDID_DETAIL_MONITOR_NAME && bytes[90+3]==EDID_DETAIL_MONITOR_SERIAL); assert(!memcmp(bytes+90+5,"12345678\n",9)); if(argc==2) {FILE *file=fopen(argv[1],"wb");assert(file);assert(fwrite(bytes,128,1,file)==1);fclose(file);} From 18b408b7368b9470c22d92c9afe1991d0fe608ba Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 25 Oct 2026 20:58:53 -0400 Subject: [PATCH 126/388] drm/asahi: check the G17 admission churn test against its deadline, not the clock The test that gate churn cannot renew a compute admission budget looped until the budget ran out and then required the whole loop to have taken less than 500 ms. The loop does not sleep, so that bound only measured how long the test thread was descheduled, and a KUnit run under KVM on a shared CI host exceeded it. Check the property instead: every admission granted came from an attempt that started before the deadline, and the first attempt after it is refused with EBUSY. --- drivers/gpu/drm/asahi/g17/preparation.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/preparation.rs b/drivers/gpu/drm/asahi/g17/preparation.rs index 3cafecf5d8206..eb2366fa3451b 100644 --- a/drivers/gpu/drm/asahi/g17/preparation.rs +++ b/drivers/gpu/drm/asahi/g17/preparation.rs @@ -361,9 +361,9 @@ mod compute_deadline_tests { #[test] fn recovery_and_binding_retry_churn_cannot_renew_budget() { let gate = Gate::new().unwrap(); - let start = super::super::now_ns(); + let deadline = super::super::now_ns() + 20_000_000; let check = || Ok(()); - let admission = Admission::new(start + 20_000_000, &check); + let admission = Admission::new(deadline, &check); loop { // Re-entry models a channel taken between admission/publication. // Recovery joins each dropped lease, then wakes the same budget. @@ -371,12 +371,17 @@ mod compute_deadline_tests { gate.changed.notify_all(); gate.wait_drained(); assert!(gate.reopen()); + // However often the gate reopened, an attempt that starts at the deadline + // is refused. Timing the whole loop would also count time descheduled. + let attempted = super::super::now_ns(); match gate.enter_until(&admission) { - Ok(lease) => drop(lease), + Ok(lease) => { + assert!(attempted < deadline); + drop(lease); + } Err(error) => { assert_eq!(error, EBUSY); break; } } } - assert!(super::super::now_ns() - start < 500_000_000); assert_eq!(gate.state.load(Ordering::Acquire) & COUNT_MASK, 0); } From a30b34da388d10e4b4557bc31ff0f59635ba0c22 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 12:04:57 +1100 Subject: [PATCH 127/388] drm/apple: reserve separate identity trace budgets for panel setters D114 statistics must not exhaust diagnostic output before D552 or D561 publishes panel data. Give each callback class a separate bounded budget, with a fixed total of 512 key records per DCP for the diagnostic boot. Assisted-by: LLM Signed-off-by: Ryan Murray --- Documentation/gpu/apple-dcp-t6041-panel.rst | 4 +++- drivers/gpu/drm/apple/dcp-internal.h | 2 +- drivers/gpu/drm/apple/iomfb.c | 26 +++++++++++++++------ 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/Documentation/gpu/apple-dcp-t6041-panel.rst b/Documentation/gpu/apple-dcp-t6041-panel.rst index 479e1c785f2ec..1bbb62099a9c9 100644 --- a/Documentation/gpu/apple-dcp-t6041-panel.rst +++ b/Documentation/gpu/apple-dcp-t6041-panel.rst @@ -82,7 +82,9 @@ D114 and chunked property delivery report the outer key, serialized key names, value type numbers and lengths. D000 identifies chunked delivery. Type numbers are dictionary 1, array 2, integer 4, string 9, blob 10 and boolean 11. Values are never printed. Each property walk stops at depth 4 or 64 objects, and the -DCP stops after 256 key records, with bounded framing/status lines in addition. +DCP stops after 128 key records per callback class (512 total), with bounded +framing/status lines in addition. D552, D561, D114 and chunked delivery have +separate budgets so periodic statistics cannot hide the panel setters. The switch defaults off. Malformed serialization only stops inspection. A real EDID with a usable identity takes priority. It may arrive as raw bytes, diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index 170e583d645c4..a7079c1bbb814 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -248,7 +248,7 @@ struct dcp_desktop_state { struct dcp_display_metadata panel_adt_identity; u64 panel_metadata_generation; bool panel_metadata_valid; - u32 identity_log_count; + u32 identity_log_count[4]; u8 panel_raw_edid[4096]; size_t panel_raw_edid_size; /* RX records an unsupported callback until the modeset RPC has drained. */ diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index e27ac426aeef3..40e2b797b7d60 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -45,19 +45,25 @@ static bool panel_identity_debug; module_param(panel_identity_debug, bool, 0600); MODULE_PARM_DESC(panel_identity_debug, "Bounded desktop panel property keys and types (no values)"); +struct dcp_identity_trace { + struct apple_dcp *dcp; + u32 *count; +}; + static void dcp_identity_key_log(void *data, const char *key, u32 key_len, u32 type, u32 size, u32 depth) { - struct apple_dcp *dcp = data; + struct dcp_identity_trace *trace = data; + struct apple_dcp *dcp = trace->dcp; char safe[65]; u32 i; - if (dcp->desktop->identity_log_count >= 256) + if (*trace->count >= 128) return; for (i = 0; i < min_t(u32, key_len, sizeof(safe) - 1); i++) safe[i] = key[i] >= 0x20 && key[i] <= 0x7e ? key[i] : '?'; safe[i] = 0; - dcp->desktop->identity_log_count++; + ++*trace->count; dev_info(dcp->dev, "panel identity depth=%u key=%s type=%u size=%u\n", depth, safe, type, size); } @@ -65,18 +71,24 @@ static void dcp_identity_key_log(void *data, const char *key, u32 key_len, void dcp_trace_panel_property(struct apple_dcp *dcp, int tag, const char *key, size_t key_len, const void *blob, size_t len) { + struct dcp_identity_trace trace; + u32 slot = tag == 552 ? 0 : tag == 561 ? 1 : tag == 114 ? 2 : 3; int ret; if (!READ_ONCE(panel_identity_debug) || - dcp->fw_compat != DCP_FIRMWARE_V_26_6_DESKTOP || - !dcp_has_panel(dcp) || dcp->desktop->identity_log_count >= 256) + dcp->fw_compat != DCP_FIRMWARE_V_26_6_DESKTOP || !dcp_has_panel(dcp)) + return; + trace.dcp = dcp; + trace.count = &dcp->desktop->identity_log_count[slot]; + /* Periodic statistics cannot consume the panel setters' budget. */ + if (*trace.count >= 128) return; dev_info(dcp->dev, "panel identity callback=D%03d bytes=%zu null=%d\n", tag, len, !blob); - dcp_identity_key_log(dcp, key, key_len, 0, len, 0); + dcp_identity_key_log(&trace, key, key_len, 0, len, 0); if (!blob) return; - ret = parse_identity_keys(blob, len, dcp_identity_key_log, dcp); + ret = parse_identity_keys(blob, len, dcp_identity_key_log, &trace); if (ret) dev_info(dcp->dev, "panel identity inspection stopped: %d\n", ret); } From c972c29d1024380572fcaa3b75e1d3a7208d1ec0 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 12:06:01 +1100 Subject: [PATCH 128/388] selftests: apple-dcp: exercise identity trace gating and budget isolation Drive the production logger with a synthetic inspector. Check default-off and non-desktop gating, sanitization, value omission, fixed limits and that exhausted statistics do not silence D552 or D561. Assisted-by: LLM Signed-off-by: Ryan Murray --- .../drivers/gpu/apple-dcp/desktop.py | 66 +++++++++++++++++++ .../selftests/drivers/gpu/apple-dcp/run.py | 1 + 2 files changed, 67 insertions(+) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py index f28c13d238322..97a017aea8524 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py @@ -835,3 +835,69 @@ def brightness_notifications(root): return 0; } ''' + + +def identity_trace_budget(root): + source=(root/'drivers/gpu/drm/apple/iomfb.c').read_text() + code=r''' +#include +#include +#include +#include +#include +#include +#include +typedef uint32_t u32; +#define READ_ONCE(x) (x) +#define min_t(t,a,b) ((t)(a)<(t)(b)?(t)(a):(t)(b)) +#define DCP_FIRMWARE_V_26_6_DESKTOP 5 +struct apple_dcp {int fw_compat;bool has_panel;void *dev;struct {u32 identity_log_count[4];} *desktop;}; +static bool panel_identity_debug; +static unsigned messages; +static char output[256]; +static bool dcp_has_panel(struct apple_dcp *d) {return d->has_panel;} +static void dev_info(void *dev,const char *fmt,...) +{ + (void)dev;va_list args;va_start(args,fmt);vsnprintf(output,sizeof(output),fmt,args);va_end(args);messages++; +} +typedef void (*dcp_identity_key_fn)(void *,const char *,u32,u32,u32,u32); +static int parse_identity_keys(const void *blob,size_t len,dcp_identity_key_fn emit,void *data) +{ + (void)blob;(void)len; + for(unsigned i=0;i<70;i++)emit(data,"Key\n",4,9,25,0); + return 0; +} +''' + code+=block(source,'struct dcp_identity_trace {')+';\n' + code+=block(source,'static void dcp_identity_key_log(') + code+=block(source,'void dcp_trace_panel_property(') + return code+r''' +int main(void) +{ + struct apple_dcp d={.fw_compat=5,.has_panel=true}; + __typeof__(*d.desktop) desktop={0};d.desktop=&desktop; + const char secret[]="SYNTHETIC_VALUE_NOT_FOR_LOGS"; + dcp_trace_panel_property(&d,561,"Property",8,secret,sizeof(secret));assert(!messages); + panel_identity_debug=true;d.fw_compat=1; + dcp_trace_panel_property(&d,561,"Property",8,secret,sizeof(secret));assert(!messages); + d.fw_compat=5;d.has_panel=false; + dcp_trace_panel_property(&d,561,"Property",8,secret,sizeof(secret));assert(!messages); + d.has_panel=true; + for(unsigned i=0;i<100;i++)dcp_trace_panel_property(&d,114,"Statistics",10,secret,sizeof(secret)); + assert(desktop.identity_log_count[2]==128 && !desktop.identity_log_count[0] && !desktop.identity_log_count[1]); + unsigned before=messages; + dcp_trace_panel_property(&d,552,"Panel",5,secret,sizeof(secret));assert(messages>before); + dcp_trace_panel_property(&d,561,"Panel",5,secret,sizeof(secret)); + assert(desktop.identity_log_count[0] && desktop.identity_log_count[1]); + assert(strstr(output,"Key?") && !strstr(output,secret)); + for(unsigned i=0;i<100;i++) { + dcp_trace_panel_property(&d,552,"Panel",5,secret,sizeof(secret)); + dcp_trace_panel_property(&d,561,"Panel",5,secret,sizeof(secret)); + dcp_trace_panel_property(&d,0,"Chunks",6,secret,sizeof(secret)); + } + for(unsigned i=0;i<4;i++)assert(desktop.identity_log_count[i]==128); + before=messages;dcp_trace_panel_property(&d,561,"Panel",5,NULL,0);assert(messages==before); + assert(messages<=520); + return 0; +} +''' diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/run.py b/tools/testing/selftests/drivers/gpu/apple-dcp/run.py index 3c26c17ad916f..1b27400946e06 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/run.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/run.py @@ -62,6 +62,7 @@ def main(): ('display', 'panel-mode-catalog', display.modes, ((),), ()), ('display', 'desktop-brightness-notifications', desktop.brightness_notifications, ((),), ()), ('display', 'desktop-adt-identity', desktop.adt_identity, ((),), ()), + ('display', 'desktop-identity-trace-budget', desktop.identity_trace_budget, ((),), ()), ('display', 'desktop-identity-keys', desktop.identity_keys, ((),), ('-Wno-unused-function',)), ('display', 'desktop-panel-identity', lambda r: desktop.panel_identity(r, output), ((),), ()), ('display', 'desktop-powerlog-framing', desktop.powerlog_framing, ((),), ()), From 10e863250bee930b3fad31aa2d66bf9b9a2de3b7 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 12:32:07 +1100 Subject: [PATCH 129/388] drm/apple: reserve panel trace records atomically Use atomic budget reservations for both framing and key records, so the diagnostic bound also holds if callback contexts overlap. Preserve the separate setter budgets and exercise the same fixed limits on the host. Assisted-by: LLM Signed-off-by: Ryan Murray --- Documentation/gpu/apple-dcp-t6041-panel.rst | 4 ++-- drivers/gpu/drm/apple/dcp-internal.h | 3 ++- drivers/gpu/drm/apple/iomfb.c | 7 +++---- .../selftests/drivers/gpu/apple-dcp/desktop.py | 12 ++++++++---- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/Documentation/gpu/apple-dcp-t6041-panel.rst b/Documentation/gpu/apple-dcp-t6041-panel.rst index 1bbb62099a9c9..79572e5099b37 100644 --- a/Documentation/gpu/apple-dcp-t6041-panel.rst +++ b/Documentation/gpu/apple-dcp-t6041-panel.rst @@ -82,8 +82,8 @@ D114 and chunked property delivery report the outer key, serialized key names, value type numbers and lengths. D000 identifies chunked delivery. Type numbers are dictionary 1, array 2, integer 4, string 9, blob 10 and boolean 11. Values are never printed. Each property walk stops at depth 4 or 64 objects, and the -DCP stops after 128 key records per callback class (512 total), with bounded -framing/status lines in addition. D552, D561, D114 and chunked delivery have +DCP stops after 128 diagnostic records per callback class (512 total), with bounded +inspection-status lines in addition. Atomic reservations include framing records. D552, D561, D114 and chunked delivery have separate budgets so periodic statistics cannot hide the panel setters. The switch defaults off. Malformed serialization only stops inspection. diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index a7079c1bbb814..7a3a969fd7e76 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -5,6 +5,7 @@ #define __APPLE_DCP_INTERNAL_H__ #include +#include #include #include #include @@ -248,7 +249,7 @@ struct dcp_desktop_state { struct dcp_display_metadata panel_adt_identity; u64 panel_metadata_generation; bool panel_metadata_valid; - u32 identity_log_count[4]; + atomic_t identity_log_count[4]; u8 panel_raw_edid[4096]; size_t panel_raw_edid_size; /* RX records an unsupported callback until the modeset RPC has drained. */ diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index 40e2b797b7d60..6d38d96ed1365 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -47,7 +47,7 @@ MODULE_PARM_DESC(panel_identity_debug, "Bounded desktop panel property keys and struct dcp_identity_trace { struct apple_dcp *dcp; - u32 *count; + atomic_t *count; }; static void dcp_identity_key_log(void *data, const char *key, u32 key_len, @@ -58,12 +58,11 @@ static void dcp_identity_key_log(void *data, const char *key, u32 key_len, char safe[65]; u32 i; - if (*trace->count >= 128) + if (!atomic_add_unless(trace->count, 1, 128)) return; for (i = 0; i < min_t(u32, key_len, sizeof(safe) - 1); i++) safe[i] = key[i] >= 0x20 && key[i] <= 0x7e ? key[i] : '?'; safe[i] = 0; - ++*trace->count; dev_info(dcp->dev, "panel identity depth=%u key=%s type=%u size=%u\n", depth, safe, type, size); } @@ -81,7 +80,7 @@ void dcp_trace_panel_property(struct apple_dcp *dcp, int tag, const char *key, trace.dcp = dcp; trace.count = &dcp->desktop->identity_log_count[slot]; /* Periodic statistics cannot consume the panel setters' budget. */ - if (*trace.count >= 128) + if (!atomic_add_unless(trace.count, 1, 128)) return; dev_info(dcp->dev, "panel identity callback=D%03d bytes=%zu null=%d\n", tag, len, !blob); diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py index 97a017aea8524..d36274f0e3117 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py @@ -848,10 +848,14 @@ def identity_trace_budget(root): #include #include typedef uint32_t u32; +typedef struct {int value;} atomic_t; +static int atomic_read(const atomic_t *n) {return n->value;} +static bool atomic_add_unless(atomic_t *n,int add,int unless) +{if(n->value==unless)return false;n->value+=add;return true;} #define READ_ONCE(x) (x) #define min_t(t,a,b) ((t)(a)<(t)(b)?(t)(a):(t)(b)) #define DCP_FIRMWARE_V_26_6_DESKTOP 5 -struct apple_dcp {int fw_compat;bool has_panel;void *dev;struct {u32 identity_log_count[4];} *desktop;}; +struct apple_dcp {int fw_compat;bool has_panel;void *dev;struct {atomic_t identity_log_count[4];} *desktop;}; static bool panel_identity_debug; static unsigned messages; static char output[256]; @@ -884,18 +888,18 @@ def identity_trace_budget(root): dcp_trace_panel_property(&d,561,"Property",8,secret,sizeof(secret));assert(!messages); d.has_panel=true; for(unsigned i=0;i<100;i++)dcp_trace_panel_property(&d,114,"Statistics",10,secret,sizeof(secret)); - assert(desktop.identity_log_count[2]==128 && !desktop.identity_log_count[0] && !desktop.identity_log_count[1]); + assert(atomic_read(&desktop.identity_log_count[2])==128 && !atomic_read(&desktop.identity_log_count[0]) && !atomic_read(&desktop.identity_log_count[1])); unsigned before=messages; dcp_trace_panel_property(&d,552,"Panel",5,secret,sizeof(secret));assert(messages>before); dcp_trace_panel_property(&d,561,"Panel",5,secret,sizeof(secret)); - assert(desktop.identity_log_count[0] && desktop.identity_log_count[1]); + assert(atomic_read(&desktop.identity_log_count[0]) && atomic_read(&desktop.identity_log_count[1])); assert(strstr(output,"Key?") && !strstr(output,secret)); for(unsigned i=0;i<100;i++) { dcp_trace_panel_property(&d,552,"Panel",5,secret,sizeof(secret)); dcp_trace_panel_property(&d,561,"Panel",5,secret,sizeof(secret)); dcp_trace_panel_property(&d,0,"Chunks",6,secret,sizeof(secret)); } - for(unsigned i=0;i<4;i++)assert(desktop.identity_log_count[i]==128); + for(unsigned i=0;i<4;i++)assert(atomic_read(&desktop.identity_log_count[i])==128); before=messages;dcp_trace_panel_property(&d,561,"Panel",5,NULL,0);assert(messages==before); assert(messages<=520); return 0; From 6f5d208b4ce01ff8cfea04fa21f14ac9a7cb610c Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 25 Oct 2026 21:36:35 -0400 Subject: [PATCH 130/388] cpufreq: apple: apply a lowered T8140 fast die limit at once The fast die-temperature limit is a FREQ_QOS_MAX request. T8140 now switches frequency from the scheduler, where schedutil only records a lower limit and applies it at its next frequency update, which a nohz_full CPU running a task with its tick stopped may not have for a long time. Through ->target_index, which T8140 used before lazy verification, the core applies the limit at once. When a sample changes the limit of a lazy_verify cluster, record the fastest P-state at or below it and move the cluster to the P-state the scheduler last asked for, at most that one, through the same locked path as fast-switch requests. Later fast-switch requests are clamped to it while the limit holds, and return the frequency actually requested. Going back to the scheduler's request also covers a limit that rises again before the QoS request was updated: the request is then left as it was, and schedutil does not ask again. Waited-for requests own the cluster from the drain to the next fast-switch request, and the core applies the limit to them, so the samples leave the cluster alone then. The drain now marks this before it cancels the verify timer, so that no sample queues the timer again. Measured on T8140 with a diagnostic build that forces a 2196 MHz limit while a spinner runs on a P-core: the limit is written inside the sample that sets it, where the scheduler path took about 1 ms, and 0.02 to 2.5 ms with nohz_full on that CPU. A drop and restore within one sample, with the QoS request unchanged, returns the cluster to the scheduler's request. Under a real thermal load the frequency stayed at or below the limit in every sample. Static governor switches, unbinding and a pm_test=devices cycle were clean, and light GPU submission costs are unchanged. Other SoCs are unchanged. --- drivers/cpufreq/apple-soc-cpufreq.c | 151 ++++++++++++++++++++++------ 1 file changed, 122 insertions(+), 29 deletions(-) diff --git a/drivers/cpufreq/apple-soc-cpufreq.c b/drivers/cpufreq/apple-soc-cpufreq.c index a50628797fb4f..bc3e2402ced2a 100644 --- a/drivers/cpufreq/apple-soc-cpufreq.c +++ b/drivers/cpufreq/apple-soc-cpufreq.c @@ -122,9 +122,10 @@ * warning has been given. A nohz_full CPU can run a task with its tick * stopped, and so without sampling, so a cluster with nohz_full CPUs * samples from the hrtimer all the time. Where schedutil switches frequency - * from the scheduler (fast switch), a lower limit reaches the cluster at its - * next scheduler frequency update rather than at once, which a nohz_full CPU - * running with its tick stopped may not have for a long time. + * from the scheduler (fast switch), it picks up a lower limit only at its + * next frequency update, which a nohz_full CPU running with its tick stopped + * may not have for a long time, so the sample applies the limit to the + * cluster itself, see apple_soc_cpufreq_lazy_cap(). * * A sample reads the effort through the PMP report driver; only when the * limit it maps to changes is a work item queued to update the QoS request, @@ -228,9 +229,10 @@ struct apple_cpu_priv { /* * Lazy verification, see apple_soc_cpufreq_lazy_request(): the lock * serialises the command register, expected_pstate and - * transition_failed between the scheduler's requests and the timer - * that verifies them. Waited-for requests cancel the timer first and - * do not take it. + * transition_failed between the scheduler's requests, the timer that + * verifies them and the fast die-temperature samples. Waited-for + * requests cancel the timer first and do not take it; the samples + * then leave the cluster to them. */ raw_spinlock_t lock; struct hrtimer verify_timer; @@ -239,6 +241,15 @@ struct apple_cpu_priv { ktime_t verify_deadline; /* P-state requested while a transition was busy, or -1. */ int pending_pstate; + /* + * Highest P-state, and its frequency, that the fast die-temperature + * limit allows, the P-state the scheduler last asked for, and whether + * waited-for requests own the cluster, see apple_soc_cpufreq_lazy_cap(). + */ + unsigned int cap_pstate; + unsigned int cap_freq; + unsigned int asked_pstate; + bool waited; /* A failure the verify timer has yet to report, and its command. */ bool failure_unreported; u64 failure_command; @@ -632,26 +643,16 @@ static enum hrtimer_restart apple_soc_cpufreq_verify(struct hrtimer *timer) } /* - * Requests the frequency table entry @index from the scheduler's fast-switch - * path without waiting, see "Lazy verification" above. + * Requests @pstate without waiting, see "Lazy verification" above. + * Called with priv->lock held. */ -static int apple_soc_cpufreq_lazy_request(struct cpufreq_policy *policy, - unsigned int index) +static int apple_soc_cpufreq_lazy_set(struct apple_cpu_priv *priv, + unsigned int pstate) { - struct apple_cpu_priv *priv = policy->driver_data; - unsigned int pstate = policy->freq_table[index].driver_data; - unsigned long flags; - int ret = 0; u64 reg; - if (pstate < priv->info->min_pstate || pstate > priv->info->max_pstate) - return -EINVAL; - - raw_spin_lock_irqsave(&priv->lock, flags); - if (priv->transition_failed) { - ret = -EIO; - goto out; - } + if (priv->transition_failed) + return -EIO; reg = readq_relaxed(priv->reg_base + APPLE_DVFS_CMD); if (reg & APPLE_DVFS_CMD_BUSY) { @@ -669,7 +670,7 @@ static int apple_soc_cpufreq_lazy_request(struct cpufreq_policy *policy, if (!hrtimer_is_queued(&priv->verify_timer) || ktime_after(hrtimer_get_softexpires(&priv->verify_timer), at)) hrtimer_start(&priv->verify_timer, at, HRTIMER_MODE_ABS_HARD); - goto out; + return 0; } /* An idle cluster has accepted, or refused, the previous request. */ @@ -680,11 +681,69 @@ static int apple_soc_cpufreq_lazy_request(struct cpufreq_policy *policy, } else if (priv->transition_failed) { /* Let the timer report it, outside the scheduler. */ hrtimer_start(&priv->verify_timer, 0, HRTIMER_MODE_REL_HARD); - ret = -EIO; + return -EIO; } -out: + return 0; +} + +/* + * Requests the frequency table entry @index from the scheduler's fast-switch + * path, at most the fast die-temperature limit, and returns the frequency + * requested, or 0 on failure. The request is committed: the cluster applies + * it within microseconds. + */ +static unsigned int apple_soc_cpufreq_lazy_request(struct cpufreq_policy *policy, + unsigned int index) +{ + struct apple_cpu_priv *priv = policy->driver_data; + unsigned int pstate = policy->freq_table[index].driver_data; + unsigned int freq = policy->freq_table[index].frequency; + unsigned long flags; + int ret; + + if (pstate < priv->info->min_pstate || pstate > priv->info->max_pstate) + return 0; + + raw_spin_lock_irqsave(&priv->lock, flags); + priv->waited = false; + priv->asked_pstate = pstate; + if (pstate > priv->cap_pstate) { + pstate = priv->cap_pstate; + freq = priv->cap_freq; + } + ret = apple_soc_cpufreq_lazy_set(priv, pstate); + raw_spin_unlock_irqrestore(&priv->lock, flags); + return ret ? 0 : freq; +} + +/* + * Applies a new fast die-temperature limit, whose highest P-state is @cap at + * @freq, to a lazy_verify cluster at once. schedutil picks up a lower limit + * only at its next frequency update, which a nohz_full CPU running with its + * tick stopped may not have for a long time. The cluster is moved to the + * P-state the scheduler last asked for, at most @cap, and later requests are + * clamped to @cap. That also restores the request when the limit rises + * again before schedutil has seen it fall: the QoS request is then left + * unchanged, so schedutil does not ask again. + * + * Waited-for requests own the cluster from apple_soc_cpufreq_drain() to the + * next fast-switch request, and the core applies the limit to them. + */ +static void apple_soc_cpufreq_lazy_cap(struct apple_cpu_priv *priv, + unsigned int cap, unsigned int freq) +{ + unsigned int requested, pstate; + unsigned long flags; + + raw_spin_lock_irqsave(&priv->lock, flags); + priv->cap_pstate = cap; + priv->cap_freq = freq; + requested = priv->pending_pstate >= 0 ? priv->pending_pstate : + priv->expected_pstate; + pstate = min(priv->asked_pstate, cap); + if (!priv->waited && pstate != requested) + apple_soc_cpufreq_lazy_set(priv, pstate); raw_spin_unlock_irqrestore(&priv->lock, flags); - return ret; } /* @@ -701,6 +760,11 @@ static int apple_soc_cpufreq_drain(struct apple_cpu_priv *priv) int pstate; u64 reg; + /* First, so that no fast die-temperature sample queues the timer again. */ + raw_spin_lock_irqsave(&priv->lock, flags); + priv->waited = true; + raw_spin_unlock_irqrestore(&priv->lock, flags); + hrtimer_cancel(&priv->verify_timer); raw_spin_lock_irqsave(&priv->lock, flags); @@ -764,11 +828,11 @@ static unsigned int apple_soc_cpufreq_fast_switch(struct cpufreq_policy *policy, if (idx < 0) return 0; - if (priv->info->lazy_verify ? apple_soc_cpufreq_lazy_request(policy, idx) : - apple_soc_cpufreq_request(policy, idx)) + if (priv->info->lazy_verify) + return apple_soc_cpufreq_lazy_request(policy, idx); + if (apple_soc_cpufreq_request(policy, idx)) return 0; - /* A lazy request is committed: the cluster applies it within microseconds. */ return policy->freq_table[idx].frequency; } @@ -860,6 +924,31 @@ static void apple_fast_die_arm(struct apple_fast_die *fd) HRTIMER_MODE_REL_SOFT); } +/* + * Applies @limit to a lazy_verify cluster at once, as its fastest state at or + * below the limit, see apple_soc_cpufreq_lazy_cap(). + */ +static void apple_fast_die_cap(struct apple_fast_die *fd, unsigned int limit) +{ + struct cpufreq_policy *policy = fd->policy; + struct apple_cpu_priv *priv = policy->driver_data; + struct cpufreq_frequency_table *pos; + unsigned int cap = 0, freq = 0; + + if (!priv->info->lazy_verify) + return; + + cpufreq_for_each_valid_entry(pos, policy->freq_table) { + if (pos->frequency <= limit && pos->driver_data > cap) { + cap = pos->driver_data; + freq = pos->frequency; + } + } + /* Limits are frequency-table entries, see apple_fast_die_fit(). */ + if (freq) + apple_soc_cpufreq_lazy_cap(priv, cap, freq); +} + /* Take a sample and queue an update if the limit changes. Call with @lock held. */ static void apple_fast_die_sample(struct apple_fast_die *fd) { @@ -887,6 +976,7 @@ static void apple_fast_die_sample(struct apple_fast_die *fd) if (limit != fd->limit) { WRITE_ONCE(fd->limit, limit); + apple_fast_die_cap(fd, limit); schedule_work(&fd->work); } } @@ -1060,6 +1150,7 @@ static int apple_fast_die_init(struct cpufreq_policy *policy, fd->limit); if (ret < 0) goto err_free; + apple_fast_die_cap(fd, fd->limit); priv->fast_die = fd; @@ -1310,6 +1401,8 @@ static int apple_soc_cpufreq_init(struct cpufreq_policy *policy) hrtimer_setup(&priv->verify_timer, apple_soc_cpufreq_verify, CLOCK_MONOTONIC, HRTIMER_MODE_REL_HARD); priv->pending_pstate = -1; + priv->cap_pstate = info->max_pstate; + priv->waited = true; policy->driver_data = priv; policy->freq_table = freq_table; From 47f1d5cafd23a6a7689f2038949f17931e4ff53f Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 12:57:04 +1100 Subject: [PATCH 131/388] drm/apple: make the panel EDID date and white point conformant libdisplay-info found a reserved year and an unset white point in the minimal panel EDID even though its identity getters were non-null. Use EDID 1.4 model year 2024 for the J616 family and nominal D65 reference white. Do not claim a manufacture date, calibrated primaries or gamma. Check those encoded fields in the host fixture; keep the independent consumer strict rather than suppressing its diagnostics. Assisted-by: LLM Signed-off-by: Ryan Murray --- Documentation/gpu/apple-dcp-t6041-panel.rst | 4 +++- drivers/gpu/drm/apple/dcp-display-metadata.c | 7 +++++++ tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py | 3 +++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Documentation/gpu/apple-dcp-t6041-panel.rst b/Documentation/gpu/apple-dcp-t6041-panel.rst index 79572e5099b37..564d7e3bb38c9 100644 --- a/Documentation/gpu/apple-dcp-t6041-panel.rst +++ b/Documentation/gpu/apple-dcp-t6041-panel.rst @@ -118,7 +118,9 @@ serial zero, represented by the non-empty serial descriptor ``00000000``. This generic identity does not claim to identify an individual panel. The preferred representable timing and range limits come from the retained DCP TimingElements catalog. Exact mode enumeration continues to use that catalog. -No new mode, color capability or machine serial is invented. The ADT reader, +The EDID records model year 2024 (not a manufacture date) and a nominal D65 +reference white point required by EDID parsing. No calibrated primaries, gamma, +new mode or machine serial is claimed. The ADT reader, EDID fallback and real EDID setters are restricted to desktop 26.6; T8140 continues to use its existing path. diff --git a/drivers/gpu/drm/apple/dcp-display-metadata.c b/drivers/gpu/drm/apple/dcp-display-metadata.c index 6355e697a653e..f04f651a4ac57 100644 --- a/drivers/gpu/drm/apple/dcp-display-metadata.c +++ b/drivers/gpu/drm/apple/dcp-display-metadata.c @@ -293,12 +293,19 @@ const struct drm_edid *dcp_panel_edid(const struct dcp_display_metadata *metadat block.product_id.manufacturer_name = cpu_to_be16(manufacturer); block.product_id.product_code = cpu_to_le16(metadata->product); block.product_id.serial_number = cpu_to_le32(metadata->serial); + /* J616 is a 2024 model; this is not a panel manufacture date. */ + block.product_id.week_of_manufacture = 0xff; + block.product_id.year_of_manufacture = 2024 - 1990; block.version = 1; block.revision = 4; block.input = DRM_EDID_INPUT_DIGITAL; block.width_cm = dimensions.width_cm; block.height_cm = dimensions.height_cm; block.gamma = 0xff; + /* Nominal D65 reference white, without calibrated primaries or gamma. */ + block.white_x = 320 >> 2; + block.white_y = 337 >> 2; + block.blue_white_lo = (320 & 3) << 2 | (337 & 3); block.features = DRM_EDID_FEATURE_PREFERRED_TIMING; memset(block.standard_timings, 1, sizeof(block.standard_timings)); snprintf(name, sizeof(name), "%.3s %04x", metadata->manufacturer, metadata->product); diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py index d36274f0e3117..55212094c14fa 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py @@ -478,6 +478,9 @@ def panel_identity(root, output=None): assert(bytes[8]==0x52 && bytes[9]==0x74 && bytes[10]==42 && !bytes[11]); assert(bytes[12]==0x78 && bytes[13]==0x56 && bytes[14]==0x34 && bytes[15]==0x12); assert(bytes[18]==1 && bytes[19]==4 && bytes[21]==35 && bytes[22]==22 && bytes[23]==0xff); + assert(bytes[16]==0xff && bytes[17]==34); + assert(((unsigned)bytes[33]<<2 | ((bytes[26]>>2)&3))==320); + assert(((unsigned)bytes[34]<<2 | (bytes[26]&3))==337); assert(((unsigned)bytes[66] | (unsigned)(bytes[68] >> 4) << 8)==345); assert(((unsigned)bytes[67] | (unsigned)(bytes[68] & 15) << 8)==223); assert(bytes[72+3]==EDID_DETAIL_MONITOR_NAME && bytes[90+3]==EDID_DETAIL_MONITOR_SERIAL); From be1528aa91837aa272fcb1325e3c51bbf0340086 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 13:06:15 +1100 Subject: [PATCH 132/388] media: apple: isp: add IMX958 P010 preview and config discovery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port the processed config-5 preview command sequence from our m1n1 J616s work, with contiguous V4L2 P010 strides and pool geometry. Query all advertised H17 sensor configs and retain the board defaults while appending consistent output modes. Keep NV12 the default and scope the new path to T6040/IMX958. Retire output and metadata leases after the report ACK, restart an empty P010 queue on requeue, bound ring servicing and latch malformed or timed-out command replies. Add command-byte, geometry, ownership and ring-wrap coverage. These source corrections still require sustained capture validation on 26.6.2. Builds on Jonathan Goyvaerts’ T6040 camera support in PR #384. Protocol reference: our m1n1 PR #20 at 9be380c8a5 and Linux PR #231 at a4e7ee86bc5e. No firmware payload is included. Assisted-by: LLM Signed-off-by: Ryan Murray --- .../media/drivers/apple-isp-j616s.rst | 258 +++++++++++++++ .../driver-api/media/drivers/index.rst | 1 + drivers/media/platform/apple/isp/Kconfig | 5 +- .../media/platform/apple/isp/isp-cam-test.c | 295 ++++++++++++++++++ drivers/media/platform/apple/isp/isp-cam.c | 141 ++++++++- drivers/media/platform/apple/isp/isp-cmd.c | 60 +++- drivers/media/platform/apple/isp/isp-cmd.h | 14 + drivers/media/platform/apple/isp/isp-drv.c | 1 + drivers/media/platform/apple/isp/isp-drv.h | 55 ++++ drivers/media/platform/apple/isp/isp-fw.c | 1 + .../media/platform/apple/isp/isp-ipc-test.c | 27 ++ drivers/media/platform/apple/isp/isp-ipc.c | 28 +- .../media/platform/apple/isp/isp-v4l2-test.c | 228 ++++++++++++++ drivers/media/platform/apple/isp/isp-v4l2.c | 229 ++++++++++---- drivers/media/platform/apple/isp/isp-v4l2.h | 1 + 15 files changed, 1265 insertions(+), 79 deletions(-) create mode 100644 Documentation/driver-api/media/drivers/apple-isp-j616s.rst create mode 100644 drivers/media/platform/apple/isp/isp-cam-test.c diff --git a/Documentation/driver-api/media/drivers/apple-isp-j616s.rst b/Documentation/driver-api/media/drivers/apple-isp-j616s.rst new file mode 100644 index 0000000000000..7413c0e866460 --- /dev/null +++ b/Documentation/driver-api/media/drivers/apple-isp-j616s.rst @@ -0,0 +1,258 @@ +.. SPDX-License-Identifier: GPL-2.0-only + +J616s ISP modes and capture protocol +================================== + +Sources and scope +----------------- + +The source reference is Ryan's m1n1 PR 20, branch ``j616s`` at +``9be380c8a5a94609fdbe62d693c7b2121478ad8f``: + +* ``proxyclient/m1n1/fw/isp/isp_sensor.py`` discovers IMX958 and queries + ``CH_CAMERA_CONFIG_GET`` for every configuration reported by ``CH_INFO_GET``. +* ``isp_capture.py:T6040PreviewConfiguration`` contains the successful + processed capture command sequence. ``isp_pixels.py`` decodes its P010 + output as video-range BT.709. +* ``docs/j616s-bringup.md`` records all 13 configuration queries, completed + 1280x720 P010 captures (including an 80-frame ring-wrap trial) and a later + single completed 1920x1080 P010 frame. Intermittent startup/streaming stalls + remain unresolved. Neither the requested AE limits nor the host capture + rate is a measured sensor FPS. + +Both processed output sizes use firmware configuration **5**, whose reported +input dimensions are 1920x2160. The branch does not establish that this input +height is an independently deliverable 2160-row colour image, nor that config +5 is the highest-resolution configuration the sensor offers. It contains no +completed colour result from another configuration. RAW trials produced +stable pixels after stop but no completed RAW report; combined RAW/YUV was +refused at the allocation limit. No sensor full-resolution claim follows. + +Linux PR 231, branch ``j616s/linux-integration-next`` at +``a4e7ee86bc5eed8ff9dab61ef9656adfe000b207``, has the corresponding +``isp-profile.[ch]`` and IPC/buffer preparation. Its T6040 profile is pinned +to **25G76** and remains gated. It is a protocol reference, not an admission +profile for the current firmware. + +This driver change builds on the working T6040 H17 implementation in Linux +PR 384, now in ``integration/aurora``. That PR reports 26.6.x firmware capture +at 1920x1080 NV12 and use of config-5 1080p/720p presets. P010 and additional +firmware-derived output modes still need capture validation on 26.6.2. +Only our source and synthetic protocol tests are included; no Apple binary, +setfile contents, calibration table or firmware image is added. + +Mode and format selection +------------------------- + +On T6040/IMX958, probe queries every advertised configuration, bounded at 64, +using the 0x120-byte H17 ``CH_CAMERA_CONFIG_GET`` exchange. The complete list +is available through dynamic debug at probe and cached for +``VIDIOC_LOG_STATUS`` (``v4l2-ctl --log-status``), without sending commands +during a stream. The log includes +input/output dimensions and the sensor clock word; that word is not FPS. + +The board's existing config-5 1920x1080 and 1280x720 presets stay first and +NV12 stays the default. Additional distinct output sizes are appended only +when the reply's 16-bit and repeated 32-bit dimensions agree, the dimensions +are nonzero, output is even and within input, and output is at most 16384 +in either dimension. The full reported output becomes the crop and output +size for its own config index. Input dimensions alone never add a mode. +Thus a larger consistent firmware output can be selected by ``S_FMT`` and +listed by ``ENUM_FRAMESIZES``; no invented full-resolution preset is added. +These consistency checks cannot establish image quality, supported FPS or +whether a configuration needs additional sensor-specific commands. + +``V4L2_PIX_FMT_P010`` is offered only for T6040/IMX958. The config-5 +1280x720 and 1920x1080 processed previews are enabled when +the runtime input geometry matches the source profile. An unspecified P010 +size selects 1280x720 first. Larger P010 modes require a consistent firmware +output reply. P010 uses two little-endian 16-bit component planes with ten useful +MSB-aligned bits, and equal 64-byte-aligned Y/UV strides. UV immediately +follows Y, as required for contiguous P010. The optional multi-planar V4L2 +API also reports P010 as **one memory plane** containing both component +planes. The firmware still receives two component addresses. + +At 1920x1080, stride is 3840, Y is 4147200 bytes, UV is 2073600 bytes and +``sizeimage`` is 6220800. At 1280x720, stride is 2560 and ``sizeimage`` is +2764800. The proxy's independently page-aligned plane extents were +0x3f8000/0x1fc000 at 1080p and 0x1c4000/0xe4000 at 720p; inserting those +inter-plane gaps into a V4L2 contiguous P010 buffer would misdescribe it. +Linux passes its actual contiguous extents to the rendered pool instead. + +P010 uses the proxy's eight metadata slots (0x8000 bytes each) and two +rendered slots. The queue limits userspace to two rendered buffers, matching +the pool capacity. Completion matches both zero-extended low-32-bit plane +addresses, plane count, pool 9 and a fresh 64-bit submission tag; an old tag +cannot release a new lease. Buffer completion is published to videobuf2 +only after the firmware report ACK and doorbell have been published. +NV12 pool sizes and the T8140/Neo formats, presets and command path retain +their existing behavior. + +Endpoints +--------- + +These are the seven shared-memory ISP channels, not AOP/RTKit endpoints. +The table records the 25G76 source fixture. The current driver negotiates +channel names, types, sources, counts and 64-bit addresses on each boot; +it does not impose the captured ring addresses or counts on 26.6.2. + +================= ==== ====== ===== ======================================= +Channel Type Source Slots Purpose +================= ==== ====== ===== ======================================= +TERMINAL 2 0 768 Firmware log reports / terminal ACKs +IO 0 1 8 Host commands and in-place replies +DEBUG 0 1 8 Discovered; no capture commands sent +BUF_H2T 0 2 64 Host metadata/output descriptor batches +BUF_T2H 1 3 64 Firmware buffer completion/return reports +SHAREDMALLOC 1 3 8 Firmware allocation/free requests +IO_T2H 1 3 8 Discovered; no capture handler installed +================= ==== ====== ===== ======================================= + +A source word selects a doorbell bit, so IO and DEBUG share bit 1; the +three reply channels share bit 3. Each channel descriptor is 0x100 bytes +with a u64 ring address at 0x50. Each slot is 0x40 bytes / eight u64 words. +Publish the payload before arg0, which transfers slot ownership. The +existing IPC parser bounds rings against the allocation, descriptor table, +boot/command storage and other rings. The threaded IRQ services allocations, +terminal messages and buffer reports while a command waits for its ACK. + +Command stream +-------------- + +IO commands have a zero/status u32 followed by the opcode u32, then the +parameters below. Sizes include that eight-byte header. ``ch`` is zero. + +================ =================================== ==== =================== +Opcode Command Size Parameters +================ =================================== ==== =================== +0x0003 CONFIG_GET 0x1c Camera channel count +0x3206 SET_DSID_CLR_MULTI_BC_REG_BASE 0x50 8 slots, range 0x2fc +0x0000 START 0x0c Proxy 0x11, Linux 0 +0x010d CH_INFO_GET 0x190 ch +0x0106 CH_CAMERA_CONFIG_GET 0x120 ch, index (all) +0x0024 FLICKER_SENSOR_SET 0x0c 0 +0x013b CH_SBS_ENABLE 0x10 ch, 1 +0x0107 CH_CAMERA_CONFIG_SELECT 0x10 ch, selected config +0x010e CH_BUFFER_RECYCLE_MODE_SET 0x10 ch, 1 (empty only) +0x010f CH_BUFFER_RECYCLE_START 0x0c ch +0x080c CH_CROP_SCL1_SET 0x1c ch, x, y, width, height +0x0b09 CH_OUTPUT_CONFIG_SCL1_SET 0x38 ch, output geometry +0x0208 CH_AE_FRAME_RATE_MAX_SET 0x10 ch, fps * 256 +0x020a CH_AE_FRAME_RATE_MIN_SET 0x10 ch, min_fps * 256 +0x0117 CH_BUFFER_POOL_CONFIG_SET (meta) 0x9c ch, pool 0, count 8 +0x0117 CH_BUFFER_POOL_CONFIG_SET (output) 0x9c ch, pool 9, count 2 +0x0125 CH_LOCAL_RAW_BUFFER_ENABLE 0x10 ch, 1 +0x0b0d CH_PREVIEW_STREAM_SET 0x10 ch, 1 +0x0138 CH_MASTER_SLAVE_SYNC_MODE_SET 0x10 ch, 0 +0x0100 CH_START 0x0c ch +0x0101 CH_STOP 0x0c ch +0x0104 CH_BUFFER_RETURN 0x0c ch +================ =================================== ==== =================== + +The DSID bases are 0x2201d4000, 0x2221d4000, 0x2241d4000, 0x2261d4000, +then four U64_MAX sentinels. Linux retains its existing boot-time PMP and +logging commands in addition to discovery; it does not replay the proxy's +boot allocations or private power orchestration. + +For config 5, crop is (0, 0, output width, output height). The output command +parameters after ch are width, height, Rec.709=1, **format=0x12**, Y stride, +UV stride, 0, 0, height, 0, width. NV12 continues to use format 0. +Metadata pool sizes/strides are 0x8000, one data block; the output pool +contains actual Y/UV sizes and strides, two data blocks, no compression. + +Before CH_START, BUF_H2T submits metadata and output buffers as a 16-byte +header (type 1, count) and 64-byte descriptors. Each descriptor contains +four u64 addresses, four u32 flags (0x40000000 for each used plane), +u32 plane count, u32 pool and u64 tag. BUF_T2H returns completion reports; +render status 0x10000000 means a completed frame. Submission status is +0x30000000 and return ACK status is 0x80000000. Requeue only a returned lease. +CH_STOP and CH_BUFFER_RETURN retire the stream; resident firmware remains +running for the existing reopen path. + +The proxy used AE max/min 7680/3840 (30/15 fps). Linux retains its V4L2 rate +and exposure-priority controls: the default holds 30/30, and enabling +``exposure_auto_priority`` allows 30/15. These are requested limits, not FPS +measurements. P010 follows the minimal proxy sequence without adding +setfile-load, CNR, MBNR, SIF-format or temporal-filter commands; NV12 keeps +its existing calibrated path. A previously loaded setfile may still be +resident when switching streams. + +26.6.2 comparison +----------------- + +The 25G76 firmware UUID differs from the 26.6.2 image. Matching struct sizes +is source-level ABI evidence, not a new live qualification of P010. + +* Boot block: both use 0x290 bytes, IPC address at 0x08, shared base/size at + 0x10/0x18, extra heap at 0x20/0x28, platform at 0x30, queue offset+1 at + 0x50, 0x40 at 0x68, camera scheme at 0xd0 and the no-optical-card flag + at 0x120. The current H17 path checks descriptor flag bit 1. +* Memory: 25G76 pinned TEXT/DATA sizes 0xca8000/0x1344000, remap 1<<40, + shared base 0x1fec000, and a 256 MiB shared aperture. Linux uses the + bootloader's current TEXT/DATA/main-memory reservations, backs the missing + main-memory tail, and retains T6040's **0xde600000** shared-size descriptor. + No pinned segment size, heap request, IOVA or firmware hash is ported. +* IPC: 0x100-byte descriptors, 0x40-byte slots, 64-bit addresses and the + seven names agree. Counts and addresses remain negotiated, with the + existing overlap checks retained. The driver uses its three-DART IOMMU + domain and bootloader address-filter setup, not proxy DART table writes. +* Commands: DSID's 0x50-byte eight-slot layout, + CH_INFO_GET's 0x190-byte record, scaler-1 crop/output, 0x9c-byte pools, + local RAW, preview, sync and channel start/stop layouts agree with the + current H17 builders. START is an important parameter difference: the + proxy sends 0x11, while the working Linux H17 boot path sends 0. Linux + retains its current boot command; issuing START a second time after + selecting P010 would violate the proxy's guarded startup lifecycle. The new config query explicitly exchanges 0x120, + rather than the unused legacy 0xdc current-config helper. Header/channel/ + index checks and duplicated geometry checks guard decoding. The H17 + appended bytes remain uninterpreted. +* Metadata: CH_INFO_GET reports 0x4d00 on both paths. The proxy rounds its + metadata backing/pool to 0x8000; P010 does the same even on a 4 KiB host. + NV12 retains the current driver's 0x4d00 pool and 16-buffer capacity. + Neither path configures a T6040 capture-metadata pool or capture watchdog. +* Startup: Linux retains its once-per-boot resident-firmware admission, + current reset-vector check and H17 handshake; the 25G76 SHA/UUID admission + and recovery model from PR 231 are not substituted. Linux's command + timeout/IRQ service remain in use; the proxy's 30-second inactivity / + 40-second wall startup budget is not copied. + +Validation on hardware must record the current config list first, then +exercise both P010 presets, requeue, stop/reopen and a longer run. If a larger +firmware output appears, test its format and command requirements separately. +Do not label input dimensions, a successful query, or a requested rate as a +completed full-resolution stream or measured FPS. + +Open qualification and performance checks +----------------------------------------- + +``tools/testing/selftests/media/apple-isp-stress.c`` exercises long runs, +repeated start/stop and format/size switches through MMAP. It logs completed +frames, delivered FPS, maximum inter-frame gap, sequence gaps and buffer errors. +Use the established nominal rate as its explicit rate argument. A driver-generated +sequence is not a firmware frame counter, so timing and sequence checks alone +cannot prove the sensor dropped no frames internally. + +The P010 path requeues even the last returned lease into an otherwise empty +queue, publishes completion ACKs before releasing output or metadata leases, +acquires message validity before reading payload and bounds each IRQ ring drain +to one ring traversal so terminal/allocation traffic cannot starve completions. +Command replies must match the requested reply length, zero status and original +header. A timeout or malformed ACK latches failure; no late ACK can authorize +another command. The existing forced coprocessor stop runs before uncertain +leases are released. Resident firmware then needs a reboot before another capture. +These corrections address source-visible stalls/races. They are not evidence +that the intermittent stalls observed by the USB proxy have been reproduced +or cured on the current firmware. + +No verified IMX958 RAW Bayer packing/order, RAW completion path or nominal FPS +for every configuration is present in the public source. The recorded RAW trial +reads stable memory after stop but receives no completed RAW report. A Bayer +FOURCC cannot be chosen from that evidence. The RAW format and an exhaustive +per-config FPS table remain open pending the original capture evidence and +runtime command replies; neither is inferred from the P010 output or input height. +The 30/15 values in the preview are AE requests, not per-mode nominal measurements. + +The T6040 device tree's current fixed TEXT/DATA reservations should eventually +come from a bootloader handoff describing the loaded image and mapping lifetimes. +They are firmware-build-specific. This change does not implement that handoff +or replace those reservations with 25G76 addresses. diff --git a/Documentation/driver-api/media/drivers/index.rst b/Documentation/driver-api/media/drivers/index.rst index 7f6f3dcd5c90c..f1ead3c9e389f 100644 --- a/Documentation/driver-api/media/drivers/index.rst +++ b/Documentation/driver-api/media/drivers/index.rst @@ -12,6 +12,7 @@ Video4Linux (V4L) drivers .. toctree:: :maxdepth: 5 + apple-isp-j616s bttv-devel cx2341x-devel cx88-devel diff --git a/drivers/media/platform/apple/isp/Kconfig b/drivers/media/platform/apple/isp/Kconfig index b51d19ee115de..7a797a1a7e1a3 100644 --- a/drivers/media/platform/apple/isp/Kconfig +++ b/drivers/media/platform/apple/isp/Kconfig @@ -24,8 +24,9 @@ config VIDEO_APPLE_ISP_KUNIT_TEST depends on KUNIT=y || VIDEO_APPLE_ISP=m default KUNIT_ALL_TESTS help - Builds unit tests for the videobuf2 queue set-up and for the - shared memory the firmware frees into the Apple ISP driver. They + Builds unit tests for queue setup, P010 layouts and buffer identity, + firmware sensor geometry, the capture command stream and shared + memory retirement. The command tests stub the IPC transport and need no hardware. Only useful for kernel developers running KUnit. If unsure, say N. diff --git a/drivers/media/platform/apple/isp/isp-cam-test.c b/drivers/media/platform/apple/isp/isp-cam-test.c new file mode 100644 index 0000000000000..aef545b27c051 --- /dev/null +++ b/drivers/media/platform/apple/isp/isp-cam-test.c @@ -0,0 +1,295 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* Synthetic geometry checks: no camera firmware or setfile data. */ +#include +#include +#include +#include "isp-ipc.h" + +static void isp_test_sensor_geometry(struct kunit *test) +{ + struct cmd_ch_camera_config config = { + .in_width = 1920, .in_height = 2160, + .out_width = 1920, .out_height = 1080, + .in_width2 = 1920, .in_height2 = 2160, + .out_width2 = 1920, .out_height2 = 1080, + }; + + KUNIT_EXPECT_TRUE(test, isp_sensor_config_usable(&config)); + /* A future larger output is accepted only when both descriptions agree. */ + config.in_width = 4096; + config.in_width2 = 4096; + config.in_height = 3072; + config.in_height2 = 3072; + config.out_width = 4096; + config.out_width2 = 4096; + config.out_height = 3072; + config.out_height2 = 3072; + KUNIT_EXPECT_TRUE(test, isp_sensor_config_usable(&config)); + config.out_height2 = 2160; + KUNIT_EXPECT_FALSE(test, isp_sensor_config_usable(&config)); + config.out_height2 = 3072; + config.in_height2 = 2160; + KUNIT_EXPECT_FALSE(test, isp_sensor_config_usable(&config)); + config.in_height2 = 3072; + config.out_width = 4097; + config.out_width2 = 4097; + KUNIT_EXPECT_FALSE(test, isp_sensor_config_usable(&config)); + config.out_width = 0; + config.out_width2 = 0; + KUNIT_EXPECT_FALSE(test, isp_sensor_config_usable(&config)); + config.out_width = 8192; + config.out_width2 = 8192; + KUNIT_EXPECT_FALSE(test, isp_sensor_config_usable(&config)); +} + +struct isp_test_command { + u32 words[39]; + u32 size; +}; + +/* Derived from the proxy command fixture, with contiguous V4L2 plane sizes. */ +static const struct isp_test_command isp_test_preview_commands[] = { + {{0, 0x24, 0}, 0xc}, + {{0, 0x13b, 0, 1}, 0x10}, + {{0, 0x107, 0, 5}, 0x10}, + {{0, 0x10e, 0, 1}, 0x10}, + {{0, 0x10f, 0}, 0xc}, + {{0, 0x80c, 0, 0, 0, 1280, 720}, 0x1c}, + {{0, 0xb09, 0, 1280, 720, 1, 0x12, 2560, 2560, 0, 0, 720, 0, 1280}, 0x38}, + {{0, 0x208, 0, 7680}, 0x10}, + {{0, 0x20a, 0, 3840}, 0x10}, + {{[1] = 0x117, [3] = 8 << 16, [4] = 0x8000, [5] = 0x8000, [37] = 1}, 0x9c}, + {{[1] = 0x117, [3] = 9 | 2 << 16, [4] = 1843200, [5] = 2560, + [12] = 921600, [13] = 2560, [37] = 2}, 0x9c}, + {{0, 0x125, 0, 1}, 0x10}, + {{0, 0xb0d, 0, 1}, 0x10}, + {{0, 0x138, 0, 0}, 0x10}, +}; + +struct isp_test_command_trace { + unsigned int count; + unsigned int fail_at; +}; + +static int isp_test_send(struct apple_isp *isp, void *args, u32 insize, + u32 outsize, int timeout) +{ + struct kunit *test = kunit_get_current_test(); + struct isp_test_command_trace *trace = test->priv; + const struct isp_test_command *expected; + + if (trace->count >= ARRAY_SIZE(isp_test_preview_commands)) { + KUNIT_FAIL(test, "unexpected extra command"); + return -EIO; + } + expected = &isp_test_preview_commands[trace->count++]; + KUNIT_EXPECT_EQ(test, insize, expected->size); + KUNIT_EXPECT_EQ(test, outsize, expected->words[1] == 0x117 ? insize : 0); + if (insize == expected->size) + KUNIT_EXPECT_MEMEQ(test, args, expected->words, insize); + if (trace->count == trace->fail_at) + return -ETIME; + return 0; +} + +static void isp_test_free_controls(void *ptr) +{ + v4l2_ctrl_handler_free(ptr); +} + +static struct apple_isp *isp_test_capture_alloc(struct kunit *test) +{ + static const struct apple_isp_hw hw = { + .gen = ISP_GEN_T6040, .fw_abi = ISP_FW_ABI_H17, + .scl1 = true, .meta_size = ISP_META_SIZE_T8140, + }; + static struct isp_preset preset = { + .index = 5, .input_dim = {1920, 2160}, + .output_dim = {1280, 720}, .crop_size = {1280, 720}, + }; + struct apple_isp *isp = kunit_kzalloc(test, sizeof(*isp), GFP_KERNEL); + struct isp_format *fmt; + int err; + + KUNIT_ASSERT_NOT_NULL(test, isp); + isp->dev = kunit_device_register(test, "apple-isp-camera-test"); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, isp->dev); + isp->hw = &hw; + isp->frame_rate = 30; + fmt = isp_get_current_format(isp); + fmt->version = 0x958; + fmt->pixelformat = V4L2_PIX_FMT_P010; + fmt->preset = &preset; + fmt->strides[0] = 2560; + fmt->strides[1] = 2560; + fmt->plane_size[0] = 1843200; + fmt->plane_size[1] = 921600; + v4l2_ctrl_handler_init(&isp->ctrl_handler, 1); + err = kunit_add_action_or_reset(test, isp_test_free_controls, &isp->ctrl_handler); + KUNIT_ASSERT_EQ(test, err, 0); + isp->exposure_priority = + v4l2_ctrl_new_std(&isp->ctrl_handler, NULL, + V4L2_CID_EXPOSURE_AUTO_PRIORITY, 0, 1, 1, 1); + KUNIT_ASSERT_EQ(test, isp->ctrl_handler.error, 0); + kunit_activate_static_stub(test, isp_cmd_send, isp_test_send); + return isp; +} + +static void isp_test_preview_stream(struct kunit *test) +{ + struct apple_isp *isp = isp_test_capture_alloc(test); + struct isp_test_command_trace trace = {}; + + test->priv = &trace; + KUNIT_EXPECT_EQ(test, isp_ch_configure_capture(isp, 0), 0); + KUNIT_EXPECT_EQ(test, trace.count, ARRAY_SIZE(isp_test_preview_commands)); +} + +static void isp_test_preview_command_failures(struct kunit *test) +{ + struct apple_isp *isp = isp_test_capture_alloc(test); + struct isp_test_command_trace trace; + + test->priv = &trace; + for (unsigned int i = 1; i <= ARRAY_SIZE(isp_test_preview_commands); i++) { + trace = (struct isp_test_command_trace){ .fail_at = i }; + KUNIT_EXPECT_EQ(test, isp_ch_configure_capture(isp, 0), -ETIME); + KUNIT_EXPECT_EQ(test, trace.count, i); + } +} + +static int isp_test_config_reply(struct apple_isp *isp, void *args, u32 insize, + u32 outsize, int timeout) +{ + struct kunit *test = kunit_get_current_test(); + u32 *count = test->priv; + struct cmd_ch_camera_config_h17 *reply = isp->cmd_virt; + struct cmd_ch_camera_config *config = &reply->config; + + KUNIT_EXPECT_EQ(test, insize, 0x120); + KUNIT_EXPECT_EQ(test, outsize, 0x120); + memcpy(reply, args, sizeof(*reply)); + KUNIT_EXPECT_EQ(test, config->preset, (*count)++); + config->in_width = 1920; + config->in_height = 2160; + config->out_width = 1920; + config->out_height = 1080; + if (config->preset == 1) { + config->in_width = 4096; + config->in_height = 3072; + config->out_width = 4096; + config->out_height = 3072; + } + config->in_width2 = config->in_width; + config->in_height2 = config->in_height; + config->out_width2 = config->out_width; + config->out_height2 = config->out_height; + if (config->preset == 2) + config->out_height2 = 2160; + return 0; +} + +static void isp_test_sensor_config_list(struct kunit *test) +{ + struct apple_isp *isp = isp_test_capture_alloc(test); + struct isp_preset presets[] = { + { .index = 5, .input_dim = {1920, 2160}, + .output_dim = {1920, 1080}, .crop_size = {1920, 1080} }, + { .index = 5, .input_dim = {1920, 2160}, + .output_dim = {1280, 720}, .crop_size = {1280, 720} }, + }; + u32 count = 0; + + isp->cmd_virt = kunit_kzalloc(test, 0x120, GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, isp->cmd_virt); + isp->presets = presets; + isp->num_presets = ARRAY_SIZE(presets); + test->priv = &count; + kunit_activate_static_stub(test, isp_cmd_send, isp_test_config_reply); + KUNIT_ASSERT_EQ(test, isp_ch_cache_sensor_configs(isp, 0, 6), 0); + KUNIT_EXPECT_EQ(test, count, 6); + KUNIT_EXPECT_EQ(test, isp->num_sensor_configs, 6); + KUNIT_EXPECT_EQ(test, isp->num_presets, 3); + KUNIT_EXPECT_EQ(test, isp->presets[0].index, 5); + KUNIT_EXPECT_EQ(test, isp->presets[0].output_dim.y, 1080); + KUNIT_EXPECT_TRUE(test, isp->presets[1].p010); + KUNIT_EXPECT_TRUE(test, isp->presets[2].p010); + KUNIT_EXPECT_EQ(test, isp->presets[2].index, 1); + KUNIT_EXPECT_EQ(test, isp->presets[2].output_dim.x, 4096); + KUNIT_EXPECT_EQ(test, isp->presets[2].crop_size.y, 3072); + KUNIT_EXPECT_FALSE(test, isp->sensor_configs[2].usable); + KUNIT_EXPECT_EQ(test, isp_ch_cache_sensor_configs(isp, 0, 0), -EINVAL); + KUNIT_EXPECT_EQ(test, isp_ch_cache_sensor_configs(isp, 0, 65), -EINVAL); + KUNIT_EXPECT_EQ(test, count, 6); +} + +struct isp_test_ack_state { + unsigned int fault, calls; +}; + +static int isp_test_ack_reply(struct apple_isp *isp, struct isp_channel *chan, + unsigned long timeout) +{ + struct kunit *test = kunit_get_current_test(); + struct isp_test_ack_state *state = test->priv; + + state->calls++; + chan->rsp = (struct isp_message){ .arg0 = chan->req.arg0 | ISP_IPC_FLAG_ACK, + .arg1 = chan->req.arg2 }; + switch (state->fault) { + case 1: + chan->rsp.arg2 = 1; + break; + case 2: + put_unaligned_le32(1, isp->cmd_virt); + break; + case 3: + chan->rsp.arg1 += 4; + break; + case 4: + return -ETIME; + } + return 0; +} + +static void isp_test_command_ack_failure_latches(struct kunit *test) +{ + struct apple_isp *isp = isp_test_capture_alloc(test); + struct isp_test_ack_state state = {}; + + isp->cmd_virt = kunit_kzalloc(test, 0x400, GFP_KERNEL); + isp->chan_io = kunit_kzalloc(test, sizeof(*isp->chan_io), GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, isp->cmd_virt); + KUNIT_ASSERT_NOT_NULL(test, isp->chan_io); + isp->chan_io->name = "IO"; + test->priv = &state; + kunit_deactivate_static_stub(test, isp_cmd_send); + kunit_activate_static_stub(test, ipc_chan_send, isp_test_ack_reply); + KUNIT_EXPECT_EQ(test, isp_cmd_ch_camera_config_select(isp, 0, 5), 0); + for (state.fault = 1; state.fault <= 4; state.fault++) { + isp->ipc_failed = false; + state.calls = 0; + KUNIT_EXPECT_EQ(test, isp_cmd_ch_camera_config_select(isp, 0, 5), + state.fault == 4 ? -ETIME : -EIO); + KUNIT_EXPECT_TRUE(test, isp->ipc_failed); + /* A late ACK cannot admit another command on an uncertain slot. */ + KUNIT_EXPECT_EQ(test, isp_cmd_ch_camera_config_select(isp, 0, 5), -EIO); + KUNIT_EXPECT_EQ(test, state.calls, 1); + } +} + +static struct kunit_case isp_cam_test_cases[] = { + KUNIT_CASE(isp_test_command_ack_failure_latches), + KUNIT_CASE(isp_test_sensor_geometry), + KUNIT_CASE(isp_test_sensor_config_list), + KUNIT_CASE(isp_test_preview_stream), + KUNIT_CASE(isp_test_preview_command_failures), + {} +}; + +static struct kunit_suite isp_cam_test_suite = { + .name = "apple-isp-camera", + .test_cases = isp_cam_test_cases, +}; + +kunit_test_suite(isp_cam_test_suite); diff --git a/drivers/media/platform/apple/isp/isp-cam.c b/drivers/media/platform/apple/isp/isp-cam.c index a266c60609614..59f19e571bb64 100644 --- a/drivers/media/platform/apple/isp/isp-cam.c +++ b/drivers/media/platform/apple/isp/isp-cam.c @@ -10,8 +10,6 @@ #include "isp-iommu.h" #include "isp-regs.h" -#define ISP_MAX_PRESETS 32 - struct isp_setfile { u32 version; u32 magic; @@ -217,6 +215,110 @@ static int isp_check_presets(struct apple_isp *isp, u32 num_fw_presets) return 0; } +/* Do not mistake stacked sensor input rows for a processed output mode. */ +static bool isp_sensor_config_usable(const struct cmd_ch_camera_config *config) +{ + return config->in_width && config->in_height && + config->out_width && config->out_height && + config->out_width <= 16384 && config->out_height <= 16384 && + !(config->out_width & 1) && !(config->out_height & 1) && + config->out_width <= config->in_width && + config->out_height <= config->in_height && + config->in_width == config->in_width2 && + config->in_height == config->in_height2 && + config->out_width == config->out_width2 && + config->out_height == config->out_height2; +} + +static int isp_ch_cache_sensor_configs(struct apple_isp *isp, u32 ch, u32 count) +{ + struct cmd_ch_camera_config_h17 args; + struct isp_preset *presets; + unsigned int num = isp->num_presets; + int err; + + /* The extended reply is established only for the IMX958 T6040 path. */ + if (!isp_supports_p010(isp)) + return 0; + if (!count || count > ISP_MAX_SENSOR_CONFIGS) + return -EINVAL; + + presets = devm_kcalloc(isp->dev, num + count, sizeof(*presets), GFP_KERNEL); + if (!presets) + return -ENOMEM; + memcpy(presets, isp->presets, num * sizeof(*presets)); + + for (u32 i = 0; i < count; i++) { + struct cmd_ch_camera_config *config = &args.config; + struct isp_sensor_config *cached = &isp->sensor_configs[i]; + bool duplicate = false; + + err = isp_cmd_ch_camera_config_get(isp, ch, i, &args); + if (err) + return err; + if (config->opcode != (u64)CISP_CMD_CH_CAMERA_CONFIG_GET << 32 || + config->chan != ch || config->preset != i) + return -EIO; + + cached->input_dim = (struct coord){config->in_width, config->in_height}; + cached->output_dim = (struct coord){config->out_width, config->out_height}; + cached->sensor_clk = config->sensor_clk; + cached->usable = isp_sensor_config_usable(config); + dev_dbg(isp->dev, "sensor config %u: input %ux%u output %ux%u clock %u%s\n", + i, config->in_width, config->in_height, + config->out_width, config->out_height, config->sensor_clk, + cached->usable ? "" : " (unverified geometry)"); + print_hex_dump_debug("apple-isp config: ", DUMP_PREFIX_OFFSET, + 16, 4, &args, sizeof(args), false); + /* Both processed preview sizes are recorded in the proxy source. */ + for (unsigned int j = 0; j < num; j++) { + struct isp_preset *preset = &presets[j]; + bool preview = i == 5 && config->in_width == 1920 && + config->in_height == 2160 && + ((preset->output_dim.x == 1280 && + preset->output_dim.y == 720) || + (preset->output_dim.x == 1920 && + preset->output_dim.y == 1080)); + bool reported = cached->usable && + preset->output_dim.x == config->out_width && + preset->output_dim.y == config->out_height; + + if (preset->index == i && + preset->input_dim.x == config->in_width && + preset->input_dim.y == config->in_height && + !preset->crop_offset.x && !preset->crop_offset.y && + preset->crop_size.x == preset->output_dim.x && + preset->crop_size.y == preset->output_dim.y) + preset->p010 = preview || reported; + } + if (!cached->usable) + continue; + + /* Keep the board's config-5 presets, including the default, first. */ + for (unsigned int j = 0; j < num; j++) { + if (presets[j].output_dim.x == config->out_width && + presets[j].output_dim.y == config->out_height) { + duplicate = true; + break; + } + } + if (duplicate) + continue; + + presets[num++] = (struct isp_preset){ + .index = i, + .input_dim = cached->input_dim, + .output_dim = cached->output_dim, + .crop_size = cached->output_dim, + .p010 = true, + }; + } + isp->num_sensor_configs = count; + isp->presets = presets; + isp->num_presets = num; + return 0; +} + static int isp_ch_cache_sensor_info(struct apple_isp *isp, u32 ch) { struct isp_format *fmt = isp_get_format(isp, ch); @@ -264,6 +366,8 @@ static int isp_ch_cache_sensor_info(struct apple_isp *isp, u32 ch) } err = isp_check_presets(isp, args->num_presets); + if (!err) + err = isp_ch_cache_sensor_configs(isp, ch, args->num_presets); exit: kfree(args); @@ -475,7 +579,7 @@ static int isp_ch_configure_pools(struct apple_isp *isp, u32 ch) return 0; return isp_cmd_ch_buffer_pool_config_set_rendered(isp, ch, - ISP_MAX_BUFFERS, + isp_num_rendered(isp), fmt->plane_size[0], fmt->strides[0], fmt->plane_size[1], @@ -488,7 +592,13 @@ static int isp_ch_configure_capture(struct apple_isp *isp, u32 ch) bool h17 = isp->hw->fw_abi == ISP_FW_ABI_H17; int err; - isp_cmd_flicker_sensor_set(isp, 0); + err = isp_cmd_flicker_sensor_set(isp, 0); + if (isp_is_p010(isp) && err) + return err; + + /* The proxy-qualified P010 route uses the firmware defaults. */ + if (isp_is_p010(isp)) + goto select_config; /* * The setfile isn't requisite but then we don't get calibration. If @@ -504,6 +614,7 @@ static int isp_ch_configure_capture(struct apple_isp *isp, u32 ch) return err; } +select_config: err = isp_cmd_ch_sbs_enable(isp, ch, 1); if (err) return err; @@ -531,7 +642,8 @@ static int isp_ch_configure_capture(struct apple_isp *isp, u32 ch) err = isp_cmd_ch_output_config_set(isp, ch, fmt->preset->output_dim.x, fmt->preset->output_dim.y, fmt->strides, CISP_COLORSPACE_REC709, - CISP_OUTPUT_FORMAT_YUV_2PLANE); + isp_is_p010(isp) ? CISP_OUTPUT_FORMAT_P010 : + CISP_OUTPUT_FORMAT_YUV_2PLANE); if (err) return err; @@ -559,6 +671,9 @@ static int isp_ch_configure_capture(struct apple_isp *isp, u32 ch) if (err) return err; + if (isp_is_p010(isp)) + return isp_cmd_ch_master_slave_sync_mode_set(isp, ch, 0); + err = isp_cmd_ch_cnr_start(isp, ch); if (err) return err; @@ -633,7 +748,10 @@ int apple_isp_start_camera(struct apple_isp *isp) err = isp_configure_capture(isp); if (err) { dev_err(isp->dev, "failed to configure capture: %d\n", err); - apple_isp_firmware_shutdown(isp); + if (isp->ipc_failed) + apple_isp_firmware_halt(isp); + else + apple_isp_firmware_shutdown(isp); return err; } @@ -642,7 +760,10 @@ int apple_isp_start_camera(struct apple_isp *isp) void apple_isp_stop_camera(struct apple_isp *isp) { - apple_isp_firmware_shutdown(isp); + if (isp->ipc_failed) + apple_isp_firmware_halt(isp); + else + apple_isp_firmware_shutdown(isp); } /* @@ -714,9 +835,15 @@ int apple_isp_start_capture(struct apple_isp *isp) void apple_isp_stop_capture(struct apple_isp *isp) { + if (isp->ipc_failed || isp->fw_state == ISP_FW_DEAD) + return; isp_cmd_ch_stop(isp, 0); // TODO channel mask isp_cmd_ch_buffer_return(isp, isp->current_ch); /* ... until its buffers are back. */ apple_isp_wdt_stop(isp); } + +#if IS_ENABLED(CONFIG_VIDEO_APPLE_ISP_KUNIT_TEST) +#include "isp-cam-test.c" +#endif diff --git a/drivers/media/platform/apple/isp/isp-cmd.c b/drivers/media/platform/apple/isp/isp-cmd.c index fd7dcfd52899a..a525e68c230f5 100644 --- a/drivers/media/platform/apple/isp/isp-cmd.c +++ b/drivers/media/platform/apple/isp/isp-cmd.c @@ -5,30 +5,54 @@ #include "isp-drv.h" #include "isp-iommu.h" #include "isp-ipc.h" +#include + +#if IS_ENABLED(CONFIG_VIDEO_APPLE_ISP_KUNIT_TEST) +#include +#endif #define CISP_OPCODE_SHIFT 32UL #define CISP_OPCODE(x) (((u64)(x)) << CISP_OPCODE_SHIFT) #define CISP_OPCODE_GET(x) (((u64)(x)) >> CISP_OPCODE_SHIFT) #define CISP_TIMEOUT msecs_to_jiffies(3000) -#define CISP_SEND_IN(x, a) (cisp_send((x), &(a), sizeof(a), 0, CISP_TIMEOUT)) -#define CISP_SEND_INOUT(x, a) (cisp_send((x), &(a), sizeof(a), sizeof(a), CISP_TIMEOUT)) +#define CISP_SEND_IN(x, a) (isp_cmd_send((x), &(a), sizeof(a), 0, CISP_TIMEOUT)) +#define CISP_SEND_INOUT(x, a) (isp_cmd_send((x), &(a), sizeof(a), sizeof(a), CISP_TIMEOUT)) #define CISP_SEND_OUT(x, a) (cisp_send_read((x), (a), sizeof(*a), sizeof(*a))) -#define CISP_POST_IN(x, a) (cisp_send((x), &(a), sizeof(a), 0, 0)) -#define CISP_POST_INOUT(x, a) (cisp_send((x), &(a), sizeof(a), sizeof(a), 0)) +#define CISP_POST_IN(x, a) (isp_cmd_send((x), &(a), sizeof(a), 0, 0)) +#define CISP_POST_INOUT(x, a) (isp_cmd_send((x), &(a), sizeof(a), sizeof(a), 0)) -static int cisp_send(struct apple_isp *isp, void *args, u32 insize, u32 outsize, int timeout) +int isp_cmd_send(struct apple_isp *isp, void *args, u32 insize, u32 outsize, int timeout) { - struct isp_channel *chan = isp->chan_io; - struct isp_message *req = &chan->req; + struct isp_channel *chan; + struct isp_message *req; int err; +#if IS_ENABLED(CONFIG_VIDEO_APPLE_ISP_KUNIT_TEST) + KUNIT_STATIC_STUB_REDIRECT(isp_cmd_send, isp, args, insize, outsize, timeout); +#endif + if (isp->ipc_failed) + return -EIO; + chan = isp->chan_io; + req = &chan->req; + req->arg0 = isp->cmd_iova; req->arg1 = insize; req->arg2 = outsize; memcpy(isp->cmd_virt, args, insize); err = ipc_chan_send(isp, chan, timeout); + if (isp_is_p010(isp)) { + struct isp_message *rsp = &chan->rsp; + + if (!err && timeout && + (rsp->arg1 != outsize || rsp->arg2 || rsp->arg3 || + rsp->arg4 || rsp->arg5 || rsp->arg6 || rsp->arg7 || + get_unaligned_le64(isp->cmd_virt) != get_unaligned_le64(args))) + err = -EIO; + if (err) + isp->ipc_failed = true; + } if (err) { u64 opcode; memcpy(&opcode, args, sizeof(opcode)); @@ -45,7 +69,7 @@ static int cisp_send_read(struct apple_isp *isp, void *args, u32 insize, u32 outsize) { /* TODO do I need to lock the iova space? */ - int err = cisp_send(isp, args, insize, outsize, CISP_TIMEOUT); + int err = isp_cmd_send(isp, args, insize, outsize, CISP_TIMEOUT); if (err) return err; @@ -165,7 +189,7 @@ int isp_cmd_set_dsid_clr_multi_bc(struct apple_isp *isp, const u64 *bases, return -EINVAL; memcpy(args.dsid_clr_base, bases, count * sizeof(*bases)); - return cisp_send(isp, &args, + return isp_cmd_send(isp, &args, offsetof(typeof(args), dsid_clr_base) + count * sizeof(*bases), 0, CISP_TIMEOUT); @@ -232,6 +256,8 @@ int isp_cmd_flicker_sensor_set(struct apple_isp *isp, u32 mode) .opcode = CISP_OPCODE(CISP_CMD_FLICKER_SENSOR_SET), .mode = mode, }; + if (isp_is_p010(isp)) + return CISP_SEND_IN(isp, args); return CISP_SEND_INOUT(isp, args); } @@ -260,6 +286,16 @@ int isp_cmd_ch_info_get(struct apple_isp *isp, u32 chan, return err; } +int isp_cmd_ch_camera_config_get(struct apple_isp *isp, u32 chan, u32 index, + struct cmd_ch_camera_config_h17 *args) +{ + memset(args, 0, sizeof(*args)); + args->config.opcode = CISP_OPCODE(CISP_CMD_CH_CAMERA_CONFIG_GET); + args->config.chan = chan; + args->config.preset = index; + return CISP_SEND_OUT(isp, args); +} + int isp_cmd_ch_camera_config_current_get(struct apple_isp *isp, u32 chan, struct cmd_ch_camera_config *args) { @@ -468,9 +504,9 @@ int isp_cmd_ch_buffer_pool_config_set(struct apple_isp *isp, u32 chan, u16 type) .opcode = CISP_OPCODE(CISP_CMD_CH_BUFFER_POOL_CONFIG_SET), .chan = chan, .type = type, - .count = ISP_MAX_BUFFERS, - .meta_size0 = size, - .meta_size1 = size, + .count = isp_num_meta(isp), + .meta_size0 = isp_is_p010(isp) ? 0x8000 : size, + .meta_size1 = isp_is_p010(isp) ? 0x8000 : size, .unk0 = 0, .unk1 = 0, .unk2 = 0, diff --git a/drivers/media/platform/apple/isp/isp-cmd.h b/drivers/media/platform/apple/isp/isp-cmd.h index 695c0a9f7ab70..a3167108b128e 100644 --- a/drivers/media/platform/apple/isp/isp-cmd.h +++ b/drivers/media/platform/apple/isp/isp-cmd.h @@ -124,6 +124,7 @@ #define CISP_COLORSPACE_REC709 0x1 #define CISP_OUTPUT_FORMAT_YUV_2PLANE 0x0 #define CISP_OUTPUT_FORMAT_YUV_1PLANE 0x1 +#define CISP_OUTPUT_FORMAT_P010 0x12 #define CISP_OUTPUT_FORMAT_RGB 0x2 #define CISP_BUFFER_RECYCLE_MODE_EMPTY_ONLY 0x1 @@ -255,6 +256,8 @@ struct cmd_flicker_sensor_set { } __packed; static_assert(sizeof(struct cmd_flicker_sensor_set) == 0xc); +int isp_cmd_send(struct apple_isp *isp, void *args, u32 insize, u32 outsize, + int timeout); int isp_cmd_start(struct apple_isp *isp, u32 mode); int isp_cmd_stop(struct apple_isp *isp, u32 mode); int isp_cmd_power_down(struct apple_isp *isp); @@ -380,6 +383,15 @@ struct cmd_ch_camera_config { } __packed; static_assert(sizeof(struct cmd_ch_camera_config) == 0xdc); +/* H17 appends 0x44 bytes to the common camera configuration record. */ +struct cmd_ch_camera_config_h17 { + struct cmd_ch_camera_config config; + u8 unknown[0x44]; +} __packed; +static_assert(sizeof(struct cmd_ch_camera_config_h17) == 0x120); +static_assert(offsetof(struct cmd_ch_camera_config, in_width) == 0x10); +static_assert(offsetof(struct cmd_ch_camera_config, out_width2) == 0xd4); + struct cmd_ch_camera_config_select { u64 opcode; u32 chan; @@ -516,6 +528,8 @@ int isp_cmd_ch_start(struct apple_isp *isp, u32 chan); int isp_cmd_ch_stop(struct apple_isp *isp, u32 chan); int isp_cmd_ch_info_get(struct apple_isp *isp, u32 chan, struct cmd_ch_info *args); +int isp_cmd_ch_camera_config_get(struct apple_isp *isp, u32 chan, u32 index, + struct cmd_ch_camera_config_h17 *args); int isp_cmd_ch_camera_config_current_get(struct apple_isp *isp, u32 chan, struct cmd_ch_camera_config *args); int isp_cmd_ch_camera_config_select(struct apple_isp *isp, u32 chan, diff --git a/drivers/media/platform/apple/isp/isp-drv.c b/drivers/media/platform/apple/isp/isp-drv.c index da9d459dd8bdb..04c50011abda1 100644 --- a/drivers/media/platform/apple/isp/isp-drv.c +++ b/drivers/media/platform/apple/isp/isp-drv.c @@ -538,6 +538,7 @@ static int apple_isp_probe(struct platform_device *pdev) mutex_init(&isp->gc_lock); INIT_LIST_HEAD(&isp->bufs_pending); INIT_LIST_HEAD(&isp->bufs_submitted); + INIT_LIST_HEAD(&isp->bufs_returned); isp->wq = alloc_workqueue("apple-isp-wq", WQ_UNBOUND, 0); if (!isp->wq) { dev_err(dev, "failed to create workqueue\n"); diff --git a/drivers/media/platform/apple/isp/isp-drv.h b/drivers/media/platform/apple/isp/isp-drv.h index ed3e48eef569e..e02d675f0e998 100644 --- a/drivers/media/platform/apple/isp/isp-drv.h +++ b/drivers/media/platform/apple/isp/isp-drv.h @@ -30,6 +30,7 @@ /* used to limit the user space buffers to the buffer_pool_config */ #define ISP_MAX_BUFFERS 16 +#define ISP_MAX_SENSOR_CONFIGS 64 /* capture metadata buffers, for firmware that has that pool */ #define ISP_CAPMETA_BUFFERS 8 @@ -139,6 +140,8 @@ struct isp_preset { struct coord output_dim; struct coord crop_offset; struct coord crop_size; + /* Processed P010 geometry established by preview or a config reply. */ + bool p010; }; struct apple_isp_hw { @@ -255,9 +258,17 @@ enum isp_sensor_id { ISP_VD56G0_6222_01, }; +struct isp_sensor_config { + struct coord input_dim; + struct coord output_dim; + u32 sensor_clk; + bool usable; +}; + struct isp_format { enum isp_sensor_id id; u32 version; + u32 pixelformat; struct isp_preset *preset; unsigned int num_planes; u32 strides[VB2_MAX_PLANES]; @@ -274,6 +285,9 @@ struct apple_isp { u32 cam_connections_scheme; struct isp_preset *presets; int num_presets; + /* Read-only sensor configuration replies, cached before video registration. */ + unsigned int num_sensor_configs; + struct isp_sensor_config sensor_configs[ISP_MAX_SENSOR_CONFIGS]; int num_channels; struct isp_format fmts[ISP_MAX_CHANNELS]; @@ -358,16 +372,25 @@ struct apple_isp { spinlock_t buf_lock; struct list_head bufs_pending; struct list_head bufs_submitted; + struct list_head bufs_returned; + u64 buffer_tag; + unsigned long meta_returned; + /* A timed-out command leaves slot ownership ambiguous until reboot. */ + bool ipc_failed; }; struct isp_chan_ops { int (*handle)(struct apple_isp *isp, struct isp_channel *chan); + /* Called after the response slot and its doorbell are published. */ + void (*complete)(struct apple_isp *isp); }; struct isp_buffer { struct vb2_v4l2_buffer vb; struct list_head link; struct isp_surf surfs[VB2_MAX_PLANES]; + u64 tag; + enum vb2_buffer_state return_state; }; #define to_isp_buffer(x) container_of((x), struct isp_buffer, vb) @@ -395,4 +418,36 @@ enum { ((isp)->hw->capture_meta_size ? ISP_CAPMETA_BUFFERS : 0) #define isp_get_current_format(isp) (isp_get_format(isp, isp->current_ch)) +static inline bool isp_supports_p010(struct apple_isp *isp) +{ + return isp->hw->gen == ISP_GEN_T6040 && + isp_get_current_format(isp)->version == 0x958; +} + +static inline bool isp_has_p010(struct apple_isp *isp) +{ + if (!isp_supports_p010(isp)) + return false; + for (int i = 0; i < isp->num_presets; i++) { + if (isp->presets[i].p010) + return true; + } + return false; +} + +static inline bool isp_is_p010(struct apple_isp *isp) +{ + return isp_get_current_format(isp)->pixelformat == V4L2_PIX_FMT_P010; +} + +static inline unsigned int isp_num_rendered(struct apple_isp *isp) +{ + return isp_is_p010(isp) ? 2 : ISP_MAX_BUFFERS; +} + +static inline unsigned int isp_num_meta(struct apple_isp *isp) +{ + return isp_is_p010(isp) ? 8 : ISP_MAX_BUFFERS; +} + #endif /* __ISP_DRV_H__ */ diff --git a/drivers/media/platform/apple/isp/isp-fw.c b/drivers/media/platform/apple/isp/isp-fw.c index 30ff704ef193f..d448225fceec5 100644 --- a/drivers/media/platform/apple/isp/isp-fw.c +++ b/drivers/media/platform/apple/isp/isp-fw.c @@ -212,6 +212,7 @@ static const struct isp_chan_ops sm_ops = { static const struct isp_chan_ops bt_ops = { .handle = ipc_bt_handle, + .complete = ipc_bt_complete, }; static irqreturn_t apple_isp_isr(int irq, void *dev) diff --git a/drivers/media/platform/apple/isp/isp-ipc-test.c b/drivers/media/platform/apple/isp/isp-ipc-test.c index 6521cecb4cfb6..efd24f6794a5a 100644 --- a/drivers/media/platform/apple/isp/isp-ipc-test.c +++ b/drivers/media/platform/apple/isp/isp-ipc-test.c @@ -144,7 +144,34 @@ static void isp_test_sm_free_log_bt(struct kunit *test) KUNIT_EXPECT_NULL(test, t->isp->bt_surf); } +static void isp_test_command_ring_wrap(struct kunit *test) +{ + struct isp_sm_test *t = isp_sm_test_alloc(test); + struct isp_channel *chan = t->chan; + struct isp_message *slots; + + chan->num = 8; + chan->virt = kunit_kcalloc(test, chan->num, sizeof(*slots), GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, chan->virt); + slots = chan->virt; + chan->req.arg0 = 0x5000000; + isp_get_current_format(t->isp)->pixelformat = V4L2_PIX_FMT_P010; + for (unsigned int i = 0; i < 100000; i++) { + unsigned int cursor = chan->cursor; + + /* Re-publication must clear the old ACK before waiting on a wrap. */ + slots[cursor].arg0 = chan->req.arg0; + KUNIT_ASSERT_FALSE(test, chan_tx_done(t->isp, chan)); + KUNIT_ASSERT_EQ(test, chan->cursor, cursor); + slots[cursor].arg0 |= ISP_IPC_FLAG_ACK; + KUNIT_ASSERT_TRUE(test, chan_tx_done(t->isp, chan)); + KUNIT_ASSERT_EQ(test, chan->cursor, (cursor + 1) % chan->num); + } + KUNIT_EXPECT_EQ(test, chan->cursor, 0); +} + static struct kunit_case isp_ipc_test_cases[] = { + KUNIT_CASE(isp_test_command_ring_wrap), KUNIT_CASE(isp_test_sm_free_releases), KUNIT_CASE(isp_test_sm_free_alias), KUNIT_CASE(isp_test_sm_free_unknown), diff --git a/drivers/media/platform/apple/isp/isp-ipc.c b/drivers/media/platform/apple/isp/isp-ipc.c index f709f6e305763..da946f360ff5c 100644 --- a/drivers/media/platform/apple/isp/isp-ipc.c +++ b/drivers/media/platform/apple/isp/isp-ipc.c @@ -6,6 +6,10 @@ #include "isp-regs.h" #include "isp-fw.h" +#if IS_ENABLED(CONFIG_VIDEO_APPLE_ISP_KUNIT_TEST) +#include +#endif + #define ISP_IPC_FLAG_TERMINAL_ACK 0x3 /* longer firmware log lines are cut */ #define ISP_IPC_TERMINAL_MAX_LEN 512 @@ -66,7 +70,16 @@ static inline void chan_read_msg_index(struct apple_isp *isp, struct isp_channel *chan, struct isp_message *msg, u32 index) { - memcpy(msg, chan_msg_virt(chan, index), sizeof(*msg)); + if (isp_is_p010(isp)) { + u64 *slot = chan_msg_virt(chan, index); + + /* Acquire validity before consuming the firmware-owned payload. */ + msg->arg0 = READ_ONCE(*slot); + dma_rmb(); + memcpy(&msg->arg1, slot + 1, sizeof(*msg) - sizeof(msg->arg0)); + } else { + memcpy(msg, chan_msg_virt(chan, index), sizeof(*msg)); + } } static inline void chan_read_msg(struct apple_isp *isp, @@ -124,6 +137,8 @@ static int chan_handle_once(struct apple_isp *isp, struct isp_channel *chan) isp_mbox2_write32(isp, ISP_MBOX2_IRQ_DOORBELL, chan->doorbell); chan_update_cursor(chan); + if (chan->ops->complete) + chan->ops->complete(isp); return 0; } @@ -139,10 +154,12 @@ static inline bool chan_rx_done(struct apple_isp *isp, struct isp_channel *chan) int ipc_chan_handle(struct apple_isp *isp, struct isp_channel *chan) { + unsigned int budget = isp_is_p010(isp) ? chan->num : UINT_MAX; int err = 0; mutex_lock(&chan->lock); - while (1) { + /* A busy producer must not starve the other completion/control rings. */ + while (budget--) { chan_read_msg(isp, chan, &chan->req); if (chan_rx_done(isp, chan)) { err = 0; @@ -176,6 +193,11 @@ int ipc_chan_send(struct apple_isp *isp, struct isp_channel *chan, { long t; +#if IS_ENABLED(CONFIG_VIDEO_APPLE_ISP_KUNIT_TEST) + KUNIT_STATIC_STUB_REDIRECT(ipc_chan_send, isp, chan, timeout); +#endif + if (isp->ipc_failed) + return -EIO; chan_write_msg(isp, chan, &chan->req); dma_wmb(); @@ -186,6 +208,8 @@ int ipc_chan_send(struct apple_isp *isp, struct isp_channel *chan, t = wait_event_timeout(isp->wait, chan_tx_done(isp, chan), timeout); if (t == 0) { + if (isp_is_p010(isp)) + isp->ipc_failed = true; dev_err(isp->dev, "%s: timed out on request [0x%llx, 0x%llx, 0x%llx]\n", chan->name, chan->req.arg0, chan->req.arg1, diff --git a/drivers/media/platform/apple/isp/isp-v4l2-test.c b/drivers/media/platform/apple/isp/isp-v4l2-test.c index 895a6d1299c22..47f1f8900d138 100644 --- a/drivers/media/platform/apple/isp/isp-v4l2-test.c +++ b/drivers/media/platform/apple/isp/isp-v4l2-test.c @@ -7,6 +7,7 @@ #include #include +#include #include #define ISP_TEST_FRAME_SIZE 0x1000 @@ -19,6 +20,8 @@ static struct apple_isp *isp_test_alloc(struct kunit *test) KUNIT_ASSERT_NOT_ERR_OR_NULL(test, isp); mutex_init(&isp->video_lock); + spin_lock_init(&isp->buf_lock); + INIT_LIST_HEAD(&isp->bufs_returned); isp_get_current_format(isp)->total_size = ISP_TEST_FRAME_SIZE; return isp; @@ -107,7 +110,232 @@ static void isp_test_create_bufs_limit(struct kunit *test) sizes), -EINVAL); } +static const struct apple_isp_hw isp_test_t6040 = { + .gen = ISP_GEN_T6040, + .fw_abi = ISP_FW_ABI_H17, +}; + +static void isp_test_p010_format(struct kunit *test) +{ + struct apple_isp *isp = isp_test_alloc(test); + struct isp_format *fmt = isp_get_current_format(isp); + struct isp_preset preset = { .output_dim = { 1920, 1080 }, .p010 = true }; + struct v4l2_format f = {}; + unsigned int sizes[VB2_MAX_PLANES] = {}; + unsigned int nbuffers = 16, num_planes = 0; + + isp->hw = &isp_test_t6040; + fmt->version = 0x958; + KUNIT_ASSERT_EQ(test, isp_set_preset(isp, fmt, &preset, V4L2_PIX_FMT_P010), 0); + isp_get_sp_pix_format(isp, &f, fmt); + KUNIT_EXPECT_EQ(test, f.fmt.pix.pixelformat, V4L2_PIX_FMT_P010); + KUNIT_EXPECT_EQ(test, f.fmt.pix.bytesperline, 3840); + KUNIT_EXPECT_EQ(test, f.fmt.pix.sizeimage, 6220800); + KUNIT_EXPECT_EQ(test, fmt->plane_size[0], (size_t)4147200); + KUNIT_EXPECT_EQ(test, fmt->plane_size[1], (size_t)2073600); + KUNIT_EXPECT_EQ(test, f.fmt.pix.quantization, V4L2_QUANTIZATION_LIM_RANGE); + isp_get_mp_pix_format(isp, &f, fmt); + KUNIT_EXPECT_EQ(test, f.fmt.pix_mp.pixelformat, V4L2_PIX_FMT_P010); + KUNIT_EXPECT_EQ(test, f.fmt.pix_mp.num_planes, 1); + KUNIT_EXPECT_EQ(test, f.fmt.pix_mp.plane_fmt[0].sizeimage, 6220800); + + isp->vbq.drv_priv = isp; + isp_test_set_num_buffers(test, &isp->vbq, 0); + KUNIT_ASSERT_EQ(test, + isp_vb2_queue_setup(&isp->vbq, &nbuffers, &num_planes, sizes, NULL), 0); + KUNIT_EXPECT_EQ(test, nbuffers, 2); + KUNIT_EXPECT_EQ(test, num_planes, 1); + KUNIT_EXPECT_EQ(test, sizes[0], 6220800); + KUNIT_EXPECT_EQ(test, isp_num_meta(isp), 8); + + preset.output_dim = (struct coord){1280, 720}; + KUNIT_ASSERT_EQ(test, isp_set_preset(isp, fmt, &preset, V4L2_PIX_FMT_P010), 0); + KUNIT_EXPECT_EQ(test, fmt->strides[0], 2560); + KUNIT_EXPECT_EQ(test, fmt->total_size, (size_t)2764800); + /* NV12 remains the default and restores the existing pool capacity. */ + KUNIT_ASSERT_EQ(test, isp_set_preset(isp, fmt, &preset, V4L2_PIX_FMT_NV12), 0); + KUNIT_EXPECT_EQ(test, fmt->pixelformat, V4L2_PIX_FMT_NV12); + KUNIT_EXPECT_EQ(test, fmt->total_size, (size_t)1382400); + KUNIT_EXPECT_EQ(test, isp_num_rendered(isp), ISP_MAX_BUFFERS); + KUNIT_EXPECT_EQ(test, isp_num_meta(isp), ISP_MAX_BUFFERS); +} + +static void isp_test_p010_admission(struct kunit *test) +{ + static const struct apple_isp_hw neo = { .gen = ISP_GEN_T8140 }; + struct apple_isp *isp = isp_test_alloc(test); + struct isp_format *fmt = isp_get_current_format(isp); + struct isp_preset preset = { .output_dim = {1280, 720}, .p010 = true }; + struct v4l2_fmtdesc desc = { .index = 1 }; + + isp->presets = &preset; + isp->num_presets = 1; + isp->hw = &isp_test_t6040; + fmt->version = 0x958; + KUNIT_ASSERT_EQ(test, isp_enum_pixelformat(isp, &desc), 0); + KUNIT_EXPECT_EQ(test, desc.pixelformat, V4L2_PIX_FMT_P010); + desc.index = 2; + KUNIT_EXPECT_EQ(test, isp_enum_pixelformat(isp, &desc), -EINVAL); + isp->multiplanar = true; + KUNIT_ASSERT_EQ(test, isp_enum_pixelformat(isp, &desc), 0); + KUNIT_EXPECT_EQ(test, desc.pixelformat, V4L2_PIX_FMT_P010); + isp->hw = &neo; + KUNIT_EXPECT_EQ(test, isp_enum_pixelformat(isp, &desc), -EINVAL); + KUNIT_ASSERT_EQ(test, isp_set_preset(isp, fmt, &preset, V4L2_PIX_FMT_P010), 0); + KUNIT_EXPECT_EQ(test, fmt->pixelformat, V4L2_PIX_FMT_NV12); + KUNIT_EXPECT_EQ(test, fmt->strides[0], 1280); +} + +static void isp_test_p010_completion_identity(struct kunit *test) +{ + struct isp_format fmt = { .plane_size = {4147200, 2073600} }; + struct isp_buffer buf = { .surfs = {{ .iova = 0x10020000000ULL }}, .tag = 42 }; + struct isp_buflist_buffer desc = { + .iovas = {0x20000000, 0x203f4800}, + .num_planes = 2, .pool_type = CISP_POOL_TYPE_RENDERED_SCL1, .tag = 42, + }; + + KUNIT_EXPECT_TRUE(test, isp_p010_buffer_matches(&fmt, &buf, &desc)); + desc.tag--; + KUNIT_EXPECT_FALSE(test, isp_p010_buffer_matches(&fmt, &buf, &desc)); + desc.tag++; + desc.iovas[1]++; + KUNIT_EXPECT_FALSE(test, isp_p010_buffer_matches(&fmt, &buf, &desc)); + desc.iovas[1]--; + desc.iovas[0] |= BIT_ULL(40); + KUNIT_EXPECT_FALSE(test, isp_p010_buffer_matches(&fmt, &buf, &desc)); + desc.iovas[0] &= ~BIT_ULL(40); + desc.pool_type = CISP_POOL_TYPE_RENDERED; + KUNIT_EXPECT_FALSE(test, isp_p010_buffer_matches(&fmt, &buf, &desc)); + desc.pool_type = CISP_POOL_TYPE_RENDERED_SCL1; + desc.num_planes = 1; + KUNIT_EXPECT_FALSE(test, isp_p010_buffer_matches(&fmt, &buf, &desc)); +} + +static void isp_test_p010_report_defers_completion(struct kunit *test) +{ + struct apple_isp *isp = isp_test_alloc(test); + struct isp_format *fmt = isp_get_current_format(isp); + struct isp_buffer *buf = kunit_kzalloc(test, sizeof(*buf), GFP_KERNEL); + struct { + u64 type, count; + struct isp_buflist_buffer desc; + } report = { + .type = 2, .count = 1, + .desc = { .iovas = {0x20000000, 0x203f4800}, + .num_planes = 2, .pool_type = 9, .tag = 42 }, + }; + struct isp_surf report_surf = { + .iova = 0x30000000, .size = sizeof(report), .virt = &report, + }; + struct isp_channel chan = { + .req = { .arg0 = report_surf.iova, .arg1 = sizeof(report), + .arg2 = ISP_IPC_BUFEXC_FLAG_RENDER }, + }; + + KUNIT_ASSERT_NOT_NULL(test, buf); + fmt->pixelformat = V4L2_PIX_FMT_P010; + fmt->plane_size[0] = 4147200; + isp->fw_iova_mask = U64_MAX; + isp->bt_surf = &report_surf; + buf->surfs[0].iova = 0x10020000000ULL; + buf->tag = 42; + INIT_LIST_HEAD(&isp->bufs_submitted); + list_add(&buf->link, &isp->bufs_submitted); + /* There is no vb2 queue: completing it before the ACK would fault. */ + KUNIT_ASSERT_EQ(test, ipc_bt_handle(isp, &chan), 0); + KUNIT_EXPECT_TRUE(test, list_empty(&isp->bufs_submitted)); + KUNIT_EXPECT_PTR_EQ(test, + list_first_entry(&isp->bufs_returned, struct isp_buffer, link), buf); + KUNIT_EXPECT_EQ(test, buf->return_state, VB2_BUF_STATE_DONE); + KUNIT_EXPECT_EQ(test, chan.rsp.arg0, report_surf.iova | ISP_IPC_FLAG_ACK); +} + +static void isp_test_p010_mode_selection(struct kunit *test) +{ + struct apple_isp *isp = isp_test_alloc(test); + struct isp_preset presets[] = { + { .index = 5, .output_dim = {1920, 1080} }, + { .index = 5, .output_dim = {1280, 720}, .p010 = true }, + { .index = 1, .output_dim = {4096, 3072} }, + }; + + isp->hw = &isp_test_t6040; + isp_get_current_format(isp)->version = 0x958; + isp->presets = presets; + isp->num_presets = ARRAY_SIZE(presets); + KUNIT_EXPECT_PTR_EQ(test, isp_select_preset(isp, 0, 0, V4L2_PIX_FMT_P010), &presets[1]); + KUNIT_EXPECT_PTR_EQ(test, isp_select_preset(isp, 1920, 1080, V4L2_PIX_FMT_P010), + &presets[1]); + KUNIT_EXPECT_PTR_EQ(test, isp_select_preset(isp, 4096, 3072, V4L2_PIX_FMT_P010), + &presets[1]); + KUNIT_EXPECT_PTR_EQ(test, isp_select_preset(isp, 1920, 1080, V4L2_PIX_FMT_NV12), + &presets[0]); + /* Only a matching firmware output enables the larger P010 size. */ + presets[2].p010 = true; + KUNIT_EXPECT_PTR_EQ(test, isp_select_preset(isp, 4096, 3072, V4L2_PIX_FMT_P010), + &presets[2]); +} + +static int isp_test_buffer_ack(struct apple_isp *isp, struct isp_channel *chan, + unsigned long timeout) +{ + unsigned int *calls = kunit_get_current_test()->priv; + + (*calls)++; + return 0; +} + +static void isp_test_p010_last_lease_requeue(struct kunit *test) +{ + struct apple_isp *isp = isp_test_alloc(test); + struct isp_format *fmt = isp_get_current_format(isp); + struct isp_buffer *buf = kunit_kzalloc(test, sizeof(*buf), GFP_KERNEL); + struct isp_buffer *submitted; + unsigned int calls = 0; + + KUNIT_ASSERT_NOT_NULL(test, buf); + isp->hw = &isp_test_t6040; + fmt->pixelformat = V4L2_PIX_FMT_P010; + fmt->num_planes = 2; + fmt->plane_size[0] = 1843200; + fmt->plane_size[1] = 921600; + INIT_LIST_HEAD(&isp->bufs_pending); + INIT_LIST_HEAD(&isp->bufs_submitted); + isp->cmd_virt = kunit_kzalloc(test, ISP_CMD_AREA_SIZE(0), GFP_KERNEL); + isp->chan_bh = kunit_kzalloc(test, sizeof(*isp->chan_bh), GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, isp->cmd_virt); + KUNIT_ASSERT_NOT_NULL(test, isp->chan_bh); + for (unsigned int i = 0; i < isp_num_meta(isp); i++) { + isp->meta_surfs[i] = kunit_kzalloc(test, sizeof(struct isp_surf), GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, isp->meta_surfs[i]); + isp->meta_surfs[i]->submitted = true; + } + isp->vbq.drv_priv = isp; + buf->vb.vb2_buf.vb2_queue = &isp->vbq; + buf->surfs[0].iova = 0x20000000; + set_bit(ISP_STATE_STREAMING, &isp->state); + test->priv = &calls; + kunit_activate_static_stub(test, ipc_chan_send, isp_test_buffer_ack); + for (unsigned int i = 1; i <= 10000; i++) { + /* Both lists empty, as after the final completion was dequeued. */ + isp_vb2_buf_queue(&buf->vb.vb2_buf); + KUNIT_ASSERT_EQ(test, calls, i); + KUNIT_ASSERT_EQ(test, buf->tag, (u64)i); + KUNIT_ASSERT_TRUE(test, list_empty(&isp->bufs_pending)); + submitted = list_first_entry(&isp->bufs_submitted, struct isp_buffer, link); + KUNIT_ASSERT_PTR_EQ(test, submitted, buf); + list_del(&buf->link); + } +} + static struct kunit_case isp_v4l2_test_cases[] = { + KUNIT_CASE(isp_test_p010_last_lease_requeue), + KUNIT_CASE(isp_test_p010_report_defers_completion), + KUNIT_CASE(isp_test_p010_mode_selection), + KUNIT_CASE(isp_test_p010_format), + KUNIT_CASE(isp_test_p010_admission), + KUNIT_CASE(isp_test_p010_completion_identity), KUNIT_CASE(isp_test_queue_init), KUNIT_CASE(isp_test_reqbufs_limit), KUNIT_CASE(isp_test_create_bufs_limit), diff --git a/drivers/media/platform/apple/isp/isp-v4l2.c b/drivers/media/platform/apple/isp/isp-v4l2.c index 072b8523279c2..35c30582ab81d 100644 --- a/drivers/media/platform/apple/isp/isp-v4l2.c +++ b/drivers/media/platform/apple/isp/isp-v4l2.c @@ -21,7 +21,6 @@ #define ISP_MIN_FRAMES 2 #define ISP_MAX_PLANES 4 -#define ISP_MAX_PIX_FORMATS 2 #define ISP_BUFFER_TIMEOUT msecs_to_jiffies(1500) #define ISP_STRIDE_ALIGNMENT 64 @@ -37,8 +36,7 @@ struct isp_buflist_buffer { u32 flags[ISP_MAX_PLANES]; u32 num_planes; u32 pool_type; - u32 tag; - u32 pad; + u64 tag; } __packed; static_assert(sizeof(struct isp_buflist_buffer) == ISP_BUFLIST_DESC_SIZE); @@ -57,6 +55,35 @@ static_assert(ISP_CMD_AREA_SIZE(0) >= ISP_IPC_BUFEXC_STAT_SIZE); */ #define ISP_BUFLIST_POOL_CAPTURE_META 2 +static bool isp_p010_buffer_matches(const struct isp_format *fmt, + const struct isp_buffer *buf, + const struct isp_buflist_buffer *desc) +{ + /* H17 reports zero-extended low 32 bits, not the submitted high IOVA. */ + return desc->num_planes == 2 && + desc->pool_type == CISP_POOL_TYPE_RENDERED_SCL1 && + desc->iovas[0] == (u32)buf->surfs[0].iova && + desc->iovas[1] == (u32)(buf->surfs[0].iova + fmt->plane_size[0]) && + !desc->iovas[2] && !desc->iovas[3] && desc->tag == buf->tag; +} + +void ipc_bt_complete(struct apple_isp *isp) +{ + struct isp_buffer *buf, *tmp; + unsigned long flags, i; + + /* The firmware has received its ACK before userspace can reuse a lease. */ + spin_lock_irqsave(&isp->buf_lock, flags); + for_each_set_bit(i, &isp->meta_returned, ISP_MAX_BUFFERS) + isp->meta_surfs[i]->submitted = false; + isp->meta_returned = 0; + list_for_each_entry_safe(buf, tmp, &isp->bufs_returned, link) { + list_del(&buf->link); + vb2_buffer_done(&buf->vb.vb2_buf, buf->return_state); + } + spin_unlock_irqrestore(&isp->buf_lock, flags); +} + int ipc_bt_handle(struct apple_isp *isp, struct isp_channel *chan) { struct isp_message *req = &chan->req, *rsp = &chan->rsp; @@ -96,11 +123,14 @@ int ipc_bt_handle(struct apple_isp *isp, struct isp_channel *chan) bufd->pool_type); */ if (bufd->pool_type == 0) { - for (int j = 0; j < ARRAY_SIZE(isp->meta_surfs); j++) { + for (int j = 0; j < isp_num_meta(isp); j++) { struct isp_surf *meta = isp->meta_surfs[j]; if ((u32)bufd->iovas[0] == (u32)meta->iova) { WARN_ON(!meta->submitted); - meta->submitted = false; + if (isp_is_p010(isp)) + __set_bit(j, &isp->meta_returned); + else + meta->submitted = false; } } } else if (bufd->pool_type == ISP_BUFLIST_POOL_CAPTURE_META && @@ -114,8 +144,10 @@ int ipc_bt_handle(struct apple_isp *isp, struct isp_channel *chan) } else { list_for_each_entry_safe_reverse( buf, tmp, &isp->bufs_submitted, link) { - if ((u32)buf->surfs[0].iova == - (u32)bufd->iovas[0]) { + if (isp_is_p010(isp) ? + isp_p010_buffer_matches(isp_get_current_format(isp), + buf, bufd) : + (u32)buf->surfs[0].iova == (u32)bufd->iovas[0]) { enum vb2_buffer_state state = VB2_BUF_STATE_ERROR; @@ -126,9 +158,13 @@ int ipc_bt_handle(struct apple_isp *isp, struct isp_channel *chan) if (req->arg2 == ISP_IPC_BUFEXC_FLAG_RENDER) state = VB2_BUF_STATE_DONE; - vb2_buffer_done(&buf->vb.vb2_buf, - state); - list_del(&buf->link); + if (isp_is_p010(isp)) { + buf->return_state = state; + list_move_tail(&buf->link, &isp->bufs_returned); + } else { + vb2_buffer_done(&buf->vb.vb2_buf, state); + list_del(&buf->link); + } } } } @@ -166,7 +202,7 @@ static int isp_submit_buffers(struct apple_isp *isp, bool pre_start) bl->type = 1; spin_lock_irqsave(&isp->buf_lock, flags); - for (int i = 0; i < ARRAY_SIZE(isp->meta_surfs); i++) { + for (int i = 0; i < isp_num_meta(isp); i++) { struct isp_surf *meta = isp->meta_surfs[i]; if (meta->submitted) @@ -205,9 +241,13 @@ static int isp_submit_buffers(struct apple_isp *isp, bool pre_start) } /* One rendered pool per batch; the rest stays pending for the next. */ - while (num_rendered < ISP_MAX_BUFFERS && + while (num_rendered < isp_num_rendered(isp) && (buf = list_first_entry_or_null(&isp->bufs_pending, struct isp_buffer, link))) { + if (isp_is_p010(isp)) { + buf->tag = ++isp->buffer_tag; + bufd->tag = buf->tag; + } bufd->num_planes = fmt->num_planes; bufd->pool_type = isp->hw->scl1 ? CISP_POOL_TYPE_RENDERED_SCL1 : CISP_POOL_TYPE_RENDERED; @@ -249,6 +289,11 @@ static int isp_submit_buffers(struct apple_isp *isp, bool pre_start) err = ipc_chan_send(isp, chan, ISP_BUFFER_TIMEOUT); if (err) { + if (isp_is_p010(isp)) { + /* Keep leases owned until stop_camera halts the coprocessor. */ + isp->ipc_failed = true; + return err; + } /* If we fail, consider the buffer not submitted. */ dev_err(isp->dev, "%s: failed to send bufs: [0x%llx, 0x%llx, 0x%llx]\n", @@ -269,7 +314,7 @@ static int isp_submit_buffers(struct apple_isp *isp, bool pre_start) &isp->bufs_pending); } } - for (int j = 0; j < ARRAY_SIZE(isp->meta_surfs); j++) { + for (int j = 0; j < isp_num_meta(isp); j++) { struct isp_surf *meta = isp->meta_surfs[j]; if (bufd->iovas[0] == meta->iova) { meta->submitted = false; @@ -308,10 +353,10 @@ static int isp_vb2_queue_setup(struct vb2_queue *vq, unsigned int *nbuffers, * REQBUFS frees the old buffers first, CREATE_BUFS adds to them, so * count the buffers the queue already has. */ - if (q_num_bufs >= ISP_MAX_BUFFERS) + if (q_num_bufs >= isp_num_rendered(isp)) return -ENOBUFS; *nbuffers = min_t(unsigned int, *nbuffers, - ISP_MAX_BUFFERS - q_num_bufs); + isp_num_rendered(isp) - q_num_bufs); if (*num_planes) { if (sizes[0] < fmt->total_size) @@ -407,8 +452,15 @@ static void isp_vb2_buf_queue(struct vb2_buffer *vb) list_add_tail(&buf->link, &isp->bufs_pending); spin_unlock_irqrestore(&isp->buf_lock, flags); - if (test_bit(ISP_STATE_STREAMING, &isp->state) && !empty) - isp_submit_buffers(isp, false); + /* A returned last lease must restart an empty P010 queue immediately. */ + if (test_bit(ISP_STATE_STREAMING, &isp->state) && + (isp_is_p010(isp) || !empty)) { + if (isp_submit_buffers(isp, false) && isp_is_p010(isp)) { + apple_isp_firmware_halt(isp); + isp_vb2_release_buffers(isp, VB2_BUF_STATE_ERROR); + vb2_queue_error(&isp->vbq); + } + } } static int apple_isp_start_streaming(struct apple_isp *isp) @@ -565,18 +617,23 @@ static const struct vb2_ops isp_vb2_ops = { }; static int isp_set_preset(struct apple_isp *isp, struct isp_format *fmt, - struct isp_preset *preset) + struct isp_preset *preset, u32 pixelformat) { + unsigned int bytes_per_sample; int i; size_t total_size; fmt->preset = preset; - /* I really fucking hope they all use NV12. */ + /* P010 has MSB-aligned 10-bit samples in little-endian 16-bit words. */ + fmt->pixelformat = pixelformat == V4L2_PIX_FMT_P010 && + isp_supports_p010(isp) && preset->p010 ? + V4L2_PIX_FMT_P010 : V4L2_PIX_FMT_NV12; + bytes_per_sample = fmt->pixelformat == V4L2_PIX_FMT_P010 ? 2 : 1; fmt->num_planes = 2; - fmt->strides[0] = ALIGN(preset->output_dim.x, ISP_STRIDE_ALIGNMENT); - /* UV subsampled interleaved */ - fmt->strides[1] = ALIGN(preset->output_dim.x, ISP_STRIDE_ALIGNMENT); + fmt->strides[0] = ALIGN(preset->output_dim.x * bytes_per_sample, + ISP_STRIDE_ALIGNMENT); + fmt->strides[1] = fmt->strides[0]; fmt->plane_size[0] = fmt->strides[0] * preset->output_dim.y; fmt->plane_size[1] = fmt->strides[1] * preset->output_dim.y / 2; @@ -589,17 +646,25 @@ static int isp_set_preset(struct apple_isp *isp, struct isp_format *fmt, } static struct isp_preset *isp_select_preset(struct apple_isp *isp, u32 width, - u32 height) + u32 height, u32 pixelformat) { struct isp_preset *preset, *best = &isp->presets[0]; int i, score, best_score = INT_MAX; - /* Default if no dimensions */ - if (width == 0 || height == 0) - return &isp->presets[0]; + bool p010 = pixelformat == V4L2_PIX_FMT_P010 && isp_has_p010(isp); + + /* An unspecified P010 size selects the processed 720p preview first. */ + if (width == 0 || height == 0) { + if (!p010) + return &isp->presets[0]; + width = 1280; + height = 720; + } for (i = 0; i < isp->num_presets; i++) { preset = &isp->presets[i]; + if (p010 && !preset->p010) + continue; score = abs((int)preset->output_dim.x - (int)width) + abs((int)preset->output_dim.y - (int)height); if (score < best_score) { @@ -623,22 +688,22 @@ static int isp_vidioc_querycap(struct file *file, void *priv, return 0; } -static int isp_vidioc_enum_format(struct file *file, void *fh, - struct v4l2_fmtdesc *f) +static int isp_enum_pixelformat(struct apple_isp *isp, struct v4l2_fmtdesc *f) { - struct apple_isp *isp = video_drvdata(file); - - if (f->index >= ISP_MAX_PIX_FORMATS) - return -EINVAL; - switch (f->index) { case 0: f->pixelformat = V4L2_PIX_FMT_NV12; break; case 1: - if (!isp->multiplanar) + if (isp->multiplanar) { + f->pixelformat = V4L2_PIX_FMT_NV12M; + break; + } + fallthrough; + case 2: + if (!isp_has_p010(isp) || f->index != (isp->multiplanar ? 2 : 1)) return -EINVAL; - f->pixelformat = V4L2_PIX_FMT_NV12M; + f->pixelformat = V4L2_PIX_FMT_P010; break; default: return -EINVAL; @@ -647,20 +712,36 @@ static int isp_vidioc_enum_format(struct file *file, void *fh, return 0; } +static int isp_vidioc_enum_format(struct file *file, void *fh, + struct v4l2_fmtdesc *f) +{ + return isp_enum_pixelformat(video_drvdata(file), f); +} + static int isp_vidioc_enum_framesizes(struct file *file, void *fh, struct v4l2_frmsizeenum *f) { struct apple_isp *isp = video_drvdata(file); + struct isp_preset *preset = NULL; + u32 index = 0; - if (f->index >= isp->num_presets) + if (f->pixel_format != V4L2_PIX_FMT_NV12 && + f->pixel_format != V4L2_PIX_FMT_NV12M && + !(f->pixel_format == V4L2_PIX_FMT_P010 && isp_has_p010(isp))) return -EINVAL; - if ((f->pixel_format != V4L2_PIX_FMT_NV12) && - (f->pixel_format != V4L2_PIX_FMT_NV12M)) + for (int i = 0; i < isp->num_presets; i++) { + if (f->pixel_format == V4L2_PIX_FMT_P010 && !isp->presets[i].p010) + continue; + if (index++ == f->index) { + preset = &isp->presets[i]; + break; + } + } + if (!preset) return -EINVAL; - - f->discrete.width = isp->presets[f->index].output_dim.x; - f->discrete.height = isp->presets[f->index].output_dim.y; + f->discrete.width = preset->output_dim.x; + f->discrete.height = preset->output_dim.y; f->type = V4L2_FRMSIZE_TYPE_DISCRETE; return 0; @@ -676,10 +757,13 @@ static int isp_vidioc_enum_frameintervals(struct file *file, void *fh, return -EINVAL; if (interval->pixel_format != V4L2_PIX_FMT_NV12 && - interval->pixel_format != V4L2_PIX_FMT_NV12M) + interval->pixel_format != V4L2_PIX_FMT_NV12M && + !(interval->pixel_format == V4L2_PIX_FMT_P010 && isp_has_p010(isp))) return -EINVAL; for (i = 0; i < isp->num_presets; i++) { + if (interval->pixel_format == V4L2_PIX_FMT_P010 && !isp->presets[i].p010) + continue; if (isp->presets[i].output_dim.x == interval->width && isp->presets[i].output_dim.y == interval->height) break; @@ -726,10 +810,12 @@ static inline void isp_get_sp_pix_format(struct apple_isp *isp, f->fmt.pix.sizeimage = fmt->total_size; f->fmt.pix.field = V4L2_FIELD_NONE; - f->fmt.pix.pixelformat = V4L2_PIX_FMT_NV12; + f->fmt.pix.pixelformat = fmt->pixelformat; f->fmt.pix.colorspace = V4L2_COLORSPACE_REC709; f->fmt.pix.ycbcr_enc = V4L2_YCBCR_ENC_709; f->fmt.pix.xfer_func = V4L2_XFER_FUNC_709; + if (fmt->pixelformat == V4L2_PIX_FMT_P010) + f->fmt.pix.quantization = V4L2_QUANTIZATION_LIM_RANGE; } static inline void isp_get_mp_pix_format(struct apple_isp *isp, @@ -746,9 +832,19 @@ static inline void isp_get_mp_pix_format(struct apple_isp *isp, f->fmt.pix_mp.field = V4L2_FIELD_NONE; f->fmt.pix_mp.pixelformat = V4L2_PIX_FMT_NV12M; + /* P010 is contiguous, including through the multi-planar API. */ + if (fmt->pixelformat == V4L2_PIX_FMT_P010) { + f->fmt.pix_mp.pixelformat = V4L2_PIX_FMT_P010; + f->fmt.pix_mp.num_planes = 1; + f->fmt.pix_mp.plane_fmt[0].sizeimage = fmt->total_size; + memset(&f->fmt.pix_mp.plane_fmt[1], 0, + sizeof(f->fmt.pix_mp.plane_fmt[1])); + } f->fmt.pix_mp.colorspace = V4L2_COLORSPACE_REC709; f->fmt.pix_mp.ycbcr_enc = V4L2_YCBCR_ENC_709; f->fmt.pix_mp.xfer_func = V4L2_XFER_FUNC_709; + if (fmt->pixelformat == V4L2_PIX_FMT_P010) + f->fmt.pix_mp.quantization = V4L2_QUANTIZATION_LIM_RANGE; } static int isp_vidioc_get_format(struct file *file, void *fh, @@ -774,8 +870,9 @@ static int isp_vidioc_set_format(struct file *file, void *fh, if (vb2_is_busy(&isp->vbq)) return -EBUSY; - preset = isp_select_preset(isp, f->fmt.pix.width, f->fmt.pix.height); - err = isp_set_preset(isp, fmt, preset); + preset = isp_select_preset(isp, f->fmt.pix.width, f->fmt.pix.height, + f->fmt.pix.pixelformat); + err = isp_set_preset(isp, fmt, preset, f->fmt.pix.pixelformat); if (err) return err; @@ -794,8 +891,9 @@ static int isp_vidioc_try_format(struct file *file, void *fh, struct isp_preset *preset; int err; - preset = isp_select_preset(isp, f->fmt.pix.width, f->fmt.pix.height); - err = isp_set_preset(isp, &fmt, preset); + preset = isp_select_preset(isp, f->fmt.pix.width, f->fmt.pix.height, + f->fmt.pix.pixelformat); + err = isp_set_preset(isp, &fmt, preset, f->fmt.pix.pixelformat); if (err) return err; @@ -832,9 +930,9 @@ static int isp_vidioc_set_format_mplane(struct file *file, void *fh, if (vb2_is_busy(&isp->vbq)) return -EBUSY; - preset = isp_select_preset(isp, f->fmt.pix_mp.width, - f->fmt.pix_mp.height); - err = isp_set_preset(isp, fmt, preset); + preset = isp_select_preset(isp, f->fmt.pix_mp.width, f->fmt.pix_mp.height, + f->fmt.pix_mp.pixelformat); + err = isp_set_preset(isp, fmt, preset, f->fmt.pix_mp.pixelformat); if (err) return err; @@ -856,9 +954,9 @@ static int isp_vidioc_try_format_mplane(struct file *file, void *fh, if (!isp->multiplanar) return -ENOTTY; - preset = isp_select_preset(isp, f->fmt.pix_mp.width, - f->fmt.pix_mp.height); - err = isp_set_preset(isp, &fmt, preset); + preset = isp_select_preset(isp, f->fmt.pix_mp.width, f->fmt.pix_mp.height, + f->fmt.pix_mp.pixelformat); + err = isp_set_preset(isp, &fmt, preset, f->fmt.pix_mp.pixelformat); if (err) return err; @@ -940,6 +1038,24 @@ static int isp_vidioc_set_param(struct file *file, void *fh, return 0; } +static int isp_vidioc_log_status(struct file *file, void *fh) +{ + struct apple_isp *isp = video_drvdata(file); + + v4l2_ctrl_log_status(file, fh); + for (unsigned int i = 0; i < isp->num_sensor_configs; i++) { + const struct isp_sensor_config *config = &isp->sensor_configs[i]; + + v4l2_info(&isp->v4l2_dev, + "sensor config %u: input %ux%u output %ux%u clock %u%s\n", + i, config->input_dim.x, config->input_dim.y, + config->output_dim.x, config->output_dim.y, + config->sensor_clk, + config->usable ? "" : " (unverified geometry)"); + } + return 0; +} + static const struct v4l2_ioctl_ops isp_v4l2_ioctl_ops = { .vidioc_querycap = isp_vidioc_querycap, @@ -969,7 +1085,7 @@ static const struct v4l2_ioctl_ops isp_v4l2_ioctl_ops = { .vidioc_streamon = vb2_ioctl_streamon, .vidioc_streamoff = vb2_ioctl_streamoff, - .vidioc_log_status = v4l2_ctrl_log_status, + .vidioc_log_status = isp_vidioc_log_status, .vidioc_subscribe_event = v4l2_ctrl_subscribe_event, .vidioc_unsubscribe_event = v4l2_event_unsubscribe, }; @@ -1044,7 +1160,7 @@ int apple_isp_setup_video(struct apple_isp *isp) struct isp_format *fmt = isp_get_current_format(isp); int err; - err = isp_set_preset(isp, fmt, &isp->presets[0]); + err = isp_set_preset(isp, fmt, &isp->presets[0], V4L2_PIX_FMT_NV12); if (err) { dev_err(isp->dev, "failed to set default preset: %d\n", err); return err; @@ -1054,7 +1170,8 @@ int apple_isp_setup_video(struct apple_isp *isp) for (int i = 0; i < ARRAY_SIZE(isp->meta_surfs); i++) { isp->meta_surfs[i] = - isp_alloc_surface_vmap(isp, isp->hw->meta_size); + isp_alloc_surface_vmap(isp, isp_has_p010(isp) ? 0x8000 : + isp->hw->meta_size); if (!isp->meta_surfs[i]) { isp_err(isp, "failed to alloc meta surface\n"); err = -ENOMEM; diff --git a/drivers/media/platform/apple/isp/isp-v4l2.h b/drivers/media/platform/apple/isp/isp-v4l2.h index 622288296aaae..55135594576b2 100644 --- a/drivers/media/platform/apple/isp/isp-v4l2.h +++ b/drivers/media/platform/apple/isp/isp-v4l2.h @@ -9,6 +9,7 @@ int apple_isp_setup_video(struct apple_isp *isp); void apple_isp_remove_video(struct apple_isp *isp); void apple_isp_free_video(struct apple_isp *isp); +void ipc_bt_complete(struct apple_isp *isp); int ipc_bt_handle(struct apple_isp *isp, struct isp_channel *chan); int apple_isp_video_suspend(struct apple_isp *isp); From 910d31f6d65aa1eb497dcfd01fb1ca52e692e169 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 13:06:16 +1100 Subject: [PATCH 133/388] selftests: media: add Apple ISP capture delivery stress runner Exercise every offered format and discrete size with long MMAP capture, repeated stream restarts and format switches. Record delivered rate, timing gaps and buffer errors, and fail on substitutions or stalls. Allow a mode filter for different nominal rates. A driver-generated sequence alone is not evidence of zero firmware drops. Assisted-by: LLM Signed-off-by: Ryan Murray --- tools/testing/selftests/media/Makefile | 4 + .../testing/selftests/media/README.apple-isp | 34 +++ .../selftests/media/apple-isp-stress.c | 278 ++++++++++++++++++ 3 files changed, 316 insertions(+) create mode 100644 tools/testing/selftests/media/Makefile create mode 100644 tools/testing/selftests/media/README.apple-isp create mode 100644 tools/testing/selftests/media/apple-isp-stress.c diff --git a/tools/testing/selftests/media/Makefile b/tools/testing/selftests/media/Makefile new file mode 100644 index 0000000000000..b63cb0f120e7e --- /dev/null +++ b/tools/testing/selftests/media/Makefile @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: GPL-2.0-only +CFLAGS += -O2 -Wall -Wextra -Werror +TEST_GEN_PROGS_EXTENDED := apple-isp-stress +include ../lib.mk diff --git a/tools/testing/selftests/media/README.apple-isp b/tools/testing/selftests/media/README.apple-isp new file mode 100644 index 0000000000000..57b95740120c0 --- /dev/null +++ b/tools/testing/selftests/media/README.apple-isp @@ -0,0 +1,34 @@ +Apple ISP capture delivery stress +================================= + +Build with make -C tools/testing/selftests/media. This is a manual hardware +runner; run_tests does not start the camera automatically. No image files are +written. Run only on the agreed J616s test kernel while audio is idle. + +Example for configurations whose expected rate has been established as 30 fps: + + ./apple-isp-stress /dev/video0 600 10 30 | tee capture-results.jsonl + +It enumerates every offered single-planar format and discrete size. Each gets +a ten-minute run, then ten five-second start/stop cycles, switching formats and +sizes between streams. A substituted format/rate, poll timeout, queue error, +V4L2 buffer error, sequence discontinuity or delivery gap exceeding 1.5 nominal +frame periods fails. Delivered FPS must reach 99.8% of the supplied rate, allowing +29.97/30 rounding. The rate argument is an explicit test expectation, not a +rate inferred from the sensor clock or set by the tool as an alleged sensor fact. +For modes with a different nominal rate, filter the run explicitly, for example +`./apple-isp-stress /dev/video0 600 10 25 P010 1920 1080`. A filtered run tests +that mode's restart cycles; the unfiltered run also exercises format switches. This runner stops +on the first failure to preserve evidence and avoid retrying ambiguous firmware. + +The driver's sequence increments for received reports, so consecutive sequence +values alone cannot establish zero sensor/firmware drops. The timing checks +measure delivery gaps and completed frames. A true zero-drop sensor claim also +needs a validated firmware frame counter or an independent sensor-rate oracle. +No RAW packing, Bayer order or nominal per-config FPS is guessed by this tool. +It tests RAW automatically if a verified RAW format is added to ENUM_FMT later. + +Preserve the current firmware config dump, v4l2-ctl --all/--list-formats-ext, +the JSONL output, dmesg before/after, exact kernel and source hashes. Check both +P010 presets for correct image content separately; this runner deliberately +avoids a converter or disk writer in the capture loop. diff --git a/tools/testing/selftests/media/apple-isp-stress.c b/tools/testing/selftests/media/apple-isp-stress.c new file mode 100644 index 0000000000000..8490a7abe3d68 --- /dev/null +++ b/tools/testing/selftests/media/apple-isp-stress.c @@ -0,0 +1,278 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* V4L2 delivery stress test. Does not infer sensor FPS from a driver counter. */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define MAX_MODES 256 +#define MAX_BUFFERS 16 + +struct mode { + uint32_t fourcc, width, height; +}; + +struct mapping { + void *address; + size_t size; +}; + +static int xioctl(int fd, unsigned long cmd, void *arg) +{ + int ret; + + do { + ret = ioctl(fd, cmd, arg); + } while (ret < 0 && errno == EINTR); + return ret; +} + +static double monotonic(void) +{ + struct timespec ts; + + clock_gettime(CLOCK_MONOTONIC, &ts); + return ts.tv_sec + ts.tv_nsec / 1000000000.0; +} + +static int capture(int fd, struct mode mode, unsigned int seconds, + unsigned int fps, unsigned int cycle) +{ + struct v4l2_format format = { .type = V4L2_BUF_TYPE_VIDEO_CAPTURE }; + struct v4l2_streamparm parm = { .type = V4L2_BUF_TYPE_VIDEO_CAPTURE }; + struct v4l2_requestbuffers req = { + .count = 4, .type = V4L2_BUF_TYPE_VIDEO_CAPTURE, .memory = V4L2_MEMORY_MMAP, + }; + struct mapping maps[MAX_BUFFERS] = {}; + enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + unsigned int frames = 0, bad = 0, gaps = 0, sequence_gaps = 0, mapped = 0; + unsigned int last_sequence = 0; + double start, end, first = 0, last = 0, max_gap = 0, measured_fps = 0; + bool streaming = false; + int ret = -1; + + format.fmt.pix.width = mode.width; + format.fmt.pix.height = mode.height; + format.fmt.pix.pixelformat = mode.fourcc; + format.fmt.pix.field = V4L2_FIELD_NONE; + if (xioctl(fd, VIDIOC_S_FMT, &format) < 0) + goto error; + /* Fail instead of counting a silently substituted mode as tested. */ + if (format.fmt.pix.width != mode.width || format.fmt.pix.height != mode.height || + format.fmt.pix.pixelformat != mode.fourcc) { + fprintf(stderr, "requested mode was substituted\n"); + goto out; + } + parm.parm.capture.timeperframe.numerator = 1; + parm.parm.capture.timeperframe.denominator = fps; + if (xioctl(fd, VIDIOC_S_PARM, &parm) < 0) + goto error; + if (parm.parm.capture.timeperframe.numerator != 1 || + parm.parm.capture.timeperframe.denominator != fps) { + fprintf(stderr, "requested rate was substituted\n"); + goto out; + } + if (xioctl(fd, VIDIOC_REQBUFS, &req) < 0) + goto error; + if (req.count < 2 || req.count > MAX_BUFFERS) + goto out; + for (unsigned int i = 0; i < req.count; i++) { + struct v4l2_buffer buf = { + .type = type, .memory = V4L2_MEMORY_MMAP, .index = i, + }; + + if (xioctl(fd, VIDIOC_QUERYBUF, &buf) < 0) + goto error; + maps[i].size = buf.length; + maps[i].address = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, + MAP_SHARED, fd, buf.m.offset); + if (maps[i].address == MAP_FAILED) + goto error; + mapped++; + if (xioctl(fd, VIDIOC_QBUF, &buf) < 0) + goto error; + } + if (xioctl(fd, VIDIOC_STREAMON, &type) < 0) + goto error; + streaming = true; + start = monotonic(); + end = start + seconds; + while (monotonic() < end) { + struct pollfd pfd = { .fd = fd, .events = POLLIN }; + struct v4l2_buffer buf = { .type = type, .memory = V4L2_MEMORY_MMAP }; + double timestamp, gap; + int ready = poll(&pfd, 1, 2000); + + if (ready < 0 && errno == EINTR) + continue; + if (ready <= 0 || (pfd.revents & (POLLERR | POLLHUP | POLLNVAL))) { + fprintf(stderr, "stream stalled or queue failed\n"); + goto out; + } + if (xioctl(fd, VIDIOC_DQBUF, &buf) < 0) + goto error; + if (buf.index >= req.count || buf.bytesused > maps[buf.index].size) + goto out; + if (buf.flags & V4L2_BUF_FLAG_ERROR) + bad++; + timestamp = buf.timestamp.tv_sec + buf.timestamp.tv_usec / 1000000.0; + if (frames) { + gap = timestamp - last; + if (gap > max_gap) + max_gap = gap; + if (gap <= 0 || gap > 1.5 / fps) + gaps++; + if ((uint32_t)(buf.sequence - last_sequence) != 1) + sequence_gaps++; + } else { + first = timestamp; + } + last = timestamp; + last_sequence = buf.sequence; + frames++; + /* Requeue immediately; image copies/conversion do not limit the test. */ + if (xioctl(fd, VIDIOC_QBUF, &buf) < 0) + goto error; + } + if (frames > 1 && last > first) + measured_fps = (frames - 1) / (last - first); + /* Allow clock/29.97-vs-30 rounding; gaps and buffer errors must be zero. */ + ret = bad || gaps || sequence_gaps || measured_fps < fps * 0.998 ? 1 : 0; + printf("{\"cycle\":%u,\"fourcc\":\"%.4s\",\"width\":%u,\"height\":%u,", + cycle, (char *)&mode.fourcc, mode.width, mode.height); + printf("\"stride\":%u,\"sizeimage\":%u,\"requested_fps\":%u,\"frames\":%u,", + format.fmt.pix.bytesperline, format.fmt.pix.sizeimage, fps, frames); + printf("\"delivered_fps\":%.6f,\"max_gap_ms\":%.3f,\"timing_gaps\":%u,", + measured_fps, max_gap * 1000, gaps); + printf("\"buffer_errors\":%u,\"sequence_gaps\":%u,\"pass\":%s}\n", + bad, sequence_gaps, ret ? "false" : "true"); + fflush(stdout); + goto out; +error: + perror("V4L2 ioctl"); +out: + if (streaming && xioctl(fd, VIDIOC_STREAMOFF, &type) < 0) { + perror("STREAMOFF"); + ret = -1; + } + for (unsigned int i = 0; i < mapped; i++) + munmap(maps[i].address, maps[i].size); + req.count = 0; + if (xioctl(fd, VIDIOC_REQBUFS, &req) < 0) + ret = -1; + return ret; +} + +int main(int argc, char **argv) +{ + struct mode modes[MAX_MODES]; + unsigned int count = 0, seconds, cycles, fps; + int fd, failed = 0; + char *end; + unsigned long values[3]; + + if (argc != 5 && argc != 8) { + fprintf(stderr, "usage: %s device seconds cycles fps [FOURCC width height]\n", + argv[0]); + return 2; + } + for (int i = 0; i < 3; i++) { + errno = 0; + values[i] = strtoul(argv[i + 2], &end, 10); + if (errno || *end || !values[i] || values[i] > 86400) + return 2; + } + seconds = values[0]; + cycles = values[1]; + fps = values[2]; + if (fps > 240 || cycles > 1000 || seconds < 5) + return 2; + fd = open(argv[1], O_RDWR | O_NONBLOCK); + if (fd < 0) { + perror(argv[1]); + return 2; + } + for (unsigned int i = 0; ; i++) { + struct v4l2_fmtdesc format = { .type = V4L2_BUF_TYPE_VIDEO_CAPTURE, .index = i }; + + if (xioctl(fd, VIDIOC_ENUM_FMT, &format) < 0) { + if (errno != EINVAL) + failed = 1; + break; + } + /* This harness exercises the driver's contiguous, single-planar API. */ + if (format.pixelformat == V4L2_PIX_FMT_NV12M) + continue; + for (unsigned int j = 0; ; j++) { + struct v4l2_frmsizeenum size = { + .pixel_format = format.pixelformat, .index = j, + }; + + if (xioctl(fd, VIDIOC_ENUM_FRAMESIZES, &size) < 0) { + if (errno != EINVAL) + failed = 1; + break; + } + if (size.type != V4L2_FRMSIZE_TYPE_DISCRETE || count == MAX_MODES) { + failed = 1; + break; + } + modes[count++] = (struct mode){format.pixelformat, size.discrete.width, + size.discrete.height}; + } + } + if (argc == 8) { + uint32_t fourcc; + unsigned long width, height; + bool found = false; + + if (strlen(argv[5]) != 4) { + close(fd); + return 2; + } + memcpy(&fourcc, argv[5], sizeof(fourcc)); + errno = 0; + width = strtoul(argv[6], &end, 10); + if (errno || *end || !width || width > UINT32_MAX) { + close(fd); + return 2; + } + errno = 0; + height = strtoul(argv[7], &end, 10); + if (errno || *end || !height || height > UINT32_MAX) { + close(fd); + return 2; + } + for (unsigned int i = 0; i < count; i++) { + if (modes[i].fourcc == fourcc && modes[i].width == width && + modes[i].height == height) { + modes[0] = modes[i]; + found = true; + break; + } + } + count = found ? 1 : 0; + } + if (!count) + failed = 1; + /* Long runs first, then repeated stops/restarts and format/size switches. */ + for (unsigned int cycle = 0; cycle <= cycles && !failed; cycle++) { + for (unsigned int i = 0; i < count; i++) { + if (capture(fd, modes[i], cycle ? 5 : seconds, fps, cycle)) { + failed = 1; + break; + } + } + } + close(fd); + return failed ? 1 : 0; +} From a86ad6c3ba438153cf5b39384d5d619bbf94cf3d Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 13:06:58 +1100 Subject: [PATCH 134/388] drm/apple: cancel brightness notifications only on desktop contexts KUnit's external unbind path caught cancellation of the desktop-only work item on a context which does not initialize it. Match the producer's firmware guard when stopping notifications. The legacy/Neo stop path then keeps its existing two delayed-work cancellations. Exercise the real stop helper with desktop and non-desktop profiles in the host fixture; retain panic_on_warn in the complete KUnit gate. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/dcp_backlight.c | 3 ++- .../testing/selftests/drivers/gpu/apple-dcp/desktop.py | 10 +++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp_backlight.c b/drivers/gpu/drm/apple/dcp_backlight.c index 3aeabc1982737..2a9318beb4d32 100644 --- a/drivers/gpu/drm/apple/dcp_backlight.c +++ b/drivers/gpu/drm/apple/dcp_backlight.c @@ -340,7 +340,8 @@ void dcp_backlight_stop(struct apple_dcp *dcp) { cancel_delayed_work_sync(&dcp->backlight.fade_work); cancel_delayed_work_sync(&dcp->backlight.retry_work); - cancel_work_sync(&dcp->backlight.notify_work); + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) + cancel_work_sync(&dcp->backlight.notify_work); } static void dcp_backlight_release(void *data) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py index 55212094c14fa..5ba5d5e05e550 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py @@ -781,7 +781,8 @@ def brightness_notifications(root): source=(root/'drivers/gpu/drm/apple/dcp_backlight.c').read_text() functions='\n'.join(block(source,name) for name in ( 'static void dcp_backlight_notify_work(', 'static void dcp_backlight_notify_release(', 'static void dcp_backlight_notify(', - 'static bool dcp_backlight_desktop(', 'void dcp_backlight_desktop_finish(')) + 'static bool dcp_backlight_desktop(', 'void dcp_backlight_desktop_finish(', + 'void dcp_backlight_stop(')) return prefix+r''' #include #include @@ -799,10 +800,11 @@ def brightness_notifications(root): struct apple_dcp { int fw_compat;bool quiescing,crashed; struct {struct backlight_device *bl_dev;} brightness; - struct {bool lock;struct dcp_backlight_state state;struct work_struct notify_work;int retry_work;} backlight; + struct {bool lock;struct dcp_backlight_state state;struct work_struct notify_work;int fade_work,retry_work;} backlight; struct {struct dcp_backlight_swap backlight;} *desktop; }; -static unsigned events,brightness,actual,kicks,cancelled; +static unsigned events,brightness,actual,kicks,cancelled,delayed_cancelled; +static void cancel_delayed_work_sync(int *work) {(void)work;delayed_cancelled++;} static void cancel_work_sync(struct work_struct *w) {(void)w;cancelled++;} static void kobject_uevent_env(int *k,int action,char **env) {(void)k;assert(action==1 && !strcmp(env[0],"SOURCE=unknown") && !env[1]);events++;} @@ -833,8 +835,10 @@ def brightness_notifications(root): dcp_backlight_desktop_finish(&d,9,true,false);assert(events==1); d.quiescing=true;dcp_backlight_notify(&d);assert(events==1); d.quiescing=false;d.fw_compat=1;dcp_backlight_notify(&d);assert(events==1); + dcp_backlight_stop(&d);assert(!cancelled && delayed_cancelled==2); d.fw_compat=5;dcp_backlight_notify_release(&d); assert(!d.brightness.bl_dev && cancelled==1);dcp_backlight_notify(&d);assert(events==1); + dcp_backlight_stop(&d);assert(cancelled==2 && delayed_cancelled==4); return 0; } ''' From 3bd32e8909402c48eea988b72df229093f2a5cf6 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 13:09:55 +1100 Subject: [PATCH 135/388] media: apple: isp: reject P010 command ACKs after the deadline Check the command deadline before advancing the ring cursor, including when an ACK arrives at the wait timeout boundary. Keep the failed channel latched until coprocessor stop. Cover the boundary in KUnit. Emit the capture stress result after cleanup so a failed STREAMOFF cannot leave a passing result. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/media/platform/apple/isp/isp-drv.h | 2 ++ .../media/platform/apple/isp/isp-ipc-test.c | 19 +++++++++++++++++++ drivers/media/platform/apple/isp/isp-ipc.c | 8 ++++++++ .../selftests/media/apple-isp-stress.c | 18 +++++++++--------- 4 files changed, 38 insertions(+), 9 deletions(-) diff --git a/drivers/media/platform/apple/isp/isp-drv.h b/drivers/media/platform/apple/isp/isp-drv.h index e02d675f0e998..db042f94aed47 100644 --- a/drivers/media/platform/apple/isp/isp-drv.h +++ b/drivers/media/platform/apple/isp/isp-drv.h @@ -117,6 +117,8 @@ struct isp_channel { void *virt; u32 doorbell; u32 cursor; + unsigned long deadline; + bool deadline_active; struct mutex lock; struct isp_message req; struct isp_message rsp; diff --git a/drivers/media/platform/apple/isp/isp-ipc-test.c b/drivers/media/platform/apple/isp/isp-ipc-test.c index efd24f6794a5a..8b6decd63f274 100644 --- a/drivers/media/platform/apple/isp/isp-ipc-test.c +++ b/drivers/media/platform/apple/isp/isp-ipc-test.c @@ -170,7 +170,26 @@ static void isp_test_command_ring_wrap(struct kunit *test) KUNIT_EXPECT_EQ(test, chan->cursor, 0); } +static void isp_test_late_ack_keeps_cursor(struct kunit *test) +{ + struct isp_sm_test *t = isp_sm_test_alloc(test); + struct isp_message response = { .arg0 = 0x5000001 }; + + isp_get_current_format(t->isp)->pixelformat = V4L2_PIX_FMT_P010; + t->chan->virt = &response; + t->chan->num = 8; + t->chan->req.arg0 = 0x5000000; + t->chan->deadline_active = true; + t->chan->deadline = jiffies; + KUNIT_EXPECT_FALSE(test, chan_tx_done(t->isp, t->chan)); + KUNIT_EXPECT_EQ(test, t->chan->cursor, 0); + t->chan->deadline = jiffies + HZ; + KUNIT_EXPECT_TRUE(test, chan_tx_done(t->isp, t->chan)); + KUNIT_EXPECT_EQ(test, t->chan->cursor, 1); +} + static struct kunit_case isp_ipc_test_cases[] = { + KUNIT_CASE(isp_test_late_ack_keeps_cursor), KUNIT_CASE(isp_test_command_ring_wrap), KUNIT_CASE(isp_test_sm_free_releases), KUNIT_CASE(isp_test_sm_free_alias), diff --git a/drivers/media/platform/apple/isp/isp-ipc.c b/drivers/media/platform/apple/isp/isp-ipc.c index da946f360ff5c..92cfc6ab35f5b 100644 --- a/drivers/media/platform/apple/isp/isp-ipc.c +++ b/drivers/media/platform/apple/isp/isp-ipc.c @@ -177,6 +177,9 @@ int ipc_chan_handle(struct apple_isp *isp, struct isp_channel *chan) static inline bool chan_tx_done(struct apple_isp *isp, struct isp_channel *chan) { + if (isp_is_p010(isp) && chan->deadline_active && + time_after_eq(jiffies, chan->deadline)) + return false; dma_rmb(); chan_read_msg(isp, chan, &chan->rsp); @@ -198,6 +201,10 @@ int ipc_chan_send(struct apple_isp *isp, struct isp_channel *chan, #endif if (isp->ipc_failed) return -EIO; + if (isp_is_p010(isp) && timeout) { + chan->deadline = jiffies + timeout; + chan->deadline_active = true; + } chan_write_msg(isp, chan, &chan->req); dma_wmb(); @@ -217,6 +224,7 @@ int ipc_chan_send(struct apple_isp *isp, struct isp_channel *chan, return -ETIME; } + chan->deadline_active = false; isp_dbg(isp, "%s: request success (%ld)\n", chan->name, t); return 0; diff --git a/tools/testing/selftests/media/apple-isp-stress.c b/tools/testing/selftests/media/apple-isp-stress.c index 8490a7abe3d68..cd85e51ecba09 100644 --- a/tools/testing/selftests/media/apple-isp-stress.c +++ b/tools/testing/selftests/media/apple-isp-stress.c @@ -147,15 +147,6 @@ static int capture(int fd, struct mode mode, unsigned int seconds, measured_fps = (frames - 1) / (last - first); /* Allow clock/29.97-vs-30 rounding; gaps and buffer errors must be zero. */ ret = bad || gaps || sequence_gaps || measured_fps < fps * 0.998 ? 1 : 0; - printf("{\"cycle\":%u,\"fourcc\":\"%.4s\",\"width\":%u,\"height\":%u,", - cycle, (char *)&mode.fourcc, mode.width, mode.height); - printf("\"stride\":%u,\"sizeimage\":%u,\"requested_fps\":%u,\"frames\":%u,", - format.fmt.pix.bytesperline, format.fmt.pix.sizeimage, fps, frames); - printf("\"delivered_fps\":%.6f,\"max_gap_ms\":%.3f,\"timing_gaps\":%u,", - measured_fps, max_gap * 1000, gaps); - printf("\"buffer_errors\":%u,\"sequence_gaps\":%u,\"pass\":%s}\n", - bad, sequence_gaps, ret ? "false" : "true"); - fflush(stdout); goto out; error: perror("V4L2 ioctl"); @@ -169,6 +160,15 @@ static int capture(int fd, struct mode mode, unsigned int seconds, req.count = 0; if (xioctl(fd, VIDIOC_REQBUFS, &req) < 0) ret = -1; + printf("{\"cycle\":%u,\"fourcc\":\"%.4s\",\"width\":%u,\"height\":%u,", + cycle, (char *)&mode.fourcc, mode.width, mode.height); + printf("\"stride\":%u,\"sizeimage\":%u,\"requested_fps\":%u,\"frames\":%u,", + format.fmt.pix.bytesperline, format.fmt.pix.sizeimage, fps, frames); + printf("\"delivered_fps\":%.6f,\"max_gap_ms\":%.3f,\"timing_gaps\":%u,", + measured_fps, max_gap * 1000, gaps); + printf("\"buffer_errors\":%u,\"sequence_gaps\":%u,\"pass\":%s}\n", + bad, sequence_gaps, ret ? "false" : "true"); + fflush(stdout); return ret; } From b17a883263f0252e2285836b5d9454065c98a6e1 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 13:12:47 +1100 Subject: [PATCH 136/388] media: apple: isp: requeue the last IMX958 NV12 buffer immediately Apply the empty-queue restart to both processed IMX958 formats. Exercise 10,000 empty-to-submitted transitions for each of P010 and NV12. Other sensors retain their queue policy. Assisted-by: LLM Signed-off-by: Ryan Murray --- .../media/platform/apple/isp/isp-v4l2-test.c | 30 +++++++++++-------- drivers/media/platform/apple/isp/isp-v4l2.c | 4 +-- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/drivers/media/platform/apple/isp/isp-v4l2-test.c b/drivers/media/platform/apple/isp/isp-v4l2-test.c index 47f1f8900d138..58af78581bf7e 100644 --- a/drivers/media/platform/apple/isp/isp-v4l2-test.c +++ b/drivers/media/platform/apple/isp/isp-v4l2-test.c @@ -286,7 +286,7 @@ static int isp_test_buffer_ack(struct apple_isp *isp, struct isp_channel *chan, return 0; } -static void isp_test_p010_last_lease_requeue(struct kunit *test) +static void isp_test_imx958_last_lease_requeue(struct kunit *test) { struct apple_isp *isp = isp_test_alloc(test); struct isp_format *fmt = isp_get_current_format(isp); @@ -296,6 +296,7 @@ static void isp_test_p010_last_lease_requeue(struct kunit *test) KUNIT_ASSERT_NOT_NULL(test, buf); isp->hw = &isp_test_t6040; + fmt->version = 0x958; fmt->pixelformat = V4L2_PIX_FMT_P010; fmt->num_planes = 2; fmt->plane_size[0] = 1843200; @@ -306,7 +307,7 @@ static void isp_test_p010_last_lease_requeue(struct kunit *test) isp->chan_bh = kunit_kzalloc(test, sizeof(*isp->chan_bh), GFP_KERNEL); KUNIT_ASSERT_NOT_NULL(test, isp->cmd_virt); KUNIT_ASSERT_NOT_NULL(test, isp->chan_bh); - for (unsigned int i = 0; i < isp_num_meta(isp); i++) { + for (unsigned int i = 0; i < ISP_MAX_BUFFERS; i++) { isp->meta_surfs[i] = kunit_kzalloc(test, sizeof(struct isp_surf), GFP_KERNEL); KUNIT_ASSERT_NOT_NULL(test, isp->meta_surfs[i]); isp->meta_surfs[i]->submitted = true; @@ -317,20 +318,25 @@ static void isp_test_p010_last_lease_requeue(struct kunit *test) set_bit(ISP_STATE_STREAMING, &isp->state); test->priv = &calls; kunit_activate_static_stub(test, ipc_chan_send, isp_test_buffer_ack); - for (unsigned int i = 1; i <= 10000; i++) { - /* Both lists empty, as after the final completion was dequeued. */ - isp_vb2_buf_queue(&buf->vb.vb2_buf); - KUNIT_ASSERT_EQ(test, calls, i); - KUNIT_ASSERT_EQ(test, buf->tag, (u64)i); - KUNIT_ASSERT_TRUE(test, list_empty(&isp->bufs_pending)); - submitted = list_first_entry(&isp->bufs_submitted, struct isp_buffer, link); - KUNIT_ASSERT_PTR_EQ(test, submitted, buf); - list_del(&buf->link); + for (unsigned int format = 0; format < 2; format++) { + fmt->pixelformat = format ? V4L2_PIX_FMT_NV12 : V4L2_PIX_FMT_P010; + calls = 0; + for (unsigned int i = 1; i <= 10000; i++) { + /* Both lists empty, as after the last completion was dequeued. */ + isp_vb2_buf_queue(&buf->vb.vb2_buf); + KUNIT_ASSERT_EQ(test, calls, i); + if (!format) + KUNIT_ASSERT_EQ(test, buf->tag, (u64)i); + KUNIT_ASSERT_TRUE(test, list_empty(&isp->bufs_pending)); + submitted = list_first_entry(&isp->bufs_submitted, struct isp_buffer, link); + KUNIT_ASSERT_PTR_EQ(test, submitted, buf); + list_del(&buf->link); + } } } static struct kunit_case isp_v4l2_test_cases[] = { - KUNIT_CASE(isp_test_p010_last_lease_requeue), + KUNIT_CASE(isp_test_imx958_last_lease_requeue), KUNIT_CASE(isp_test_p010_report_defers_completion), KUNIT_CASE(isp_test_p010_mode_selection), KUNIT_CASE(isp_test_p010_format), diff --git a/drivers/media/platform/apple/isp/isp-v4l2.c b/drivers/media/platform/apple/isp/isp-v4l2.c index 35c30582ab81d..4fcde29410d2c 100644 --- a/drivers/media/platform/apple/isp/isp-v4l2.c +++ b/drivers/media/platform/apple/isp/isp-v4l2.c @@ -452,9 +452,9 @@ static void isp_vb2_buf_queue(struct vb2_buffer *vb) list_add_tail(&buf->link, &isp->bufs_pending); spin_unlock_irqrestore(&isp->buf_lock, flags); - /* A returned last lease must restart an empty P010 queue immediately. */ + /* Restart an empty IMX958 queue when its last returned lease is queued. */ if (test_bit(ISP_STATE_STREAMING, &isp->state) && - (isp_is_p010(isp) || !empty)) { + (isp_supports_p010(isp) || !empty)) { if (isp_submit_buffers(isp, false) && isp_is_p010(isp)) { apple_isp_firmware_halt(isp); isp_vb2_release_buffers(isp, VB2_BUF_STATE_ERROR); From 7f571507eb1a8140721067d50321a24efe58d59d Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 13:23:46 +1100 Subject: [PATCH 137/388] docs: media: fix J616s ISP protocol table markup Size the command table columns for the full H17 record sizes and fix the document title underline. The standalone RST parser now accepts the document with warnings treated as errors. Assisted-by: LLM Signed-off-by: Ryan Murray --- .../media/drivers/apple-isp-j616s.rst | 58 ++++++++++--------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/Documentation/driver-api/media/drivers/apple-isp-j616s.rst b/Documentation/driver-api/media/drivers/apple-isp-j616s.rst index 7413c0e866460..707ac2c22e6e7 100644 --- a/Documentation/driver-api/media/drivers/apple-isp-j616s.rst +++ b/Documentation/driver-api/media/drivers/apple-isp-j616s.rst @@ -1,7 +1,7 @@ .. SPDX-License-Identifier: GPL-2.0-only J616s ISP modes and capture protocol -================================== +==================================== Sources and scope ----------------- @@ -122,32 +122,32 @@ Command stream IO commands have a zero/status u32 followed by the opcode u32, then the parameters below. Sizes include that eight-byte header. ``ch`` is zero. -================ =================================== ==== =================== -Opcode Command Size Parameters -================ =================================== ==== =================== -0x0003 CONFIG_GET 0x1c Camera channel count -0x3206 SET_DSID_CLR_MULTI_BC_REG_BASE 0x50 8 slots, range 0x2fc -0x0000 START 0x0c Proxy 0x11, Linux 0 -0x010d CH_INFO_GET 0x190 ch -0x0106 CH_CAMERA_CONFIG_GET 0x120 ch, index (all) -0x0024 FLICKER_SENSOR_SET 0x0c 0 -0x013b CH_SBS_ENABLE 0x10 ch, 1 -0x0107 CH_CAMERA_CONFIG_SELECT 0x10 ch, selected config -0x010e CH_BUFFER_RECYCLE_MODE_SET 0x10 ch, 1 (empty only) -0x010f CH_BUFFER_RECYCLE_START 0x0c ch -0x080c CH_CROP_SCL1_SET 0x1c ch, x, y, width, height -0x0b09 CH_OUTPUT_CONFIG_SCL1_SET 0x38 ch, output geometry -0x0208 CH_AE_FRAME_RATE_MAX_SET 0x10 ch, fps * 256 -0x020a CH_AE_FRAME_RATE_MIN_SET 0x10 ch, min_fps * 256 -0x0117 CH_BUFFER_POOL_CONFIG_SET (meta) 0x9c ch, pool 0, count 8 -0x0117 CH_BUFFER_POOL_CONFIG_SET (output) 0x9c ch, pool 9, count 2 -0x0125 CH_LOCAL_RAW_BUFFER_ENABLE 0x10 ch, 1 -0x0b0d CH_PREVIEW_STREAM_SET 0x10 ch, 1 -0x0138 CH_MASTER_SLAVE_SYNC_MODE_SET 0x10 ch, 0 -0x0100 CH_START 0x0c ch -0x0101 CH_STOP 0x0c ch -0x0104 CH_BUFFER_RETURN 0x0c ch -================ =================================== ==== =================== +====== ================================== ===== ======================= +Opcode Command Size Parameters +====== ================================== ===== ======================= +0x0003 CONFIG_GET 0x1c Camera channel count +0x3206 SET_DSID_CLR_MULTI_BC_REG_BASE 0x50 8 slots, range 0x2fc +0x0000 START 0x0c Proxy 0x11, Linux 0 +0x010d CH_INFO_GET 0x190 ch +0x0106 CH_CAMERA_CONFIG_GET 0x120 ch, index (all) +0x0024 FLICKER_SENSOR_SET 0x0c 0 +0x013b CH_SBS_ENABLE 0x10 ch, 1 +0x0107 CH_CAMERA_CONFIG_SELECT 0x10 ch, selected config +0x010e CH_BUFFER_RECYCLE_MODE_SET 0x10 ch, 1 (empty only) +0x010f CH_BUFFER_RECYCLE_START 0x0c ch +0x080c CH_CROP_SCL1_SET 0x1c ch, x, y, width, height +0x0b09 CH_OUTPUT_CONFIG_SCL1_SET 0x38 ch, output geometry +0x0208 CH_AE_FRAME_RATE_MAX_SET 0x10 ch, fps * 256 +0x020a CH_AE_FRAME_RATE_MIN_SET 0x10 ch, min_fps * 256 +0x0117 CH_BUFFER_POOL_CONFIG_SET (meta) 0x9c ch, pool 0, count 8 +0x0117 CH_BUFFER_POOL_CONFIG_SET (output) 0x9c ch, pool 9, count 2 +0x0125 CH_LOCAL_RAW_BUFFER_ENABLE 0x10 ch, 1 +0x0b0d CH_PREVIEW_STREAM_SET 0x10 ch, 1 +0x0138 CH_MASTER_SLAVE_SYNC_MODE_SET 0x10 ch, 0 +0x0100 CH_START 0x0c ch +0x0101 CH_STOP 0x0c ch +0x0104 CH_BUFFER_RETURN 0x0c ch +====== ================================== ===== ======================= The DSID bases are 0x2201d4000, 0x2221d4000, 0x2241d4000, 0x2261d4000, then four U64_MAX sentinels. Linux retains its existing boot-time PMP and @@ -205,7 +205,9 @@ is source-level ABI evidence, not a new live qualification of P010. selecting P010 would violate the proxy's guarded startup lifecycle. The new config query explicitly exchanges 0x120, rather than the unused legacy 0xdc current-config helper. Header/channel/ index checks and duplicated geometry checks guard decoding. The H17 - appended bytes remain uninterpreted. + appended bytes remain uninterpreted. The old preview validator also checks + CH_INFO_GET byte 0x138; the current Linux helper retains only the common + 0x118-byte prefix of its 0x190-byte reply and does not make that check. * Metadata: CH_INFO_GET reports 0x4d00 on both paths. The proxy rounds its metadata backing/pool to 0x8000; P010 does the same even on a 4 KiB host. NV12 retains the current driver's 0x4d00 pool and 16-buffer capacity. From 946458f3814ffd01ef9f2f3737e7c455363112f7 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 13:48:05 +1100 Subject: [PATCH 138/388] media: apple: isp: accept full-width P010 completion address pairs The buffer wire carries 64-bit addresses, while the proxy observed zero-extended low words on its firmware. Accept either complete canonical pair for a submitted lease and reject mixed or unrelated upper bits. Keep pool, plane and fresh-tag checks. Also assert that NV12 stays first in format enumeration. Assisted-by: LLM Signed-off-by: Ryan Murray --- .../driver-api/media/drivers/apple-isp-j616s.rst | 5 +++-- drivers/media/platform/apple/isp/isp-v4l2-test.c | 14 ++++++++++++-- drivers/media/platform/apple/isp/isp-v4l2.c | 11 +++++++---- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/Documentation/driver-api/media/drivers/apple-isp-j616s.rst b/Documentation/driver-api/media/drivers/apple-isp-j616s.rst index 707ac2c22e6e7..f92cecf30f3e5 100644 --- a/Documentation/driver-api/media/drivers/apple-isp-j616s.rst +++ b/Documentation/driver-api/media/drivers/apple-isp-j616s.rst @@ -81,8 +81,9 @@ Linux passes its actual contiguous extents to the rendered pool instead. P010 uses the proxy's eight metadata slots (0x8000 bytes each) and two rendered slots. The queue limits userspace to two rendered buffers, matching -the pool capacity. Completion matches both zero-extended low-32-bit plane -addresses, plane count, pool 9 and a fresh 64-bit submission tag; an old tag +the pool capacity. Completion matches either both full submitted addresses +or both zero-extended low-32-bit plane addresses, plus plane count, pool 9 +and a fresh 64-bit submission tag; an old tag cannot release a new lease. Buffer completion is published to videobuf2 only after the firmware report ACK and doorbell have been published. NV12 pool sizes and the T8140/Neo formats, presets and command path retain diff --git a/drivers/media/platform/apple/isp/isp-v4l2-test.c b/drivers/media/platform/apple/isp/isp-v4l2-test.c index 58af78581bf7e..6af295a7da23e 100644 --- a/drivers/media/platform/apple/isp/isp-v4l2-test.c +++ b/drivers/media/platform/apple/isp/isp-v4l2-test.c @@ -166,13 +166,16 @@ static void isp_test_p010_admission(struct kunit *test) struct apple_isp *isp = isp_test_alloc(test); struct isp_format *fmt = isp_get_current_format(isp); struct isp_preset preset = { .output_dim = {1280, 720}, .p010 = true }; - struct v4l2_fmtdesc desc = { .index = 1 }; + struct v4l2_fmtdesc desc = { .index = 0 }; isp->presets = &preset; isp->num_presets = 1; isp->hw = &isp_test_t6040; fmt->version = 0x958; KUNIT_ASSERT_EQ(test, isp_enum_pixelformat(isp, &desc), 0); + KUNIT_EXPECT_EQ(test, desc.pixelformat, V4L2_PIX_FMT_NV12); + desc.index = 1; + KUNIT_ASSERT_EQ(test, isp_enum_pixelformat(isp, &desc), 0); KUNIT_EXPECT_EQ(test, desc.pixelformat, V4L2_PIX_FMT_P010); desc.index = 2; KUNIT_EXPECT_EQ(test, isp_enum_pixelformat(isp, &desc), -EINVAL); @@ -203,8 +206,15 @@ static void isp_test_p010_completion_identity(struct kunit *test) KUNIT_EXPECT_FALSE(test, isp_p010_buffer_matches(&fmt, &buf, &desc)); desc.iovas[1]--; desc.iovas[0] |= BIT_ULL(40); + /* Reject mixed encodings, accept both exact full submitted addresses. */ + KUNIT_EXPECT_FALSE(test, isp_p010_buffer_matches(&fmt, &buf, &desc)); + desc.iovas[1] |= BIT_ULL(40); + KUNIT_EXPECT_TRUE(test, isp_p010_buffer_matches(&fmt, &buf, &desc)); + desc.iovas[0] |= BIT_ULL(41); + desc.iovas[1] |= BIT_ULL(41); KUNIT_EXPECT_FALSE(test, isp_p010_buffer_matches(&fmt, &buf, &desc)); - desc.iovas[0] &= ~BIT_ULL(40); + desc.iovas[0] = (u32)desc.iovas[0]; + desc.iovas[1] = (u32)desc.iovas[1]; desc.pool_type = CISP_POOL_TYPE_RENDERED; KUNIT_EXPECT_FALSE(test, isp_p010_buffer_matches(&fmt, &buf, &desc)); desc.pool_type = CISP_POOL_TYPE_RENDERED_SCL1; diff --git a/drivers/media/platform/apple/isp/isp-v4l2.c b/drivers/media/platform/apple/isp/isp-v4l2.c index 4fcde29410d2c..b5bfea73e2d82 100644 --- a/drivers/media/platform/apple/isp/isp-v4l2.c +++ b/drivers/media/platform/apple/isp/isp-v4l2.c @@ -59,11 +59,14 @@ static bool isp_p010_buffer_matches(const struct isp_format *fmt, const struct isp_buffer *buf, const struct isp_buflist_buffer *desc) { - /* H17 reports zero-extended low 32 bits, not the submitted high IOVA. */ + u64 y = buf->surfs[0].iova; + u64 uv = y + fmt->plane_size[0]; + bool full = desc->iovas[0] == y && desc->iovas[1] == uv; + bool low = desc->iovas[0] == (u32)y && desc->iovas[1] == (u32)uv; + + /* Accept the submitted pair or the zero-extended pair observed on H17. */ return desc->num_planes == 2 && - desc->pool_type == CISP_POOL_TYPE_RENDERED_SCL1 && - desc->iovas[0] == (u32)buf->surfs[0].iova && - desc->iovas[1] == (u32)(buf->surfs[0].iova + fmt->plane_size[0]) && + desc->pool_type == CISP_POOL_TYPE_RENDERED_SCL1 && (full || low) && !desc->iovas[2] && !desc->iovas[3] && desc->tag == buf->tag; } From 154afdbd8987d796fba232760e32d671da4b818c Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 13:52:36 +1100 Subject: [PATCH 139/388] drm/apple: derive J616 adaptive sync capability from its DT panel The J616 panel compatible identifies the ProMotion panel. Record that capability when reading the panel node and use it with the existing firmware, main-display, mode-range and failure checks. Remove the T6041 VRR module parameter so support follows the DT without a boot flag. Exercise capable and incapable panels and retain the fixed-refresh retry. Assisted-by: LLM Signed-off-by: Ryan Murray --- Documentation/gpu/apple-dcp-t6041-panel.rst | 7 ++++--- drivers/gpu/drm/apple/dcp-internal.h | 2 ++ drivers/gpu/drm/apple/dcp.c | 2 ++ drivers/gpu/drm/apple/iomfb.c | 7 +------ .../selftests/drivers/gpu/apple-dcp/display.py | 11 +++++------ 5 files changed, 14 insertions(+), 15 deletions(-) diff --git a/Documentation/gpu/apple-dcp-t6041-panel.rst b/Documentation/gpu/apple-dcp-t6041-panel.rst index 564d7e3bb38c9..c615dc32ac1e7 100644 --- a/Documentation/gpu/apple-dcp-t6041-panel.rst +++ b/Documentation/gpu/apple-dcp-t6041-panel.rst @@ -26,9 +26,10 @@ does not synthesize lower refresh modes from a 120 Hz mode. Fractional rates remain in 16.16 Hz until the DRM pixel clock is calculated. A ProMotion mode needs desktop presentation deadlines even with adaptive sync disabled. -Adaptive sync is experimental and disabled by default. To qualify the internal -panel, boot with ``appledrm.t6041_vrr=1``. This enables the connector capability -only for the internal main display and a valid adaptive-sync range. The +The DT panel compatible ``apple,panel-j616`` identifies the ProMotion panel and +enables adaptive sync support without a module parameter. The connector capability +requires the desktop firmware profile, the internal main display, and a valid +adaptive-sync range. Other panel compatibles do not acquire this capability. The existing internal ProMotion convention supplies 24 Hz to the mode rate when the firmware omits both range fields or reports both as zero placeholders; a partial, duplicate or invalid range diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index 7a3a969fd7e76..b720cd80c11d6 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -186,6 +186,8 @@ struct dcp_panel { int height_mm; /// panel has a mini-LED backlight bool has_mini_led; + /* The DT panel compatible identifies an adaptive-sync capable panel. */ + bool has_vrr; }; enum dcp_iomfb_method_profile { diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 6a2eebc1cf057..1d0b81aa3ad65 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -4937,6 +4937,8 @@ static int dcp_comp_bind_pipe(struct device *dev, struct device *main, void *dat dev_err(dev, "Missing property 'apple,max-brightness'\n"); } + /* J616 ProMotion capability belongs to the panel, not a boot option. */ + dcp->panel.has_vrr = of_device_is_compatible(panel_np, "apple,panel-j616"); of_property_read_u32(panel_np, "width-mm", &dcp->panel.width_mm); /* use adjusted height as long as the notch is hidden */ of_property_read_u32(panel_np, height_prop[!dcp->notch_height], diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index 6d38d96ed1365..b56d74a7ac19e 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -142,14 +142,9 @@ static inline u64 dcpep_ack(enum dcp_context_id id) return dcpep_msg(id, 0, 0) | IOMFB_MSG_ACK; } -/* Keep the established fixed-refresh default until panel qualification. */ -static bool t6041_vrr; -module_param(t6041_vrr, bool, 0444); -MODULE_PARM_DESC(t6041_vrr, "Enable experimental adaptive sync on the T6041 internal panel"); - bool dcp_desktop_vrr_supported(struct apple_dcp *dcp) { - return t6041_vrr && dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && + return dcp->panel.has_vrr && dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && dcp_has_panel(dcp) && dcp->main_display && !READ_ONCE(dcp->desktop->vrr_failed); } diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/display.py b/tools/testing/selftests/drivers/gpu/apple-dcp/display.py index 5ee71a0b20993..1236325213994 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/display.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/display.py @@ -226,14 +226,13 @@ def adaptive(root): struct dcp_wait_cookie { u32 status; bool done,replied; }; struct dcp_set_digital_out_mode_req {u32 color_mode_id,timing_mode_id;}; struct dcp_set_parameter_dcp { u32 param; u64 value[4]; u32 count; }; -struct apple_dcp {void *dev;int fw_compat;bool main_display,panel;struct {bool vrr_failed,modeset_callback_error;} *desktop;}; -static bool t6041_vrr; +struct apple_dcp {void *dev;int fw_compat;bool main_display,has_panel;struct {bool has_vrr;} panel;struct {bool vrr_failed,modeset_callback_error;} *desktop;}; struct drm_display_mode {int id;}; struct drm_crtc_state {bool vrr_enabled;struct drm_display_mode mode;}; struct dcp_display_mode {bool vrr;}; static bool lookup_mode(struct apple_dcp *d,struct drm_display_mode *m,struct dcp_display_mode *out) {out->vrr=m->id==120;return m->id==60 || m->id==120;} -static bool dcp_has_panel(struct apple_dcp *d) {return d->panel;} +static bool dcp_has_panel(struct apple_dcp *d) {return d->has_panel;} static u32 get_unaligned_le32(const void *p) {u32 v;memcpy(&v,p,4);return v;} typedef void (*callback)(struct apple_dcp *,void *,void *); static callback pending; @@ -262,18 +261,18 @@ def adaptive(root): {u32 zero=0;reply(d,2,&zero);assert(requested.color_mode_id==11);reply(d,3,&zero);assert(m->wait.done && m->wait.replied && !m->wait.status);} int main(void) { - struct apple_dcp d={.fw_compat=DCP_FIRMWARE_V_26_6_DESKTOP,.main_display=true,.panel=true}; + struct apple_dcp d={.fw_compat=DCP_FIRMWARE_V_26_6_DESKTOP,.main_display=true,.has_panel=true}; __typeof__(*d.desktop) desktop={0};d.desktop=&desktop; struct drm_crtc_state state={.vrr_enabled=true,.mode={120}}; assert(dcp_desktop_vrr_check(&d,&state)==-EINVAL); - assert(!dcp_desktop_vrr_supported(&d));t6041_vrr=true; + assert(!dcp_desktop_vrr_supported(&d));d.panel.has_vrr=true; assert(!dcp_desktop_vrr_check(&d,&state)); state.mode.id=60;assert(dcp_desktop_vrr_check(&d,&state)==-EINVAL); state.vrr_enabled=false;assert(!dcp_desktop_vrr_check(&d,&state)); state.vrr_enabled=true;state.mode.id=120; assert(dcp_desktop_vrr_supported(&d)); d.main_display=false;assert(!dcp_desktop_vrr_supported(&d));d.main_display=true; - d.panel=false;assert(!dcp_desktop_vrr_supported(&d));d.panel=true; + d.has_panel=false;assert(!dcp_desktop_vrr_supported(&d));d.has_panel=true; d.fw_compat=3;assert(!dcp_desktop_vrr_supported(&d));d.fw_compat=DCP_FIRMWARE_V_26_6_DESKTOP; struct dcp_modeset_cookie m;u32 zero=0,error=1; initialize(&m);dcp_modeset_sync(&d,&m); From 0e173632391454447506ea7c8259d47cb696a625 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 13:52:37 +1100 Subject: [PATCH 140/388] drm/apple: use the existing KMS debug category for identity inspection Remove the panel_identity_debug module parameter. The existing DRM KMS debug category gates structural inspection and its bounded per-callback budget. Disabled inspection consumes no records, and no values are logged. Update the host fixture to exercise the existing debug gate. Assisted-by: LLM Signed-off-by: Ryan Murray --- Documentation/gpu/apple-dcp-t6041-panel.rst | 7 ++++--- drivers/gpu/drm/apple/iomfb.c | 8 ++------ tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py | 6 ++++-- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/Documentation/gpu/apple-dcp-t6041-panel.rst b/Documentation/gpu/apple-dcp-t6041-panel.rst index c615dc32ac1e7..350d22e927e13 100644 --- a/Documentation/gpu/apple-dcp-t6041-panel.rst +++ b/Documentation/gpu/apple-dcp-t6041-panel.rst @@ -77,8 +77,8 @@ updates for the same vendor/product retain known optional identity fields. Desktop identity diagnostics and fallback ----------------------------------------- -Enable ``appledrm.panel_identity_debug=1`` for a diagnostic boot (or set the -module's ``panel_identity_debug`` parameter before a modeset). D552, D561, +Identity inspection uses the existing DRM KMS debug category (``drm.debug`` +bit 2), with no driver-specific switch. When that category is enabled, D552, D561, D114 and chunked property delivery report the outer key, serialized key names, value type numbers and lengths. D000 identifies chunked delivery. Type numbers are dictionary 1, array 2, integer 4, string 9, blob 10 and boolean 11. Values @@ -86,7 +86,8 @@ are never printed. Each property walk stops at depth 4 or 64 objects, and the DCP stops after 128 diagnostic records per callback class (512 total), with bounded inspection-status lines in addition. Atomic reservations include framing records. D552, D561, D114 and chunked delivery have separate budgets so periodic statistics cannot hide the panel setters. -The switch defaults off. Malformed serialization only stops inspection. +When KMS debugging is disabled no inspection budget is consumed. Malformed +serialization only stops inspection. A real EDID with a usable identity takes priority. It may arrive as raw bytes, a serialized blob, or an ``EDID``, ``edid`` or ``IODisplayEDID`` blob nested diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index b56d74a7ac19e..fb6d435787cae 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -30,6 +30,7 @@ #include #include #include +#include #include #include "dcp.h" @@ -40,11 +41,6 @@ #include "parser.h" #include "trace.h" -/* Enable for one diagnostic boot with appledrm.panel_identity_debug=1. */ -static bool panel_identity_debug; -module_param(panel_identity_debug, bool, 0600); -MODULE_PARM_DESC(panel_identity_debug, "Bounded desktop panel property keys and types (no values)"); - struct dcp_identity_trace { struct apple_dcp *dcp; atomic_t *count; @@ -74,7 +70,7 @@ void dcp_trace_panel_property(struct apple_dcp *dcp, int tag, const char *key, u32 slot = tag == 552 ? 0 : tag == 561 ? 1 : tag == 114 ? 2 : 3; int ret; - if (!READ_ONCE(panel_identity_debug) || + if (!drm_debug_enabled(DRM_UT_KMS) || dcp->fw_compat != DCP_FIRMWARE_V_26_6_DESKTOP || !dcp_has_panel(dcp)) return; trace.dcp = dcp; diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py index 5ba5d5e05e550..b7125295bcc9d 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py @@ -863,7 +863,9 @@ def identity_trace_budget(root): #define min_t(t,a,b) ((t)(a)<(t)(b)?(t)(a):(t)(b)) #define DCP_FIRMWARE_V_26_6_DESKTOP 5 struct apple_dcp {int fw_compat;bool has_panel;void *dev;struct {atomic_t identity_log_count[4];} *desktop;}; -static bool panel_identity_debug; +static bool kms_debug; +#define DRM_UT_KMS 4 +static bool drm_debug_enabled(int category) {assert(category==DRM_UT_KMS);return kms_debug;} static unsigned messages; static char output[256]; static bool dcp_has_panel(struct apple_dcp *d) {return d->has_panel;} @@ -889,7 +891,7 @@ def identity_trace_budget(root): __typeof__(*d.desktop) desktop={0};d.desktop=&desktop; const char secret[]="SYNTHETIC_VALUE_NOT_FOR_LOGS"; dcp_trace_panel_property(&d,561,"Property",8,secret,sizeof(secret));assert(!messages); - panel_identity_debug=true;d.fw_compat=1; + kms_debug=true;d.fw_compat=1; dcp_trace_panel_property(&d,561,"Property",8,secret,sizeof(secret));assert(!messages); d.fw_compat=5;d.has_panel=false; dcp_trace_panel_property(&d,561,"Property",8,secret,sizeof(secret));assert(!messages); From a18546bbc60eef5835242babc1a9ad26831951af Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 13:53:08 +1100 Subject: [PATCH 141/388] docs: explain DT-driven T6041 capability and loader handoff contract Record that the kernel already adopts published handoff properties without a chosen opt-in. Removing the current Stage 2 switch requires a producer change in m1n1, keeping its transactional validation and the kernel's resource, firmware and session checks. Describe the required producer and test behavior without adding a policy flag or weakening the consumer. Assisted-by: LLM Signed-off-by: Ryan Murray --- Documentation/gpu/apple-dcp-t6041-panel.rst | 30 +++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/Documentation/gpu/apple-dcp-t6041-panel.rst b/Documentation/gpu/apple-dcp-t6041-panel.rst index 350d22e927e13..aa15af0f59913 100644 --- a/Documentation/gpu/apple-dcp-t6041-panel.rst +++ b/Documentation/gpu/apple-dcp-t6041-panel.rst @@ -131,3 +131,33 @@ both ``brightness`` and ``actual_brightness``. The work runs outside the receive path's locks and is drained during shutdown. Takeover sends the same event when it updates the inherited level. Requested brightness remains the user's target through the fade; notifications do not replace it with an intermediate level. + +DT capability and boot-loader handoff +----------------------------------- + +The J616 panel's existing DT compatible supplies adaptive-sync capability; +there is no T6041 VRR module parameter. Firmware and mode validation, the main +panel restriction, and the fixed-refresh failure latch still apply. EDID and +backlight notifications have no driver-specific tunables. + +The kernel does not consume ``chosen.asahi,t6041-dcp``. Its early T6041 gate +already follows the handoff published by m1n1: ``apple,t6041-handoff = <1>`` on +DCP, display and PIODMA, firmware compatibility 26.6.2, the expected DART stream +and mailbox relationships, initially disabled nodes, and a running DCP. +The DCP driver separately validates register resources and the firmware memory +handoff. Missing or invalid handoff data leaves the boot framebuffer in place. +These checks must remain when the loader's policy changes. + +The current m1n1 producer uses that chosen property to decide whether to attempt +publication. Removing it cannot be completed by a kernel change: a loader with +no published handoff provides nothing for the kernel to adopt. The producer +needs to select the T6041 handoff from its existing chip/board match and the +supported DCP/display/panel DT compatibles, then run the existing ADT, power, +running-session, firmware-segment, reservation and live DART mapping checks +without a chosen opt-in. Only a successful transaction may publish the existing +markers, memory regions and firmware properties. Keeping nodes disabled until +the kernel accepts that contract preserves compatibility with older kernels. +A failed transaction must publish no partial handoff. Loader host tests should +cover supported DT without the chosen property, an unsupported board/DT, and +each failed validation, as well as unchanged other-SoC policy. No new policy +flag or kernel bypass is needed. This kernel series does not replace Stage 2. From 6741e1e8703b823edbfb2cf0e067bc51da15797a Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 11:48:36 +1100 Subject: [PATCH 142/388] arm64: dts: apple: repair the J616s PCI Wi-Fi schema description Carry the independent #458 prerequisite for complete T6040 DT validation. Keep the PCI identity and family fallback and omit the redundant root-port compatible. Its diff can disappear once #458 lands. Link: https://github.com/aurora-silicon/linux/pull/458 Assisted-by: LLM Signed-off-by: Ryan Murray --- .../devicetree/bindings/net/wireless/brcm,bcm4329-fmac.yaml | 1 + arch/arm64/boot/dts/apple/t6040-j616s.dts | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Documentation/devicetree/bindings/net/wireless/brcm,bcm4329-fmac.yaml b/Documentation/devicetree/bindings/net/wireless/brcm,bcm4329-fmac.yaml index 81fd3e37452a6..8f935a2d2b320 100644 --- a/Documentation/devicetree/bindings/net/wireless/brcm,bcm4329-fmac.yaml +++ b/Documentation/devicetree/bindings/net/wireless/brcm,bcm4329-fmac.yaml @@ -46,6 +46,7 @@ properties: - cypress,cyw4373-fmac - cypress,cyw43012-fmac - infineon,cyw43439-fmac + - pci14e4,4434 # BCM4388 - const: brcm,bcm4329-fmac - enum: - brcm,bcm4329-fmac diff --git a/arch/arm64/boot/dts/apple/t6040-j616s.dts b/arch/arm64/boot/dts/apple/t6040-j616s.dts index a40a3fa1e1dc0..65ee34b8b362f 100644 --- a/arch/arm64/boot/dts/apple/t6040-j616s.dts +++ b/arch/arm64/boot/dts/apple/t6040-j616s.dts @@ -569,7 +569,6 @@ device_type = "pci"; port00: pci@0,0 { - compatible = "pciclass,0604"; reg = <0 0 0 0 0>; ranges; interrupt-map = <0 0 0 1 &port00 0 0 0 0 0 0 0 2 &port00 0 0 0 1 0 0 0 3 &port00 0 0 0 2 0 0 0 4 &port00 0 0 0 3>; @@ -592,7 +591,7 @@ }; wifi@0,0 { - compatible = "brcm,bcm4329-fmac"; + compatible = "pci14e4,4434", "brcm,bcm4329-fmac"; reg = <0 0 0 0 0>; brcm,ccode-map-trivial; brcm,board-type = "apple,hostomel"; From 804bba86ca55cb0598e90c99b3dafc3e48b40c78 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 11:48:37 +1100 Subject: [PATCH 143/388] drm/asahi: use the loaded image to select identified firmware Treat the OS version as a candidate-order hint. Continue to require an exact admitted loaded TEXT image and preserve #453's images list. This extends Jonathan Goyvaerts' shared identity work. Originally-by: Jonathan Goyvaerts Link: https://github.com/aurora-silicon/linux/pull/428 Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/asahi/g17/fw/variant.rs | 10 ++++------ drivers/gpu/drm/asahi/g17/mod.rs | 20 +++++++++++--------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/fw/variant.rs b/drivers/gpu/drm/asahi/g17/fw/variant.rs index d2d6d2404dfc8..78951f6d10a7e 100644 --- a/drivers/gpu/drm/asahi/g17/fw/variant.rs +++ b/drivers/gpu/drm/asahi/g17/fw/variant.rs @@ -64,10 +64,9 @@ pub(crate) struct Variant { pub(crate) programs: super::render::RegisterPrograms, } -/// A firmware build a SoC supports, selected by the macOS release it ships with and proven by -/// its loaded TEXT. +/// A firmware build a SoC supports, identified by its loaded TEXT. pub(crate) struct Build { - /// macOS version (major, minor, patch). + /// macOS version (major, minor, patch), used to order image candidates. pub(crate) os_version: [u32; 3], /// Name, for the log. pub(crate) name: &'static str, @@ -81,9 +80,8 @@ pub(crate) struct Build { pub(crate) enum Firmware { /// The SoC runs a single build, which is not identified (A18 Pro). Fixed(&'static Variant), - /// One of these builds, selected by the macOS version that the boot loader reports in - /// `apple,firmware-version` (the release whose boot firmware loaded the GPU firmware) and - /// then proven by the loaded TEXT. Any other version or image is refused. + /// One of these exact loaded images. The loader's `apple,firmware-version` is only + /// a candidate-ordering hint because multiple OS releases can share an iBoot string. Identified(&'static [Build]), } diff --git a/drivers/gpu/drm/asahi/g17/mod.rs b/drivers/gpu/drm/asahi/g17/mod.rs index ae78efaad1a95..10891ce09babf 100644 --- a/drivers/gpu/drm/asahi/g17/mod.rs +++ b/drivers/gpu/drm/asahi/g17/mod.rs @@ -1101,24 +1101,26 @@ impl Gpu { fw::variant::Firmware::Identified(builds) => builds, }; let version = Self::os_firmware_version(dev); - if let Some(build) = builds.iter().find(|build| Some(build.os_version) == version) { - dev_info!(dev.as_ref(), "GPU firmware: {}\n", build.name); - Self::identify_image(dev, build).map_err(|_| ENODEV)?; - return Ok(build.variant); + // iBoot strings can be shared by OS releases. Prefer the reported release's + // candidate, but admit only an exact loaded image, regardless of that hint. + let preferred = builds.iter().filter(|build| Some(build.os_version) == version); + let others = builds.iter().filter(|build| Some(build.os_version) != version); + for build in Iterator::chain(preferred, others) { + if Self::identify_image(dev, build).is_ok() { + dev_info!(dev.as_ref(), "GPU firmware: {}\n", build.name); + return Ok(build.variant); + } } dev_err!( dev.as_ref(), - "Unsupported GPU firmware: macOS version {:?} (apple,firmware-version)\n", + "Unsupported GPU firmware image (reported macOS version {:?})\n", version ); - for build in builds.iter() { - dev_err!(dev.as_ref(), "Supported GPU firmware: {}\n", build.name); - } Err(ENODEV) } /// Checks that the loaded TEXT (the `fw-text` region) is one of `build`'s images, with the DATA size - /// of the `fw-data` region. The version property only says which build to expect. + /// of the `fw-data` region. The version property is only a selection hint. fn identify_image(dev: &AsahiDrmDevice, build: &'static fw::variant::Build) -> Result { let node = dev.as_ref().as_ref().of_node().ok_or(EINVAL)?; let text = node.reserved_mem_region_to_resource_byname(c_str!("fw-text"))?; From a8fb1380b41456833b30014e6e354fba31551cd3 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 11:48:37 +1100 Subject: [PATCH 144/388] drm/asahi: introduce the complete T6040 calibration reader Validate version 2 of the per-chip handoff, sixteen raw OPPs and selector slots, eleven populated selectors, zero tails and base state 1. There is no single-unit fallback. The initdata patch wires the reader into probe and firmware construction so this patch adds no unused compiled items. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/asahi/g17/calibration.rs | 231 +++++++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100644 drivers/gpu/drm/asahi/g17/calibration.rs diff --git a/drivers/gpu/drm/asahi/g17/calibration.rs b/drivers/gpu/drm/asahi/g17/calibration.rs new file mode 100644 index 0000000000000..d973349d35d40 --- /dev/null +++ b/drivers/gpu/drm/asahi/g17/calibration.rs @@ -0,0 +1,231 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT + +//! Per-chip T6040 power data supplied by the boot loader. + +use super::fw::initdata::{Opp, PerfConfig, PERF_STATES}; +use crate::driver::AsahiDrmDevice; +use kernel::{c_str, of, prelude::*, str::CStr}; + +/// Validated calibration of the running chip. There is no built-in fallback. +pub(super) struct Calibration { + pub(super) perf: PerfConfig, + pub(super) leakage: [u32; 56], + pub(super) max_power_mw: u32, + core_mask: u32, + base: usize, +} + +fn words(node: &of::Node, name: &CStr) -> Result<[u32; N]> { + let values: KVec = node.get_property(name)?; + values.as_slice().try_into().map_err(|_| EINVAL) +} + +// Raw ADT OPP slots and SGX register slots number 16. The supported firmware consumes +// the 11 populated selectors; the remaining handoff entries must all be zero. +const RAW_STATES: usize = 16; + +fn active(values: [u32; RAW_STATES]) -> Result<[u32; PERF_STATES]> { + if values[PERF_STATES..].iter().any(|&v| v != 0) { + return Err(EINVAL); + } + values[..PERF_STATES].try_into().map_err(|_| EINVAL) +} + +fn map(values: [u32; RAW_STATES]) -> Result<[u8; PERF_STATES]> { + let values = active(values)?; + if values[0] != 0 + || values[PERF_STATES - 1] != RAW_STATES as u32 - 1 + || values.iter().any(|&v| v >= RAW_STATES as u32) + || values.windows(2).any(|p| p[0] >= p[1]) + { + return Err(EINVAL); + } + Ok(values.map(|v| v as u8)) +} + +fn relative(values: &[u32; PERF_STATES]) -> bool { + values[0] == 0 + && values[PERF_STATES - 1] == 100 + && values.windows(2).all(|p| p[0] <= p[1] && p[1] <= 100) +} + +impl Calibration { + pub(super) fn read(dev: &AsahiDrmDevice) -> Result { + let node = dev.as_ref().as_ref().of_node().ok_or(EIO)?; + if node.get_property::(c_str!("apple,t6040-handoff-version"))? != 2 { + return Err(EINVAL); + } + let core_mask = node.get_property(c_str!("apple,core-mask"))?; + let max_power_mw = node.get_property(c_str!("apple,max-power-mw"))?; + let base = node.get_property::(c_str!("apple,perf-base-pstate"))? as usize; + if core_mask == 0 + || core_mask & !0xfffff != 0 + || !(1..=200_000).contains(&max_power_mw) + || base != 1 + { + return Err(EINVAL); + } + let perf = PerfConfig { + states_a: map(words(&node, c_str!("apple,perf-map-a"))?)?, + states_b: map(words(&node, c_str!("apple,perf-map-b"))?)?, + relative_a: active(words(&node, c_str!("apple,perf-relative-a"))?)?, + relative_b: active(words(&node, c_str!("apple,perf-relative-b"))?)?, + sram_min_mv: 810, + }; + if !relative(&perf.relative_a) + || !relative(&perf.relative_b) + || perf.relative_b[..=base].iter().any(|&v| v != 0) + { + return Err(EINVAL); + } + let core = words::<2>(&node, c_str!("apple,core-leak-coef"))?; + let sram = words::<2>(&node, c_str!("apple,sram-leak-coef"))?; + let cs = words::<1>(&node, c_str!("apple,cs-leak-coef"))?; + let afr = words::<1>(&node, c_str!("apple,afr-leak-coef"))?; + // Positive finite binary32 values. Integer comparisons preserve their ordering. + if Iterator::chain( + Iterator::chain(Iterator::chain(core.iter(), &sram), &cs), + &afr, + ) + .any(|&v| !(0x3f80_0000..0x4780_0000).contains(&v)) + { + return Err(EINVAL); + } + let mut leakage = [0; 56]; + leakage[..2].copy_from_slice(&core); + leakage[32..34].copy_from_slice(&sram); + leakage[52] = cs[0]; + leakage[54] = afr[0]; + Ok(Self { + perf, + leakage, + max_power_mw, + core_mask, + base, + }) + } + + /// Check the loader's mask against the GPU before starting its firmware. + pub(super) fn check_cores(&self, masks: &[u32]) -> Result { + if masks != [self.core_mask & 0x3ff, self.core_mask >> 10] { + return Err(EINVAL); + } + Ok(()) + } + + /// The performance maps index OPPs in ADT order, rather than sorted frequency order. + pub(super) fn check_opps(&self, opps: &[Opp]) -> Result { + if opps.len() != 16 || opps[0].freq_hz != 0 || opps.iter().any(|opp| opp.cells != 2) { + return Err(EINVAL); + } + for map in [&self.perf.states_a, &self.perf.states_b] { + let mut previous = 0; + for &index in &map[1..] { + let opp = &opps[usize::from(index)]; + let mhz = opp.freq_hz / 1_000_000; + if mhz <= previous + || opp.volt_uv[..2] + .iter() + .any(|v| !(400_000..=1_300_000).contains(v)) + { + return Err(EINVAL); + } + previous = mhz; + } + } + let base = opps[usize::from(self.perf.states_a[self.base])].freq_hz / 1_000_000; + let top = opps[usize::from(self.perf.states_a[PERF_STATES - 1])].freq_hz / 1_000_000; + for (i, &index) in self.perf.states_a.iter().enumerate() { + let freq = opps[usize::from(index)].freq_hz / 1_000_000; + let expected = if i < self.base { + 0 + } else { + 100 * (freq - base) / (top - base) + }; + if u64::from(self.perf.relative_b[i]) != expected { + return Err(EINVAL); + } + } + Ok(()) + } +} + +#[kunit_tests(asahi_t6040_calibration)] +mod tests { + use super::*; + + fn chip() -> Calibration { + Calibration { + perf: PerfConfig { + states_a: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + states_b: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + relative_a: [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100], + relative_b: [0, 0, 11, 22, 33, 44, 55, 66, 77, 88, 100], + sram_min_mv: 810, + }, + leakage: [0; 56], + max_power_mw: 70_000, + core_mask: 0xfffff, + base: 1, + } + } + + #[test] + fn operating_points_and_mask_must_match_the_handoff() { + let chip = chip(); + let mut opps = [Opp { + freq_hz: 0, + volt_uv: [800_000; super::super::fw::initdata::OPP_CELLS], + cells: 2, + }; 16]; + for (i, opp) in opps.iter_mut().enumerate() { + opp.freq_hz = i as u64 * 100_000_000; + } + assert!(chip.check_opps(&opps).is_ok()); + opps[0].freq_hz = 100_000_000; + assert!(chip.check_opps(&opps).is_err()); + opps[0].freq_hz = 0; + assert!(chip.check_cores(&[0x3ff, 0x3ff]).is_ok()); + assert!(chip.check_cores(&[0x3ff, 0x1ff]).is_err()); + assert!(chip.check_cores(&[0xfffff]).is_err()); + opps[5].volt_uv[1] = 0; + assert!(chip.check_opps(&opps).is_err()); + opps[5].volt_uv[1] = 800_000; + opps[5].freq_hz = opps[4].freq_hz; + assert!(chip.check_opps(&opps).is_err()); + opps[5].freq_hz = 501_000_000; + assert!(chip.check_opps(&opps).is_ok()); + opps[5].freq_hz = 511_000_000; + assert!(chip.check_opps(&opps).is_err()); + } + + #[test] + fn maps_refuse_off_repeats_and_out_of_range() { + let valid = [0, 1, 2, 3, 4, 6, 8, 10, 12, 14, 15, 0, 0, 0, 0, 0]; + assert!(map(valid).is_ok()); + for (index, value) in [ + (0, 1), + (1, 0), + (5, 4), + (10, 16), + (10, u32::MAX), + (10, 14), + (11, 1), + (15, 15), + ] { + let mut bad = valid; + bad[index] = value; + assert!(map(bad).is_err()); + } + } + + #[test] + fn relative_tables_are_bounded_and_monotonic() { + let mut values = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]; + assert!(relative(&values)); + values[5] = 101; + assert!(!relative(&values)); + values[5] = 39; + assert!(!relative(&values)); + } +} From 53877eaebd4b9d4d41ef9e1ce56c00d038192cff Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 11:48:39 +1100 Subject: [PATCH 145/388] drm/asahi: build T6040 initdata from validated per-chip calibration Wire the reader into probe and firmware construction, validating OPPs and enabled cores before firmware starts. Use the maps, leakage and power ceiling in the 25G83 objects, replacing bin-specific output hashes with layout and host encoder checks. Keep the Neo path unchanged. The old ceiling declaration remains only for the hardware-default reference removed by the next patch. Firmware uses the calibration. Retain GPL-2.0-only and the Gravity Linux encoder attribution to Cody Ho and Niklas Sheth. The original T6040 work is by Jonathan Goyvaerts. Originally-by: Jonathan Goyvaerts Link: https://github.com/aurora-silicon/linux/pull/428 Link: https://github.com/GravityLinux/linux Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/asahi/float.rs | 5 + .../gpu/drm/asahi/g17/fw/initdata/v25g83.rs | 111 ++++-------------- drivers/gpu/drm/asahi/g17/initdata.rs | 6 +- drivers/gpu/drm/asahi/g17/initdata/v25g83.rs | 44 +++---- drivers/gpu/drm/asahi/g17/mod.rs | 12 ++ 5 files changed, 66 insertions(+), 112 deletions(-) diff --git a/drivers/gpu/drm/asahi/float.rs b/drivers/gpu/drm/asahi/float.rs index 46c05699d7722..224e92c6d97e2 100644 --- a/drivers/gpu/drm/asahi/float.rs +++ b/drivers/gpu/drm/asahi/float.rs @@ -44,6 +44,11 @@ struct F32U { } impl F32 { + /// Return the IEEE-754 binary32 representation. + pub(crate) const fn to_bits(self) -> u32 { + self.0 + } + /// Convert a raw 32-bit representation into an F32 pub(crate) const fn from_bits(u: u32) -> F32 { F32(u) diff --git a/drivers/gpu/drm/asahi/g17/fw/initdata/v25g83.rs b/drivers/gpu/drm/asahi/g17/fw/initdata/v25g83.rs index c237896a49fb1..db554b0d9b8fc 100644 --- a/drivers/gpu/drm/asahi/g17/fw/initdata/v25g83.rs +++ b/drivers/gpu/drm/asahi/g17/fw/initdata/v25g83.rs @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-2.0-only OR MIT +// SPDX-License-Identifier: GPL-2.0-only //! Initialization data of the 25G83 firmware build (macOS 26.6, `mBoot-18000.161.10`). //! @@ -17,9 +17,9 @@ //! firmware on the M4 Pro, by evaluating them for this SoC; see the commit that added them. use super::{ - fill_words, perf_states, CompletionRingDescriptor, HwDataArgs, Opp, PerfGroup, RegisterMapEntry, - Words, COMPLETION_RING_ENTRIES, COMPLETION_RING_ENTRY_SIZE, COMPLETION_RING_INDEX, PERF_STATES, - REGISTER_MAP_SLOTS, + fill_words, perf_states, CompletionRingDescriptor, HwDataArgs, Opp, PerfGroup, + RegisterMapEntry, Words, COMPLETION_RING_ENTRIES, COMPLETION_RING_ENTRY_SIZE, + COMPLETION_RING_INDEX, PERF_STATES, REGISTER_MAP_SLOTS, }; use kernel::prelude::*; @@ -379,28 +379,15 @@ impl HwData25G83 { } } +/// Transitional hardware default, removed with the inert T6040 defaults. +pub(crate) const MAX_POWER_MW: u32 = 69_116; + /// Power configuration control period, in 24 MHz clocks (8 ms). const PERIOD_CLOCKS: u32 = 200_000; -/// Highest GPU power, in mW. -pub(crate) const MAX_POWER_MW: u32 = 69_116; -/// [`MAX_POWER_MW`] as an IEEE-754 single. -const MAX_POWER_MW_F32: u32 = 0x4786_fe00; /// Temperature sensors read by the die and MTR temperature limiters. const TEMP_SENSOR_MASK: u32 = 0x2a22_2aa2; -/// Leakage coefficients, IEEE-754 singles: three arrays of sixteen clusters, then four arrays -/// of two dies. Not found in the ADT; constant for the SoC. -const LEAKAGE: [u32; 56] = { - let mut words = [0; 56]; - words[0] = 0x44f0_3000; // 1921.5 - words[1] = 0x44f3_0000; // 1944.0 - words[32] = 0x4373_0000; // 243.0 - words[33] = 0x4373_0000; // 243.0 - words[52] = 0x437b_0000; // 251.0 - words[54] = 0x43bd_c000; // 379.5 - words -}; -/// Offset of [`LEAKAGE`] in the power configuration. +/// Offset of the per-chip leakage coefficients in the power configuration. const LEAKAGE_OFFSET: usize = 0x642c; /// Offset in the power configuration of the GPU address of the configuration's status (the /// configuration named by the root, `+0x80`). @@ -433,12 +420,10 @@ const POWER_CONFIG_WORDS: Words = &[ (0x9c4, 0x3f7f2e9f, 1), (0x9cc, 0x3b516154, 1), (0x9d4, 0x3ca59586, 1), - (0x9e0, MAX_POWER_MW_F32, 1), (0x9e4, 0x40a90fdb, 1), (0x9ec, 0xbc6d0cfe, 1), (0x9f0, 0x64, 1), (0x9f4, 0x3e8, 2), - (0xa04, MAX_POWER_MW, 1), (0xa38, 0x3e8, 1), (0xa70, 0x8, 1), (0xa7c, 0x3f000000, 1), @@ -448,7 +433,6 @@ const POWER_CONFIG_WORDS: Words = &[ (0xa9c, 0x3ecf2e49, 1), (0xaa8, 0x64, 1), (0xaac, 0x3e8, 1), - (0xabc, MAX_POWER_MW, 1), (0xac8, 0x10, 1), (0xad0, 0x5dc00, 1), (0xb28, 0x50, 1), @@ -508,7 +492,6 @@ const POWER_CONFIG_WORDS: Words = &[ (0x27b8, 0xc80000, 1), (0x2870, 0x3e8, 1), (0x28c0, 0x1, 1), - (0x28c8, MAX_POWER_MW, 3), (0x28e0, 0x3f800000, 1), (0x28e4, 0x4, 1), (0x28e8, 0x8, 1), @@ -521,8 +504,6 @@ const POWER_CONFIG_WORDS: Words = &[ (0x29e8, 0x3f4f2e49, 1), (0x29f4, 0x28, 1), (0x29f8, 0x3e8, 2), - (0x2a04, MAX_POWER_MW_F32, 1), - (0x2a08, MAX_POWER_MW, 1), (0x2a14, 0xf8, 1), (0x2a1c, 0x5ad200, 1), (0x2a3c, 0x3e8, 1), @@ -591,27 +572,34 @@ unsafe impl Zeroable for PowerConfig25G83 {} static_assert!(LEAKAGE_OFFSET + size_of::<[u32; 56]>() <= size_of::()); impl PowerConfig25G83 { - /// Pointer and random words, which [`Self::hash`] leaves out. - const RUNTIME: [(usize, usize); 3] = - [(CONFIG_STATUS_POINTER, 8), (AON_POINTER, 8), (ENTROPY, 4)]; - /// Initializes the power configuration. - pub(crate) fn init(&mut self, config_va: u64, aon_va: u64, entropy: u32) { + pub(crate) fn init( + &mut self, + config_va: u64, + aon_va: u64, + entropy: u32, + max_power_mw: u32, + leakage: &[u32; 56], + ) { + self.0.fill(0); fill_words(&mut self.0, POWER_CONFIG_WORDS); - for (index, word) in LEAKAGE.iter().enumerate() { + for (index, word) in leakage.iter().enumerate() { let at = LEAKAGE_OFFSET + 4 * index; self.0[at..at + 4].copy_from_slice(&word.to_le_bytes()); } + // All copies of the power ceiling must describe the same chip. + for offset in [0xa04, 0xabc, 0x28c8, 0x28cc, 0x28d0, 0x2a08] { + self.0[offset..offset + 4].copy_from_slice(&max_power_mw.to_le_bytes()); + } + let power = crate::float::F32::from(max_power_mw).to_bits(); + for offset in [0x9e0, 0x2a04] { + self.0[offset..offset + 4].copy_from_slice(&power.to_le_bytes()); + } self.0[CONFIG_STATUS_POINTER..CONFIG_STATUS_POINTER + 8] .copy_from_slice(&(config_va + 0x80).to_le_bytes()); self.0[AON_POINTER..AON_POINTER + 8].copy_from_slice(&aon_va.to_le_bytes()); self.0[ENTROPY..ENTROPY + 4].copy_from_slice(&entropy.to_le_bytes()); } - - /// Hash of the contents without the pointers and the random word. - pub(crate) fn hash(&self) -> u64 { - hash_without(&self.0, &Self::RUNTIME) - } } /// The AON temperature-source rows the power configuration names: the GPU cluster 0, GPU @@ -632,50 +620,3 @@ pub(crate) const AON_TABLE: [u32; 9] = [ /// (`+0xc8`, 101.00 °C) and release (`+0xcc`, 96.00 °C) temperatures in 0.01 °C; left at zero /// the limit is 0 °C and the GPU stays at its lowest state whenever the PMP runs. pub(crate) const POWER_STATE_WORDS: Words = &[(0xc8, 10_100, 1), (0xcc, 9_600, 1)]; - -impl HwData25G83 { - /// What [`Self::hash`] leaves out: the runtime words (completion descriptors, QoS pointer) - /// and what comes from the device: the performance tables built from its operating points, - /// which are binned per device, and its enabled core count. - const UNHASHED: [(usize, usize); 10] = [ - (COMPLETION_DESCRIPTORS, 0x80), - (QOS_POINTER, 8), - (core::mem::offset_of!(Self, unk_f88), size_of::()), - (core::mem::offset_of!(Self, unk_fc0), size_of::()), - (core::mem::offset_of!(Self, perf_a), size_of::()), - (core::mem::offset_of!(Self, perf_b_freq_mhz), size_of::<[u32; PERF_STATES]>()), - (core::mem::offset_of!(Self, cs_perf), size_of::()), - (core::mem::offset_of!(Self, afr_perf), size_of::()), - (core::mem::offset_of!(Self, perf_b), size_of::()), - (core::mem::offset_of!(Self, unk_2570), size_of::()), - ]; - - /// Hash of the constant contents (everything but [`Self::UNHASHED`]). - pub(crate) fn hash(&self) -> u64 { - // SAFETY: `HwData25G83` consists of integers only and has no padding. - let bytes = unsafe { - core::slice::from_raw_parts((self as *const Self).cast::(), size_of::()) - }; - hash_without(bytes, &Self::UNHASHED) - } -} - -/// FNV-1a 64 of `bytes` with the `(offset, length)` ranges in `skip` left out. -pub(crate) fn hash_without(bytes: &[u8], skip: &[(usize, usize)]) -> u64 { - let mut hash = 0xcbf2_9ce4_8422_2325u64; - for (offset, byte) in bytes.iter().enumerate() { - if skip.iter().any(|&(at, len)| (at..at + len).contains(&offset)) { - continue; - } - hash = (hash ^ u64::from(*byte)).wrapping_mul(0x100_0000_01b3); - } - hash -} - -/// Hashes ([`hash_without`]) of the hardware data, power configuration and region C that the -/// Gravity Linux G16 encoders produce for the J616s, which this build's objects must equal. -pub(crate) mod expected { - pub(crate) const HW_DATA: u64 = 0xfd3b_a26f_570f_57cd; - pub(crate) const POWER_CONFIG: u64 = 0xb669_5b84_2ad2_36fe; - pub(crate) const REGION_C: u64 = 0xd2db_fa6b_8976_47b4; -} diff --git a/drivers/gpu/drm/asahi/g17/initdata.rs b/drivers/gpu/drm/asahi/g17/initdata.rs index 10af261aeb3ac..83a7bbc29dd05 100644 --- a/drivers/gpu/drm/asahi/g17/initdata.rs +++ b/drivers/gpu/drm/asahi/g17/initdata.rs @@ -1223,7 +1223,11 @@ impl InitData { /// The GPU's operating points: `count` of them, each with one voltage cell for the whole /// GPU or one per cluster. - fn opps(dev: &AsahiDrmDevice, count: usize, clusters: u32) -> Result> { + pub(super) fn opps( + dev: &AsahiDrmDevice, + count: usize, + clusters: u32, + ) -> Result> { let opps = Self::opp_table(dev, c_str!("operating-points-v2"))?; if opps.len() != count { return Err(EINVAL); diff --git a/drivers/gpu/drm/asahi/g17/initdata/v25g83.rs b/drivers/gpu/drm/asahi/g17/initdata/v25g83.rs index 85aa6468fa311..670f4b96dcb76 100644 --- a/drivers/gpu/drm/asahi/g17/initdata/v25g83.rs +++ b/drivers/gpu/drm/asahi/g17/initdata/v25g83.rs @@ -8,7 +8,7 @@ //! ([`abi::v25g83`]); free lists, pools, descriptor tables, rings and regions are the A18 Pro's. use super::{abi, low, BuildEnv, BundleObjects, CpuMap, DescriptorTables, InitData, KernelObject}; -use crate::g17::{Config, Role}; +use crate::g17::{calibration::Calibration, Config, Role}; use crate::mmu; use abi::v25g83 as v; use core::sync::atomic::{fence, Ordering}; @@ -74,7 +74,7 @@ impl InitData { }) } - /// Writes the 25G83 firmware objects, checks them against the expected contents, then + /// Writes the 25G83 firmware objects using the chip calibration, then /// stages the opening control-ring state. fn initialize_25g83( &mut self, @@ -101,12 +101,14 @@ impl InitData { let opps = Self::opps(env.dev, cfg.opps, env.clusters)?; let cs_opps = Self::opp_table(env.dev, c_str!("apple,cs-opp"))?; let afr_opps = Self::opp_table(env.dev, c_str!("apple,afr-opp"))?; + let calibration = Calibration::read(env.dev)?; + calibration.check_opps(&opps)?; let hw_args = abi::HwDataArgs { chip_id: cfg.chip_id, windows: cfg.windows, reserved_slots: cfg.reserved_slots, opps: &opps, - perf: cfg.perf, + perf: &calibration.perf, completion_rings: bundle.completion_rings, qos_va: self.object(bundle.qos)?.gpu_va(), }; @@ -117,41 +119,28 @@ impl InitData { }; // SAFETY: Plain C entropy source. let entropy = unsafe { kernel::bindings::get_random_u32() }; - let mut hashes = [0; 3]; self.object_mut(self.bundle)? .initialize::(0, |hw| { hw.init(&hw_args, &hw_args_25g83)?; - hashes[0] = hw.hash(); Ok(()) })?; self.object_mut(self.bundle)? .initialize::(v::POWER_CONFIG, |power| { - power.init(config_va, aon_va, entropy); - hashes[1] = power.hash(); + power.init( + config_va, + aon_va, + entropy, + calibration.max_power_mw, + &calibration.leakage, + ); Ok(()) })?; self.object_mut(self.region_c)? .initialize::(0, |region| { region.init(cfg.region_c); + region.max_power_mw = calibration.max_power_mw; Ok(()) })?; - hashes[2] = v::hash_without(self.object_mut(self.region_c)?.bytes_mut(), &[]); - // The constant contents must equal what the Gravity Linux encoders produce on the proven - // setup; the operating points and the core count come from the device and are not hashed. - let expected = [ - v::expected::HW_DATA, - v::expected::POWER_CONFIG, - v::expected::REGION_C, - ]; - if hashes != expected { - dev_err!( - env.dev.as_ref(), - "25G83 initialization data differs: {:#x?}, expected {:#x?}\n", - hashes, - expected - ); - return Err(EINVAL); - } for (index, word) in v::AON_TABLE.iter().enumerate() { self.object_mut(objects.aon)?.write(index * 4, *word)?; } @@ -167,7 +156,9 @@ impl InitData { v::private::STATUS_B + core::mem::offset_of!(abi::PrimaryStatusB, fwctl_state_va); let cluster = self.object(self.cluster)?; cluster.dword(fwctl)?.store(fwctl_va, Ordering::Relaxed); - cluster.dword(fwctl + 8)?.store(fwctl_va + abi::FWCTL_RING, Ordering::Relaxed); + cluster + .dword(fwctl + 8)? + .store(fwctl_va + abi::FWCTL_RING, Ordering::Relaxed); // One main configuration carrying both roles. let main_offset = abi::bundle::MAIN_CONFIG[Role::Primary as usize]; @@ -218,7 +209,8 @@ impl InitData { )?; for &(offset, value, repeat) in v::POWER_STATE_WORDS { for index in 0..repeat { - self.object_mut(objects.power_state)?.write(offset + 4 * index, value)?; + self.object_mut(objects.power_state)? + .write(offset + 4 * index, value)?; } } diff --git a/drivers/gpu/drm/asahi/g17/mod.rs b/drivers/gpu/drm/asahi/g17/mod.rs index 10891ce09babf..eab0ca15f21f8 100644 --- a/drivers/gpu/drm/asahi/g17/mod.rs +++ b/drivers/gpu/drm/asahi/g17/mod.rs @@ -18,6 +18,7 @@ //! -> VM exec lock -> GEM reservation. `RoleState::ack` nests inside nothing. mod buffer; +mod calibration; mod channel; mod command; pub(crate) use command::Attachments; @@ -1002,8 +1003,19 @@ impl Gpu { cfg: &'static Config, regs: regs::G17Resources, ) -> Result> { + let calibration = if cfg.chip_id == crate::hw::t6040::CHIP_ID { + Some(calibration::Calibration::read(dev).inspect_err(|_| { + dev_err!(dev.as_ref(), "Missing or invalid T6040 GPU calibration handoff\n"); + })?) + } else { + None + }; let variant = Self::firmware_variant(dev, &cfg.firmware)?; let id = regs.get_g17_id()?; + if let Some(calibration) = calibration { + calibration.check_cores(&id.core_masks)?; + calibration.check_opps(&initdata::InitData::opps(dev, cfg.opps, id.num_clusters)?)?; + } if id.family != cfg.identity.family || id.variant != cfg.identity.variant || id.num_dies != cfg.identity.num_dies From 415fd2e6ee2d5175dd4bf4148c391f7daf575328 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 11:48:40 +1100 Subject: [PATCH 146/388] drm/asahi: remove single-unit T6040 power defaults Keep the structural hardware description while requiring the loader maps and power ceiling. Remove the last single-unit ceiling reference and its declaration together, keeping this commit buildable independently. Assisted-by: LLM Signed-off-by: Ryan Murray --- .../gpu/drm/asahi/g17/fw/initdata/v25g83.rs | 3 --- drivers/gpu/drm/asahi/hw/t6040.rs | 24 ++++++------------- 2 files changed, 7 insertions(+), 20 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/fw/initdata/v25g83.rs b/drivers/gpu/drm/asahi/g17/fw/initdata/v25g83.rs index db554b0d9b8fc..f4c3ee5db22e2 100644 --- a/drivers/gpu/drm/asahi/g17/fw/initdata/v25g83.rs +++ b/drivers/gpu/drm/asahi/g17/fw/initdata/v25g83.rs @@ -379,9 +379,6 @@ impl HwData25G83 { } } -/// Transitional hardware default, removed with the inert T6040 defaults. -pub(crate) const MAX_POWER_MW: u32 = 69_116; - /// Power configuration control period, in 24 MHz clocks (8 ms). const PERIOD_CLOCKS: u32 = 200_000; /// Temperature sensors read by the die and MTR temperature limiters. diff --git a/drivers/gpu/drm/asahi/hw/t6040.rs b/drivers/gpu/drm/asahi/hw/t6040.rs index 0007fdf132949..fa3e51efacadc 100644 --- a/drivers/gpu/drm/asahi/hw/t6040.rs +++ b/drivers/gpu/drm/asahi/hw/t6040.rs @@ -7,7 +7,7 @@ use crate::g17::fw::{ self, - initdata::{v25g83, PerfConfig, RegionCConfig, RegisterWindow, PERF_STATES}, + initdata::{PerfConfig, RegionCConfig, RegisterWindow}, }; use crate::g17::{Config, CoreLayout, CrashBuffers, FixedBuffer, Identity}; use crate::hw::{GpuGen, GpuVariant}; @@ -169,33 +169,23 @@ pub(crate) const REGISTER_WINDOWS: [RegisterWindow; 25] = [ window(43, 0x2_88e6_0000, 0x47_4000, 0x58, 0xe6_0000, 2), ]; -/// Performance-table configuration. The device tree lists the sixteen operating points in -/// performance-state order, with one voltage per cluster. +/// Inert defaults. The 25G83 builder requires the loader's per-chip performance maps. pub(crate) const PERF: PerfConfig = PerfConfig { - states_a: [0, 1, 2, 3, 4, 6, 8, 10, 12, 14, 15], - states_b: [0, 1, 2, 3, 4, 6, 7, 9, 11, 13, 15], - relative_a: [0, 9, 16, 23, 29, 38, 49, 60, 69, 79, 100], - relative_b: [0, 0, 22, 36, 47, 57, 68, 78, 84, 91, 100], + states_a: [0; 11], + states_b: [0; 11], + relative_a: [0; 11], + relative_b: [0; 11], sram_min_mv: 810, }; /// Operating points the performance tables reference. pub(crate) const OPPS: usize = 16; -const _: () = { - let mut index = 0; - while index < PERF_STATES { - assert!((PERF.states_a[index] as usize) < OPPS); - assert!((PERF.states_b[index] as usize) < OPPS); - index += 1; - } -}; - /// Region C. const REGION_C: RegionCConfig = RegionCConfig { unk_54: 0x0fa0, unk_78: [1, 0, 0], - max_power_mw: v25g83::MAX_POWER_MW, + max_power_mw: 0, unk_9c: 0, unk_a0: 0, unk_7c4: 1500, From cf9ac97063729e81ea4fa82a64f63206cd2de804 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 11:48:41 +1100 Subject: [PATCH 147/388] arm64: dts: apple: enable the calibrated GPU on J616s Describe the calibration binding and use loader-filled regions and OPPs. Enable the mailbox and keep the firmware domains powered while the image lives. The GPU consumer owns the PMP report vote. Booting requires the matching T6040 m1n1 handoff. Assisted-by: LLM Signed-off-by: Ryan Murray --- .../devicetree/bindings/gpu/apple,agx.yaml | 107 +++++++++- arch/arm64/boot/dts/apple/t6040-j616s.dts | 185 ++++++++++-------- arch/arm64/boot/dts/apple/t6040.dtsi | 6 +- 3 files changed, 208 insertions(+), 90 deletions(-) diff --git a/Documentation/devicetree/bindings/gpu/apple,agx.yaml b/Documentation/devicetree/bindings/gpu/apple,agx.yaml index c0a32c3badd07..0cd4dc1d6fd78 100644 --- a/Documentation/devicetree/bindings/gpu/apple,agx.yaml +++ b/Documentation/devicetree/bindings/gpu/apple,agx.yaml @@ -83,9 +83,9 @@ properties: minItems: 3 maxItems: 3 description: - macOS version (major, minor, patch) whose boot firmware loaded the GPU - firmware, which identifies the firmware build. Filled in by the boot - loader. + Reported macOS version (major, minor, patch) whose boot firmware loaded + the GPU firmware. Filled in by the boot loader. This is a selection + hint; the T6040 driver requires the exact loaded image identity. apple,cs-opp: $ref: /schemas/types.yaml#/definitions/phandle @@ -99,6 +99,75 @@ properties: Operating-point table of the GPU's AFR clock domain, one voltage cell per state. + apple,t6040-handoff-version: + $ref: /schemas/types.yaml#/definitions/uint32 + const: 2 + description: + Written last by the boot loader after it has filled the T6040 operating + points, memory regions and per-chip calibration. Absent in the source DT. + + apple,core-mask: + $ref: /schemas/types.yaml#/definitions/uint32 + description: Enabled cores in the two ten-core T6040 clusters, low cluster first + + apple,max-power-mw: + $ref: /schemas/types.yaml#/definitions/uint32 + description: Per-chip maximum GPU power in milliwatts, calculated by the loader + + apple,perf-base-pstate: + $ref: /schemas/types.yaml#/definitions/uint32 + description: + Logical base state of the frequency-relative performance table. T6040 + supports the first active state, also used when its ADT omits the property. + + apple,perf-map-a: + $ref: /schemas/types.yaml#/definitions/uint32-array + description: + All 16 A selector register fields, indexing raw ADT operating points. + Eleven populated entries, including off, followed by five zeros. + + apple,perf-map-b: + $ref: /schemas/types.yaml#/definitions/uint32-array + description: + All 16 B selector register fields, indexing raw ADT operating points. + Eleven populated entries, including off, followed by five zeros. + + apple,perf-relative-a: + $ref: /schemas/types.yaml#/definitions/uint32-array + description: + Per-chip relative power of each populated A selector, in percent, + followed by zeros for the five unused selectors. + + apple,perf-relative-b: + $ref: /schemas/types.yaml#/definitions/uint32-array + description: + Relative frequency of the populated A ladder above the base state, + in percent, followed by zeros for the five unused selectors. + + apple,core-leak-coef: + $ref: /schemas/types.yaml#/definitions/uint32-array + description: + GPU core leakage, one coefficient per cluster. Per-chip efuse values decoded by the boot loader, + stored as IEEE-754 binary32 bit patterns in big-endian cells. + + apple,sram-leak-coef: + $ref: /schemas/types.yaml#/definitions/uint32-array + description: + GPU SRAM leakage, one coefficient per cluster. Per-chip efuse values decoded by the boot loader, + stored as IEEE-754 binary32 bit patterns in big-endian cells. + + apple,cs-leak-coef: + $ref: /schemas/types.yaml#/definitions/uint32-array + description: + CS leakage. Per-chip efuse values decoded by the boot loader, + stored as IEEE-754 binary32 bit patterns in big-endian cells. + + apple,afr-leak-coef: + $ref: /schemas/types.yaml#/definitions/uint32-array + description: + AFR leakage. Per-chip efuse values decoded by the boot loader, + stored as IEEE-754 binary32 bit patterns in big-endian cells. + apple,firmware-abi: $ref: /schemas/types.yaml#/definitions/uint32-array minItems: 3 @@ -190,6 +259,38 @@ allOf: const: apple,agx-t6040 then: properties: + apple,core-mask: + minimum: 1 + maximum: 1048575 + apple,max-power-mw: + minimum: 1 + maximum: 200000 + apple,perf-base-pstate: + const: 1 + apple,perf-map-a: + minItems: 16 + maxItems: 16 + apple,perf-map-b: + minItems: 16 + maxItems: 16 + apple,perf-relative-a: + minItems: 16 + maxItems: 16 + apple,perf-relative-b: + minItems: 16 + maxItems: 16 + apple,core-leak-coef: + minItems: 2 + maxItems: 2 + apple,sram-leak-coef: + minItems: 2 + maxItems: 2 + apple,cs-leak-coef: + minItems: 1 + maxItems: 1 + apple,afr-leak-coef: + minItems: 1 + maxItems: 1 reg: items: - description: GFX coprocessor control registers diff --git a/arch/arm64/boot/dts/apple/t6040-j616s.dts b/arch/arm64/boot/dts/apple/t6040-j616s.dts index 65ee34b8b362f..5707ec4a632d2 100644 --- a/arch/arm64/boot/dts/apple/t6040-j616s.dts +++ b/arch/arm64/boot/dts/apple/t6040-j616s.dts @@ -79,8 +79,9 @@ * Text segment of the loaded GPU firmware (ADT gfx-asc * segment-ranges[0]); the driver identifies the build from it. */ - gpu_fw_text: gpu-fw-text@10000be4000 { - reg = <0x100 0x00be4000 0x0 0x60000>; + gpu_fw_text: gpu-fw-text { + reg = <0 0 0 0>; + status = "disabled"; no-map; }; @@ -88,8 +89,9 @@ * Data segment of the loaded GPU firmware (ADT sgx gfx-data-*); it * places its crash log here. */ - gpu_fw_data: gpu-fw-data@10001d44000 { - reg = <0x100 0x01d44000 0x0 0x138000>; + gpu_fw_data: gpu-fw-data { + reg = <0 0 0 0>; + status = "disabled"; no-map; }; @@ -98,23 +100,27 @@ * gfx-shared-l2-region-*, gfx-handoff-*). They stay no-map until * the boot loader adds them to /memory. */ - uat_ttbs: uat-ttbs@10bfffb8000 { - reg = <0x10b 0xfffb8000 0x0 0x4000>; + uat_ttbs: uat-ttbs { + reg = <0 0 0 0>; + status = "disabled"; no-map; }; - uat_pagetables: uat-pagetables@10bfff78000 { - reg = <0x10b 0xfff78000 0x0 0x40000>; + uat_pagetables: uat-pagetables { + reg = <0 0 0 0>; + status = "disabled"; no-map; }; - uat_l2_pagetables: uat-l2-pagetables@10bfff74000 { - reg = <0x10b 0xfff74000 0x0 0x4000>; + uat_l2_pagetables: uat-l2-pagetables { + reg = <0 0 0 0>; + status = "disabled"; no-map; }; - uat_handoff: uat-handoff@10bfff70000 { - reg = <0x10b 0xfff70000 0x0 0x4000>; + uat_handoff: uat-handoff { + reg = <0 0 0 0>; + status = "disabled"; no-map; }; }; @@ -139,76 +145,76 @@ /* * GPU performance states, one opp-microvolt cell per cluster (ADT * /arm-io/sgx perf-states and perf-states1). The operating points are - * binned per device: these are this unit's values until the boot - * loader fills them in. Entries keep the order of the GPU performance - * states, which is not sorted by frequency. + * binned per device: the loader fills these placeholders. + * The driver requires a completed calibration handoff. Entries keep the + * order of the GPU performance states, which is not sorted by frequency. */ gpu_opp: opp-table-gpu { compatible = "operating-points-v2"; opp00 { opp-hz = /bits/ 64 <0>; - opp-microvolt = <125000 125000>; + opp-microvolt = <0 0>; }; opp01 { - opp-hz = /bits/ 64 <338000000>; - opp-microvolt = <605000 585000>; + opp-hz = /bits/ 64 <0>; + opp-microvolt = <0 0>; }; opp02 { - opp-hz = /bits/ 64 <618000000>; - opp-microvolt = <655000 640000>; + opp-hz = /bits/ 64 <0>; + opp-microvolt = <0 0>; }; opp03 { - opp-hz = /bits/ 64 <796000000>; - opp-microvolt = <690000 690000>; + opp-hz = /bits/ 64 <0>; + opp-microvolt = <0 0>; }; opp04 { - opp-hz = /bits/ 64 <924000000>; - opp-microvolt = <715000 725000>; + opp-hz = /bits/ 64 <0>; + opp-microvolt = <0 0>; }; opp05 { - opp-hz = /bits/ 64 <952000000>; - opp-microvolt = <755000 770000>; + opp-hz = /bits/ 64 <0>; + opp-microvolt = <0 0>; }; opp06 { - opp-hz = /bits/ 64 <1056000000>; - opp-microvolt = <755000 770000>; + opp-hz = /bits/ 64 <0>; + opp-microvolt = <0 0>; }; opp07 { - opp-hz = /bits/ 64 <1062000000>; - opp-microvolt = <795000 810000>; + opp-hz = /bits/ 64 <0>; + opp-microvolt = <0 0>; }; opp08 { - opp-hz = /bits/ 64 <1182000000>; - opp-microvolt = <795000 810000>; + opp-hz = /bits/ 64 <0>; + opp-microvolt = <0 0>; }; opp09 { - opp-hz = /bits/ 64 <1182000000>; - opp-microvolt = <845000 860000>; + opp-hz = /bits/ 64 <0>; + opp-microvolt = <0 0>; }; opp10 { - opp-hz = /bits/ 64 <1312000000>; - opp-microvolt = <845000 860000>; + opp-hz = /bits/ 64 <0>; + opp-microvolt = <0 0>; }; opp11 { - opp-hz = /bits/ 64 <1242000000>; - opp-microvolt = <900000 915000>; + opp-hz = /bits/ 64 <0>; + opp-microvolt = <0 0>; }; opp12 { - opp-hz = /bits/ 64 <1380000000>; - opp-microvolt = <900000 915000>; + opp-hz = /bits/ 64 <0>; + opp-microvolt = <0 0>; }; opp13 { - opp-hz = /bits/ 64 <1326000000>; - opp-microvolt = <945000 960000>; + opp-hz = /bits/ 64 <0>; + opp-microvolt = <0 0>; }; opp14 { - opp-hz = /bits/ 64 <1470000000>; - opp-microvolt = <945000 960000>; + opp-hz = /bits/ 64 <0>; + opp-microvolt = <0 0>; }; opp15 { - opp-hz = /bits/ 64 <1578000000>; - opp-microvolt = <1030000 1045000>; + opp-hz = /bits/ 64 <0>; + opp-microvolt = <0 0>; }; }; @@ -217,36 +223,36 @@ compatible = "operating-points-v2"; opp00 { - opp-hz = /bits/ 64 <24000000>; - opp-microvolt = <555000>; + opp-hz = /bits/ 64 <0>; + opp-microvolt = <0>; }; opp01 { - opp-hz = /bits/ 64 <338000000>; - opp-microvolt = <555000>; + opp-hz = /bits/ 64 <0>; + opp-microvolt = <0>; }; opp02 { - opp-hz = /bits/ 64 <618000000>; - opp-microvolt = <620000>; + opp-hz = /bits/ 64 <0>; + opp-microvolt = <0>; }; opp03 { - opp-hz = /bits/ 64 <796000000>; - opp-microvolt = <660000>; + opp-hz = /bits/ 64 <0>; + opp-microvolt = <0>; }; opp04 { - opp-hz = /bits/ 64 <992000000>; - opp-microvolt = <715000>; + opp-hz = /bits/ 64 <0>; + opp-microvolt = <0>; }; opp05 { - opp-hz = /bits/ 64 <1188000000>; - opp-microvolt = <780000>; + opp-hz = /bits/ 64 <0>; + opp-microvolt = <0>; }; opp06 { - opp-hz = /bits/ 64 <1380000000>; - opp-microvolt = <870000>; + opp-hz = /bits/ 64 <0>; + opp-microvolt = <0>; }; opp07 { - opp-hz = /bits/ 64 <1470000000>; - opp-microvolt = <910000>; + opp-hz = /bits/ 64 <0>; + opp-microvolt = <0>; }; }; @@ -255,36 +261,36 @@ compatible = "operating-points-v2"; opp00 { - opp-hz = /bits/ 64 <24000000>; - opp-microvolt = <555000>; + opp-hz = /bits/ 64 <0>; + opp-microvolt = <0>; }; opp01 { - opp-hz = /bits/ 64 <338000000>; - opp-microvolt = <555000>; + opp-hz = /bits/ 64 <0>; + opp-microvolt = <0>; }; opp02 { - opp-hz = /bits/ 64 <618000000>; - opp-microvolt = <620000>; + opp-hz = /bits/ 64 <0>; + opp-microvolt = <0>; }; opp03 { - opp-hz = /bits/ 64 <796000000>; - opp-microvolt = <660000>; + opp-hz = /bits/ 64 <0>; + opp-microvolt = <0>; }; opp04 { - opp-hz = /bits/ 64 <992000000>; - opp-microvolt = <715000>; + opp-hz = /bits/ 64 <0>; + opp-microvolt = <0>; }; opp05 { - opp-hz = /bits/ 64 <1188000000>; - opp-microvolt = <780000>; + opp-hz = /bits/ 64 <0>; + opp-microvolt = <0>; }; opp06 { - opp-hz = /bits/ 64 <1380000000>; - opp-microvolt = <870000>; + opp-hz = /bits/ 64 <0>; + opp-microvolt = <0>; }; opp07 { - opp-hz = /bits/ 64 <1470000000>; - opp-microvolt = <910000>; + opp-hz = /bits/ 64 <0>; + opp-microvolt = <0>; }; }; @@ -821,12 +827,23 @@ }; /* - * The GPU stays disabled until a boot loader fills in this unit's operating - * points, leakage and regions from the ADT; the values here are one unit's. - * Enabling it also takes gfx_mbox, keeps ps_afr and ps_gfx always on and - * claims pmp_gfx always-on, so that the PMP sees the GPU on exactly while it - * is powered. + * The loaded GPU firmware keeps running across system sleep. Like the Neo's + * GFX domain, these domains must not be switched off while that image lives. + * The PMP report vote is still owned by the GPU consumer. */ +&ps_afr { + apple,always-on; +}; + +&ps_gfx { + apple,always-on; +}; + +/* The loader fills the operating points, calibration and all six memory regions. */ +&gfx_mbox { + status = "okay"; +}; + &gpu { memory-region = <&uat_ttbs>, <&uat_pagetables>, <&uat_handoff>, <&gpu_fw_data>, <&uat_l2_pagetables>, <&gpu_fw_text>; @@ -835,7 +852,7 @@ operating-points-v2 = <&gpu_opp>; apple,cs-opp = <&gpu_cs_opp>; apple,afr-opp = <&gpu_afr_opp>; - status = "disabled"; + status = "okay"; }; /* diff --git a/arch/arm64/boot/dts/apple/t6040.dtsi b/arch/arm64/boot/dts/apple/t6040.dtsi index 213723f3c34b2..ad87eaf38ac31 100644 --- a/arch/arm64/boot/dts/apple/t6040.dtsi +++ b/arch/arm64/boot/dts/apple/t6040.dtsi @@ -616,7 +616,7 @@ <0x2 0x88000000 0x0 0x3758000>; reg-names = "asc", "sgx"; mboxes = <&gfx_mbox>; - power-domains = <&ps_gfx>; + power-domains = <&pmp_gfx>; status = "disabled"; }; @@ -909,8 +909,8 @@ * the GPU as off and holds it at its lowest state, but claiming * it while nothing powers the GPU crashes the PMP firmware * (SError) right after it starts. It sits on the GPU's own - * power state, and a board that powers the GPU (ps_gfx stays on - * while the GPU firmware runs) claims it always-on. + * power state. The GPU consumer takes this domain, so the request + * follows the GFX domain's power lifetime. */ pmp_gfx: report@d { compatible = "apple,t6041-pmp-v2-report-entry", From b8f8724bff9c7fd29e2d1162e5fae471f111e1f9 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 11:48:44 +1100 Subject: [PATCH 148/388] selftests: asahi-m4: follow shared firmware and MMU interfaces Use the current shared firmware type and extracted MMU helpers. Compare the immutable ABI prefix while independently checking the additive tail size and offsets. Keep the golden fixture bytes unchanged. Assisted-by: LLM Signed-off-by: Ryan Murray --- tools/testing/selftests/drivers/gpu/asahi-m4/README | 5 +++++ tools/testing/selftests/drivers/gpu/asahi-m4/abi.py | 12 ++++++++++++ .../selftests/drivers/gpu/asahi-m4/check-channels.rs | 3 +++ .../drivers/gpu/asahi-m4/run-golden-tests.py | 4 ++++ .../drivers/gpu/asahi-m4/test-mmu-retention.py | 11 +++++++---- .../selftests/drivers/gpu/asahi-m4/test-uapi.py | 2 +- .../drivers/gpu/asahi-m4/test-vm-registry.py | 2 +- 7 files changed, 33 insertions(+), 6 deletions(-) diff --git a/tools/testing/selftests/drivers/gpu/asahi-m4/README b/tools/testing/selftests/drivers/gpu/asahi-m4/README index 9f7859015a19e..c88abd122428e 100644 --- a/tools/testing/selftests/drivers/gpu/asahi-m4/README +++ b/tools/testing/selftests/drivers/gpu/asahi-m4/README @@ -39,6 +39,11 @@ queue/context/notifier records and TTBAT generation words. Comparisons include are compared against the actual kernel header by compiling complete C and Rust definitions and measuring sizes, alignments, member offsets and constants. Comments and documentation can change without invalidating that ABI check. +The current global-parameters reply has a documented additive capacity tail. +The frozen 600-byte prefix is compared unchanged; independent C assertions +require the full reply to be exactly 616 bytes, with two eight-byte fields at +600 and 608. Older Mesa headers can still match the shared prefix. Firmware +encoder golden data and its fixture manifest are never regenerated. No Mesa headers are vendored. Set ASAHI_M4_MESA=/path/to/mesa, or run `test-uapi.py --mesa /path/to/mesa`, for a compatibility check against the real userspace source; a default host run reports that Mesa comparison was omitted. diff --git a/tools/testing/selftests/drivers/gpu/asahi-m4/abi.py b/tools/testing/selftests/drivers/gpu/asahi-m4/abi.py index dd4391a686a14..38c6d7806eabd 100644 --- a/tools/testing/selftests/drivers/gpu/asahi-m4/abi.py +++ b/tools/testing/selftests/drivers/gpu/asahi-m4/abi.py @@ -61,12 +61,16 @@ def run_measurements(command, source, binary): def c_layout(header, bindings, output, tag): keys, _ = expressions(bindings) + capacity_tail = 'client_memory_limit;' in header.read_text() # Linux 7.2 folds the historical extension structs into the base payloads. # Compare the same wire prefixes and offsets while keeping the C oracle frozen. if 'struct drm_asahi_cmd_render_with_prior {' not in header.read_text(): translated = [] for key, expr in keys: kind, _, field = key.partition(':') + if capacity_tail and key == 'drm_asahi_params_global:size': + # Compare the frozen reply prefix; validate the entire known additive tail below. + expr = 'offsetof(struct drm_asahi_params_global, memory_limit)' if key == "drm_asahi_vm_create:pad": expr = "offsetof(struct drm_asahi_vm_create, flags)" base = None @@ -89,6 +93,14 @@ def c_layout(header, bindings, output, tag): source = output / (tag + '.c') lines = ['#include ', '#include ', '#include ' + json.dumps(str(header)), 'int main(void) {'] + if capacity_tail: + lines[3:3] = [ + '_Static_assert(sizeof(struct drm_asahi_params_global) == 616, "capacity reply size");', + '_Static_assert(offsetof(struct drm_asahi_params_global, memory_limit) == 600, "capacity prefix");', + '_Static_assert(offsetof(struct drm_asahi_params_global, client_memory_limit) == 608, "client capacity offset");', + '_Static_assert(sizeof(((struct drm_asahi_params_global *)0)->memory_limit) == 8, "capacity width");', + '_Static_assert(sizeof(((struct drm_asahi_params_global *)0)->client_memory_limit) == 8, "client capacity width");', + ] lines += [f'printf("{key}=%llu\\n", (unsigned long long)({expr}));' for key, expr in keys] source.write_text('\n'.join(lines + ['return 0;', '}']) + '\n') return run_measurements(shlex.split(os.environ.get('CC', 'cc')) + diff --git a/tools/testing/selftests/drivers/gpu/asahi-m4/check-channels.rs b/tools/testing/selftests/drivers/gpu/asahi-m4/check-channels.rs index 52461a122b89d..925fb72a49931 100644 --- a/tools/testing/selftests/drivers/gpu/asahi-m4/check-channels.rs +++ b/tools/testing/selftests/drivers/gpu/asahi-m4/check-channels.rs @@ -4,6 +4,9 @@ mod channels; #[path = "@KERNEL@/drivers/gpu/drm/asahi/g16/fw/graph.rs"] mod graph; +mod fwid { +@FWID_BUILD@ +} mod hw {pub enum GpuVariant {G}} struct Config { chip_id:u32,gpu_variant:hw::GpuVariant,gpu_revision:u32,uat_ias:u32,uat_oas:u32, diff --git a/tools/testing/selftests/drivers/gpu/asahi-m4/run-golden-tests.py b/tools/testing/selftests/drivers/gpu/asahi-m4/run-golden-tests.py index 7d3a06dac82c0..962713941338b 100644 --- a/tools/testing/selftests/drivers/gpu/asahi-m4/run-golden-tests.py +++ b/tools/testing/selftests/drivers/gpu/asahi-m4/run-golden-tests.py @@ -15,10 +15,14 @@ def main(): manifest = unpack_fixtures(golden) (out / 'public-uapi.rs').write_bytes(frozen_bindings().read_bytes()) backend = root / 'drivers/gpu/drm/asahi/g16' + identity = (root / 'drivers/gpu/drm/asahi/fwid.rs').read_text() + first = identity.index('pub(crate) struct FirmwareBuild {') + build_type = identity[first:identity.index('\n}', first) + 2] logs = [] for name in ['initdata', 'compute', 'render', 'channels', 'initdata-bounds']: source = (HERE / f'check-{name}.rs').read_text() source = source.replace('@KERNEL@', str(root)).replace('@OUTPUT@', str(out)) + source = source.replace('@FWID_BUILD@', build_type) test = out / f'check-{name}.rs' test.write_text(source) binary = out / f'check-{name}' diff --git a/tools/testing/selftests/drivers/gpu/asahi-m4/test-mmu-retention.py b/tools/testing/selftests/drivers/gpu/asahi-m4/test-mmu-retention.py index 03184a58211cf..787992e9b4c24 100755 --- a/tools/testing/selftests/drivers/gpu/asahi-m4/test-mmu-retention.py +++ b/tools/testing/selftests/drivers/gpu/asahi-m4/test-mmu-retention.py @@ -19,7 +19,7 @@ def body(source,marker): mm=(ROOT/'drivers/gpu/drm/asahi/mmu.rs').read_text();pt=(ROOT/'drivers/gpu/drm/asahi/pgtable.rs').read_text() pre=r''' #![allow(dead_code,unused_variables)] -use std::{cell::RefCell,rc::Rc,mem::ManuallyDrop,ops::{Deref,DerefMut,Range},num::NonZeroUsize,sync::{Mutex as StdMutex,atomic::{AtomicBool as StdAtomicBool,AtomicU8,AtomicU32,AtomicU64,AtomicUsize,Ordering,fence}}}; +use std::{cell::RefCell,rc::Rc,mem::{ManuallyDrop,size_of},ops::{Deref,DerefMut,Range},num::NonZeroUsize,sync::{Mutex as StdMutex,atomic::{AtomicBool as StdAtomicBool,AtomicU8,AtomicU32,AtomicU64,AtomicUsize,Ordering,fence}}}; type Result = std::result::Result;const EIO:i32=5;const ETIMEDOUT:i32=110;const EOVERFLOW:i32=75; static TRIP_LOAD:AtomicUsize=AtomicUsize::new(0); struct AtomicBool(StdAtomicBool);impl AtomicBool{fn new(v:bool)->Self{Self(StdAtomicBool::new(v))}fn store(&self,v:bool,o:Ordering){self.0.store(v,o);}fn load(&self,o:Ordering)->bool{let n=TRIP_LOAD.load(Ordering::Relaxed);if n!=0&&TRIP_LOAD.fetch_sub(1,Ordering::Relaxed)==1{self.0.store(true,Ordering::Release);}self.0.load(o)}} @@ -29,6 +29,7 @@ def body(source,marker): struct MutexBackend;struct Mutex(StdMutex);implMutex{fn new(v:T)->Self{Self(StdMutex::new(v))}fn lock(&self)->Guard<'_,T,MutexBackend>{Guard(self.0.lock().unwrap(),std::marker::PhantomData)}} struct Guard<'a,T,M>(std::sync::MutexGuard<'a,T>,std::marker::PhantomData);implDeref for Guard<'_,T,M>{type Target=T;fn deref(&self)->&T{&self.0}}implDerefMut for Guard<'_,T,M>{fn deref_mut(&mut self)->&mut T{&mut self.0}} struct Monotonic;struct Delta;impl Delta{fn from_millis(n:u64)->u64{n}}struct Instant{n:std::cell::Cell,p:std::marker::PhantomData}implInstant{fn now()->Self{Self{n:std::cell::Cell::new(0),p:std::marker::PhantomData}}fn elapsed(&self)->u64{let n=self.n.get();self.n.set(n+1);n}} +mod pgtable{pub fn clean_poc(_p:*const u8,_n:usize){super::BARRIERS.fetch_add(1,super::Ordering::Relaxed);}pub fn poc_barrier(){super::BARRIERS.fetch_add(1,super::Ordering::Relaxed);}} mod mem{pub fn sync(){super::BARRIERS.fetch_add(1,super::Ordering::Relaxed);}pub fn tlbi_asid(_:u8){}} #[derive(Clone,Copy,PartialEq,Eq)]enum UatFirmware{Handoff,PublishedRoots} struct Handoff{lock_ap:AtomicU8,lock_fw:AtomicU8,turn:AtomicU32,cur_slot:AtomicU32}impl Handoff{fn new()->Self{Self{lock_ap:AtomicU8::new(0),lock_fw:AtomicU8::new(0),turn:AtomicU32::new(0),cur_slot:AtomicU32::new(0xffff)}} @@ -36,17 +37,19 @@ def body(source,marker): handoff='\n'.join(body(mm,m) for m in ['fn lock(&self) {\n self.lock_ap', 'fn unlock(&self) {\n self.turn', 'fn current_slot(']) mid=r''' } -struct SlotTTBS{ttb0:AtomicU64,ttb1:AtomicU64}struct UatShared{handoff:Handoff,ttbs:[SlotTTBS;64],map_kernel_to_user:bool,kernel_ttb1:u64,binding_generations:[u64;64],binding_published:[bool;64],owned_user_roots:[u64;64]} +struct Quirks{handoff_lock:bool,clean_tables:bool} +struct SlotTTBS{ttb0:AtomicU64,ttb1:AtomicU64}struct UatShared{handoff:Handoff,ttbs:[SlotTTBS;64],map_kernel_to_user:bool,kernel_ttb1:u64,binding_generations:[u64;64],binding_published:[bool;64],owned_user_roots:[u64;64],quirks:Quirks} impl UatShared{fn handoff(&self)->&Handoff{&self.handoff}fn ttbs(&self)->&[SlotTTBS;64]{&self.ttbs}} struct UatInner{firmware:UatFirmware,m4_adopted:bool,failed:AtomicBool,shared:Mutex} ''' +mid=mid.replace('impl UatShared{','impl UatShared{'+''.join(body(mm,m) for m in ['fn handoff_lock(', 'fn handoff_unlock(', 'fn publish_slot('])) raii='#[path="'+str(ROOT/'drivers/gpu/drm/asahi/mmu/ppl.rs')+'"]mod ppl;\n'+'\n'.join(body(mm,m) for m in [ 'struct M4Transport<', 'impl ppl::Memory for M4Transport', 'enum HandoffGuard<', 'impl HandoffGuard<', 'impl Drop for HandoffGuard<' ]) inner='impl UatInner{\n'+'\n'.join(body(mm,m) for m in ['fn lock(&self) -> Guard', 'fn check_m4(', 'fn lock_handoff(', 'fn lock_m4_upper('])+'\n}\n' models=r''' -fn uat(m4:bool,firmware:UatFirmware)->Rc{Rc::new(UatInner{firmware,m4_adopted:m4,failed:AtomicBool::new(false),shared:Mutex::new(UatShared{handoff:Handoff::new(),ttbs:std::array::from_fn(|_|SlotTTBS{ttb0:AtomicU64::new(0),ttb1:AtomicU64::new(0)}),map_kernel_to_user:!m4,kernel_ttb1:0x800000000,binding_generations:[0;64],binding_published:[false;64],owned_user_roots:[0;64]})})} +fn uat(m4:bool,firmware:UatFirmware)->Rc{Rc::new(UatInner{firmware,m4_adopted:m4,failed:AtomicBool::new(false),shared:Mutex::new(UatShared{handoff:Handoff::new(),ttbs:std::array::from_fn(|_|SlotTTBS{ttb0:AtomicU64::new(0),ttb1:AtomicU64::new(0)}),map_kernel_to_user:!m4,kernel_ttb1:0x800000000,binding_generations:[0;64],binding_published:[false;64],owned_user_roots:[0;64],quirks:Quirks{handoff_lock:true,clean_tables:false}})})} #[derive(Clone,Copy)]struct Prot{cached:bool}impl Prot{fn is_cached_noncoherent(&self)->bool{self.cached}} struct Mutation;impl Drop for Mutation{fn drop(&mut self){MUTATIONS.fetch_sub(1,Ordering::Relaxed);}} struct MapTable{fail:bool}impl MapTable{fn unmap_pages(&mut self,_r:Range)->Result{LEAF_CLEARS.fetch_add(1,Ordering::Relaxed);if self.fail{Err(EIO)}else{Ok(())}}} @@ -72,7 +75,7 @@ def body(source,marker): #[derive(Clone)]struct Vm{id:u64,binding:Rc>} struct VmBind(Vm,u32); struct Uat{inner:Rc,slots:Allocator} -mod module_parameters{pub struct Param;impl Param{pub fn value(&self)->&u32{&0}}pub static robust_isolation:Param=Param;} +mod module_parameters{pub struct Param;impl Param{pub fn value(&self)->u32{0}}pub static robust_isolation:Param=Param;} ''' helpers='\n'.join(body(mm,m) for m in ['const fn binding_wire_generation(', 'const fn application_ttb1(']) bind='impl Uat{'+body(mm,'pub(crate) fn bind(&self, vm: &Vm)')+'}\n' diff --git a/tools/testing/selftests/drivers/gpu/asahi-m4/test-uapi.py b/tools/testing/selftests/drivers/gpu/asahi-m4/test-uapi.py index 39d2500f60b19..a6c936f3c0b4a 100755 --- a/tools/testing/selftests/drivers/gpu/asahi-m4/test-uapi.py +++ b/tools/testing/selftests/drivers/gpu/asahi-m4/test-uapi.py @@ -27,7 +27,7 @@ def main(): if args.mesa: mesa = args.mesa.resolve() / 'include/drm-uapi/asahi_drm.h' errors += differences(kernel, c_layout(mesa, bindings, out, 'mesa-layout')) - report = {'passed': not errors, 'scope': 'Compiled public UAPI structures, field offsets and constants; no kernel parser or feature-semantic coverage', + report = {'passed': not errors, 'scope': 'Compiled public UAPI structures, field offsets and constants; frozen params prefix plus exact known capacity-tail assertions; no parser or feature-semantic coverage', 'kernel': str(header), 'bindings': str(bindings), 'comparisons': len(kernel), 'mesa': str(mesa) if mesa else None, 'mesa_comparison': 'performed' if mesa else 'not requested; set ASAHI_M4_MESA or --mesa', diff --git a/tools/testing/selftests/drivers/gpu/asahi-m4/test-vm-registry.py b/tools/testing/selftests/drivers/gpu/asahi-m4/test-vm-registry.py index d7750aab9e147..add96b48bb335 100644 --- a/tools/testing/selftests/drivers/gpu/asahi-m4/test-vm-registry.py +++ b/tools/testing/selftests/drivers/gpu/asahi-m4/test-vm-registry.py @@ -20,7 +20,7 @@ def item(marker): implstd::ops::Deref for KVec{type Target=Vec;fn deref(&self)->&Vec{&self.0}} implstd::ops::DerefMut for KVec{fn deref_mut(&mut self)->&mut Vec{&mut self.0}} mod hw{pub mod sizes{pub const MAX_VMS:u32=63;}} -mod module_parameters{pub struct Param;impl Param{pub fn value(&self)->&u32{&0}}pub static robust_isolation:Param=Param;} +mod module_parameters{pub struct Param;impl Param{pub fn value(&self)->u32{0}}pub static robust_isolation:Param=Param;} mod types{pub struct ScopeGuard(Option);implScopeGuard{pub fn new(f:F)->Self{Self(Some(f))}pub fn dismiss(mut self){self.0.take();}}implDrop for ScopeGuard{fn drop(&mut self){if let Some(f)=self.0.take(){f()}}}} struct Handle;impl Handle{fn is_closed(&self)->bool{false}} struct VmState{id:u64,vm:Handle,drops:Arc}impl Drop for VmState{fn drop(&mut self){self.drops.fetch_add(1,Ordering::AcqRel);}} From a0a9485847f8ba37c4fc8de581c9856118ab7371 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 11:48:45 +1100 Subject: [PATCH 149/388] tools/asahi: test T6040 calibration and loaded-image selection Exercise the production reader with synthetic properties, invalid tails and OPPs. Cover candidate ordering and exact image refusal, and update G17 fixtures for current queue ownership. Assisted-by: LLM Signed-off-by: Ryan Murray --- tools/asahi/g17-admission-deadline-tests.py | 7 +- tools/asahi/g17-firmware-selection-tests.py | 103 ++++++++++ tools/asahi/g17-owner-release-order-tests.py | 4 +- tools/asahi/g17-resource-tests.py | 13 +- tools/asahi/t6040-calibration-tests.py | 191 +++++++++++++++++++ 5 files changed, 309 insertions(+), 9 deletions(-) create mode 100644 tools/asahi/g17-firmware-selection-tests.py create mode 100644 tools/asahi/t6040-calibration-tests.py diff --git a/tools/asahi/g17-admission-deadline-tests.py b/tools/asahi/g17-admission-deadline-tests.py index 5002f70997307..b58715f6fcc58 100644 --- a/tools/asahi/g17-admission-deadline-tests.py +++ b/tools/asahi/g17-admission-deadline-tests.py @@ -40,7 +40,7 @@ def fixture(): const QUARANTINE_WAIT_MS:u32=1000; #[derive(Clone,Copy,PartialEq)] enum Plan {Any,Conflict} #[derive(Clone,Copy)] struct Request<'a>(&'a ()); -struct State {owners:[u64;2],quarantined:u64,next_ticket:u64,waiters:usize,conflict:bool,ready:bool} +struct State {owners:[u64;2],quarantined:u64,spared:u64,next_ticket:u64,waiters:usize,conflict:bool,ready:bool} impl State { fn exhausted(&self)->bool {false} fn refusal(&self,_:u64)->Result {Ok(())} @@ -69,7 +69,7 @@ def fixture(): ''' + item(source, 'fn wait_slice_ms(') + '\nimpl Pool {\n' + item(source, 'pub(crate) fn acquire_render(') + r''' } fn new_pool(conflict:bool,quarantined:u64)->Arc { - Arc::new(Pool{state:Lock(Mutex::new(State{owners:[0,1],quarantined,next_ticket:1,waiters:0,conflict,ready:false})),released:CondVar}) + Arc::new(Pool{state:Lock(Mutex::new(State{owners:[0,1],quarantined,spared:0,next_ticket:1,waiters:0,conflict,ready:false})),released:CondVar}) } fn reset(mode:usize){NOW.store(0,Ordering::SeqCst);WAITS.store(0,Ordering::SeqCst);MODE.store(mode,Ordering::SeqCst);} fn main(){ @@ -89,6 +89,9 @@ def fixture(): reset(mode);let pool=new_pool(false,2); assert!(matches!(pool.acquire_render(1,None,2_000_000_000,||Ok(())),Err(EIO))); assert!(NOW.load(Ordering::SeqCst)<=1_010_000_000,"quarantine budget extended"); + reset(mode);let pool=new_pool(false,2);pool.state.lock().spared=2; + assert!(matches!(pool.acquire_render(1,None,2_000_000_000,||Ok(())),Err(EBUSY))); + assert!(NOW.load(Ordering::SeqCst)<=1_010_000_000,"spared quarantine budget extended"); } reset(2);let pool=new_pool(true,0); assert!(matches!(pool.acquire_render(1,Some(Request(&())),2_000_000_000,||Ok(())),Err(ERESTARTSYS))); diff --git a/tools/asahi/g17-firmware-selection-tests.py b/tools/asahi/g17-firmware-selection-tests.py new file mode 100644 index 0000000000000..771c9c19c6e1e --- /dev/null +++ b/tools/asahi/g17-firmware-selection-tests.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Compile the G17 build selector with a modelled image-identity result. + +The shared fwid KUnit tests check the actual SHA-256 normalization. This host +check ensures OS metadata cannot admit a different image or veto a known one. +""" + +from pathlib import Path +import subprocess +import tempfile + +DRIVER = Path(__file__).resolve().parents[2] / "drivers/gpu/drm/asahi" + + +def method(source, signature): + start = source.index(signature) + end = source.index("{", start) + 1 + depth = 1 + while depth: + depth += (source[end] == "{") - (source[end] == "}") + end += 1 + return source[start:end] + + +HEADER = r''' +#![allow(dead_code)] +type Result = std::result::Result; +const ENODEV: i32 = -19; +macro_rules! dev_info { ($($t:tt)*) => { () }; } +macro_rules! dev_err { ($($t:tt)*) => { () }; } +struct AsahiDrmDevice { + version: Option<[u32; 3]>, + image: u32, + checked: std::cell::RefCell>, +} +mod fw { + pub struct Variant(pub u32); + pub mod variant { + use super::Variant; + pub struct Build { + pub os_version: [u32; 3], + pub name: &'static str, + pub image: u32, + pub variant: &'static Variant, + } + pub enum Firmware { Fixed(&'static Variant), Identified(&'static [Build]) } + } +} +struct Gpu; +impl Gpu { + fn os_firmware_version(dev: &AsahiDrmDevice) -> Option<[u32; 3]> { dev.version } + fn identify_image(dev: &AsahiDrmDevice, build: &'static fw::variant::Build) -> Result { + dev.checked.borrow_mut().push(build.image); + if dev.image == build.image { Ok(()) } else { Err(ENODEV) } + } +''' + +TESTS = r''' +} +static FIRST: fw::Variant = fw::Variant(1); +static SECOND: fw::Variant = fw::Variant(2); +static BUILDS: [fw::variant::Build; 2] = [ + fw::variant::Build { os_version: [26,6,2], name: "first", image: 1, variant: &FIRST }, + fw::variant::Build { os_version: [27,0,0], name: "second", image: 2, variant: &SECOND }, +]; +fn device(version: Option<[u32; 3]>, image: u32) -> AsahiDrmDevice { + AsahiDrmDevice { version, image, checked: std::cell::RefCell::new(Vec::new()) } +} +fn main() { + use fw::variant::Firmware; + for version in [None, Some([26,6,2]), Some([26,7,0]), Some([99,0,0])] { + let dev = device(version, 1); + assert_eq!(Gpu::firmware_variant(&dev, &Firmware::Identified(&BUILDS)).unwrap().0, 1); + } + let dev = device(Some([27,0,0]), 1); + assert_eq!(Gpu::firmware_variant(&dev, &Firmware::Identified(&BUILDS)).unwrap().0, 1); + assert_eq!(*dev.checked.borrow(), [2,1], "the OS version only orders the candidates"); + let dev = device(Some([26,6,2]), 9); + assert!(Gpu::firmware_variant(&dev, &Firmware::Identified(&BUILDS)).is_err()); + assert_eq!(*dev.checked.borrow(), [1,2]); + let dev = device(None, 9); + assert_eq!(Gpu::firmware_variant(&dev, &Firmware::Fixed(&SECOND)).unwrap().0, 2); + assert!(dev.checked.borrow().is_empty(), "fixed Neo admission is unchanged"); + println!("PASS G17 selection: exact image required, OS hint optional, fixed Neo unchanged"); +} +''' + + +def main(): + source = (DRIVER / "g17/mod.rs").read_text() + selector = method(source, " fn firmware_variant(") + with tempfile.TemporaryDirectory(prefix="asahi-firmware-selection-") as tmp: + root = Path(tmp) + (root / "main.rs").write_text(HEADER + selector + TESTS) + binary = root / "test" + subprocess.run(["rustc", "--edition=2021", "-Dwarnings", str(root / "main.rs"), + "-o", str(binary)], check=True) + subprocess.run([str(binary)], check=True) + + +if __name__ == "__main__": + main() diff --git a/tools/asahi/g17-owner-release-order-tests.py b/tools/asahi/g17-owner-release-order-tests.py index 201f525718950..fd0e2c4a80936 100755 --- a/tools/asahi/g17-owner-release-order-tests.py +++ b/tools/asahi/g17-owner-release-order-tests.py @@ -118,7 +118,7 @@ def fixture(): struct Ticket { ordinal: u64 } struct Active { ticket: Ticket } struct Packet { context: Arc } -struct Pair { executed: u64, switch: Option<(u64, u64)>, replay: Option<()>, active: Vec, idle: bool } +struct Pair { executed: u64, switch: Option<(u64, u64)>, replay: Vec<()>, active: Vec, idle: bool } impl Pair { SWITCH /// Host stand-in for a successful publication of `ordinal` (stand-in, not production). @@ -177,7 +177,7 @@ def fixture(): assert!(!table.releasing(&quiet)); // Switch tracking across an owner change of one pair. - let mut pair = Pair { executed: 0, switch: None, replay: None, active: Vec::new(), idle: true }; + let mut pair = Pair { executed: 0, switch: None, replay: Vec::new(), active: Vec::new(), idle: true }; assert_eq!(pair.owner_switch_from(&c), None, "a fresh graph has no previous scheduler"); pair.publish(&Packet { context: d.clone() }, 0); pair.active.clear(); diff --git a/tools/asahi/g17-resource-tests.py b/tools/asahi/g17-resource-tests.py index 35f970799b086..5f87d2ea9e0f5 100644 --- a/tools/asahi/g17-resource-tests.py +++ b/tools/asahi/g17-resource-tests.py @@ -150,14 +150,17 @@ def item(s,start): #[derive(Clone,Copy)] struct Identity {id:Id,published:bool} '''+constants+'\n'+item(s,'pub(crate) struct QueueIds')+'\nimpl QueueIds {\n'+''.join(item(s,'pub(crate) fn '+name+'(')+'\n' for name in ['reserve_initial_render','reserve','publish','can_cancel_unpublished','cancel_unpublished'])+'}\n' test=''' - fn fresh()->QueueIds {QueueIds {entries:[None;QID_COUNT],initial_render:[None;2],compute_publication:0,leased:0}} + fn fresh()->QueueIds {QueueIds {entries:[None;QID_COUNT],initial_render:[None;2],publication:0,leased:0,floor:0}} fn main(){ - for render in [0,2,16,32,112,126] { - let mut q=fresh(); + for floor in [0,32] { + for render in [0,2,16,32,80,96,112,126].into_iter().filter(|render|*render<=QID_COUNT-floor) { + let mut q=fresh();q.floor=floor; for i in 0..render {q.reserve(i as u64+1,if i%2==0 {DataMaster::Tiling}else{DataMaster::Fragment},None).unwrap();} let mut compute=0; while q.reserve(1000+compute,DataMaster::Compute,None).is_ok(){compute+=1;} - assert_eq!(compute as usize,QID_COUNT-render.max(COMPUTE_RENDER_RESERVE)); + assert_eq!(compute as usize,QID_COUNT-floor-render.max(COMPUTE_RENDER_RESERVE-floor)); + assert!(q.entries[..floor].iter().all(Option::is_none)); + } } let mut q=fresh();let first=q.reserve(1,DataMaster::Tiling,None).unwrap(); assert!(q.can_cancel_unpublished(first));q.publish(first).unwrap(); @@ -165,7 +168,7 @@ def item(s,start): let unused=q.reserve(2,DataMaster::Fragment,None).unwrap();q.cancel_unpublished(unused).unwrap();assert_eq!(q.leased,1); let next=q.reserve(3,DataMaster::Fragment,None).unwrap();assert_eq!(next.qid,unused.qid); assert!(!q.can_cancel_unpublished(unused));assert_eq!(q.cancel_unpublished(unused),Err(EINVAL)); - println!("queue IDs: six render/compute capacity boundaries; compute-only render reserve retained; published and stale identities protected"); + println!("queue IDs: render/compute capacity boundaries at zero and reserved floors; compute-only render reserve retained; published and stale identities protected"); // Reproduce the hardware snapshot: 124 installed render identities, two // installed compute identities, and two initial reservations fill128 IDs. let mut q=fresh();q.reserve_initial_render().unwrap(); diff --git a/tools/asahi/t6040-calibration-tests.py b/tools/asahi/t6040-calibration-tests.py new file mode 100644 index 0000000000000..d802013175b97 --- /dev/null +++ b/tools/asahi/t6040-calibration-tests.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +"""Compile the T6040 wire builders and calibration checks without a GPU.""" + +from pathlib import Path +import re +import subprocess +import tempfile + +DRIVER = Path(__file__).resolve().parents[2] / "drivers/gpu/drm/asahi" + + + +HEADER = r''' +#![allow(dead_code)] +extern crate self as kernel; +mod prelude { + pub type KVec = Vec; + pub use std::mem::size_of; + pub type Result = std::result::Result; + #[derive(Debug)] + pub struct Error; + impl From for Error { + fn from(_: std::num::TryFromIntError) -> Self { Self } + } + pub const EINVAL: Error = Error; + pub const EIO: Error = Error; + pub const EOVERFLOW: Error = Error; + pub unsafe trait Zeroable {} +} +macro_rules! static_assert { ($e:expr) => { const _: () = assert!($e); } } +fn clear(value: &mut T) { + unsafe { std::ptr::write_bytes(value, 0, 1); } +} +#[macro_export] +macro_rules! c_str { + ($s:literal) => { std::ffi::CStr::from_bytes_with_nul(concat!($s, "\0").as_bytes()).unwrap() }; +} +pub mod str { pub use std::ffi::CStr; } +mod of { + use crate::prelude::*; + #[derive(Clone)] + pub struct Node(pub std::collections::BTreeMap>); + pub trait Property: Sized { fn decode(value: &[u32]) -> Result; } + impl Property for u32 { + fn decode(value: &[u32]) -> Result { + if value.len() == 1 { Ok(value[0]) } else { Err(EINVAL) } + } + } + impl Property for Vec { fn decode(value: &[u32]) -> Result { Ok(value.to_vec()) } } + impl Node { + pub fn get_property(&self, name: &crate::str::CStr) -> Result { + T::decode(self.0.get(name.to_str().unwrap()).ok_or(EINVAL)?) + } + } +} +mod driver { + pub struct AsahiDrmDevice(pub DrmDevice); + pub struct DrmDevice(pub Device); + pub struct Device(pub crate::of::Node); + impl AsRef for AsahiDrmDevice { fn as_ref(&self) -> &DrmDevice { &self.0 } } + impl AsRef for DrmDevice { fn as_ref(&self) -> &Device { &self.0 } } + impl Device { pub fn of_node(&self) -> Option { Some(self.0.clone()) } } +} +mod float; +mod initdata; +mod g17 { + pub mod fw { pub(crate) use crate::initdata; } + mod calibration; + pub fn check_property_adapter() { + use crate::{driver::*, of::Node}; + let fields: &[(&str, &[u32])] = &[ + ("apple,t6040-handoff-version", &[2]), + ("apple,core-mask", &[0xfffff]), + ("apple,max-power-mw", &[70000]), + ("apple,perf-base-pstate", &[1]), + ("apple,perf-map-a", &[0,1,2,3,4,5,6,7,8,9,15,0,0,0,0,0]), + ("apple,perf-map-b", &[0,1,2,3,4,5,6,7,8,9,15,0,0,0,0,0]), + ("apple,perf-relative-a", &[0,10,20,30,40,50,60,70,80,90,100,0,0,0,0,0]), + ("apple,perf-relative-b", &[0,0,11,22,33,44,55,66,77,88,100,0,0,0,0,0]), + ("apple,core-leak-coef", &[0x44480000, 0x44800000]), + ("apple,sram-leak-coef", &[0x43000000, 0x43000000]), + ("apple,cs-leak-coef", &[0x43800000]), + ("apple,afr-leak-coef", &[0x44000000]), + ]; + let node = Node(fields.iter().map(|(k,v)| (k.to_string(), v.to_vec())).collect()); + let read = |n| calibration::Calibration::read(&AsahiDrmDevice(DrmDevice(Device(n)))); + let chip = read(node.clone()).unwrap(); + assert_eq!(chip.max_power_mw, 70000); + assert_eq!(chip.leakage[0], 0x44480000); + assert_eq!(chip.leakage[1], 0x44800000); + assert_eq!(chip.leakage[32..34], [0x43000000; 2]); + assert_eq!(chip.leakage[52], 0x43800000); + assert_eq!(chip.leakage[54], 0x44000000); + for (key, _) in fields { + let mut missing = node.clone(); missing.0.remove(*key); + assert!(read(missing).is_err(), "accepted missing {key}"); + let mut short = node.clone(); short.0.get_mut(*key).unwrap().pop(); + assert!(read(short).is_err(), "accepted short {key}"); + } + for key in ["apple,perf-map-a", "apple,perf-map-b", "apple,perf-relative-a", "apple,perf-relative-b"] { + for index in 11..16 { + let mut bad = node.clone(); bad.0.get_mut(key).unwrap()[index] = 1; + assert!(read(bad).is_err(), "accepted nonzero tail {key}[{index}]"); + } + } + for (key, value) in [ + ("apple,t6040-handoff-version", 1), + ("apple,core-mask", 0x100000), + ("apple,max-power-mw", 0), + ("apple,perf-base-pstate", 0), + ("apple,perf-base-pstate", 2), + ("apple,cs-leak-coef", 0x7fc00000), + ("apple,afr-leak-coef", 0xbf800000), + ] { + let mut bad = node.clone(); bad.0.insert(key.to_string(), vec![value]); + assert!(read(bad).is_err(), "accepted invalid {key}"); + } + } +} +''' + +TEST = r''' +#[test] +fn property_adapter_requires_complete_chip_data() { g17::check_property_adapter(); } + +#[test] +fn power_configuration_uses_only_its_inputs() { + use initdata::v25g83::PowerConfig25G83; + let mut first = PowerConfig25G83([0xcc; 0x6510]); + let mut leakage = [0; 56]; + leakage[0] = 0x44480000; + leakage[1] = 0x44800000; + leakage[32] = 0x43000000; + leakage[33] = 0x43000000; + leakage[52] = 0x43800000; + leakage[54] = 0x44000000; + first.init(0x12340000, 0x56780000, 0xabcdef, 76543, &leakage); + let word = |bytes: &[u8], off| u32::from_le_bytes(bytes[off..off + 4].try_into().unwrap()); + for off in [0xa04, 0xabc, 0x28c8, 0x28cc, 0x28d0, 0x2a08] { + assert_eq!(word(&first.0, off), 76543); + } + for off in [0x9e0, 0x2a04] { + assert_eq!(word(&first.0, off), (76543f32).to_bits()); + } + for (i, value) in leakage.iter().enumerate() { + assert_eq!(word(&first.0, 0x642c + 4 * i), *value); + } + assert_eq!(u64::from_le_bytes(first.0[0x3b50..0x3b58].try_into().unwrap()), 0x12340080); + assert_eq!(u64::from_le_bytes(first.0[0x6318..0x6320].try_into().unwrap()), 0x56780000); + assert_eq!(word(&first.0, 0x63d4), 0xabcdef); + let mut second = PowerConfig25G83([0; 0x6510]); + second.init(0x12340000, 0x56780000, 0xabcdef, 76543, &leakage); + assert_eq!(first.0, second.0); + second.init(0x12340000, 0x56780000, 0xabcdef, 80000, &[0; 56]); + assert_eq!(word(&second.0, 0xa04), 80000); + assert_eq!(word(&second.0, 0x9e0), (80000f32).to_bits()); + for i in 0..56 { + assert_eq!(word(&second.0, 0x642c + 4 * i), 0); + } +} +''' + + +def main(): + with tempfile.TemporaryDirectory(prefix="asahi-t6040-") as tmp: + root = Path(tmp) + (root / "initdata").mkdir() + (root / "g17").mkdir() + for source, target in [ + ("g17/fw/initdata.rs", "initdata.rs"), + ("g17/fw/initdata/v25g83.rs", "initdata/v25g83.rs"), + ]: + text = (DRIVER / source).read_text() + (root / target).write_text(text.replace("use kernel::prelude::*;", "use crate::prelude::*;")) + text = (DRIVER / "float.rs").read_text().split("impl<'a> TryFrom Date: Mon, 26 Oct 2026 14:03:02 +1100 Subject: [PATCH 150/388] media: apple: isp: remove parameter-controlled format exposure Remove the inherited multiplanar module parameter. T6040/IMX958 supports normal V4L2 negotiation for contiguous NV12 and P010 through either API, with one memory plane containing the two ISP components. Keep NV12 first and initial; other sensors retain their shipped single-planar API. Do not advertise the old non-contiguous NV12M alias. Cache complete sensor configuration replies with the video state and print them on the standard log-status ioctl, so collecting the firmware list needs no driver or boot parameter. Free the cache on probe failure and final video release, and test failed discovery without publishing partial state. No module/kernel parameters or platform Kconfig options are added. Assisted-by: LLM Signed-off-by: Ryan Murray --- .../media/drivers/apple-isp-j616s.rst | 25 ++++--- .../media/platform/apple/isp/isp-cam-test.c | 41 +++++++++++ drivers/media/platform/apple/isp/isp-cam.c | 20 ++++-- drivers/media/platform/apple/isp/isp-drv.c | 1 + drivers/media/platform/apple/isp/isp-drv.h | 5 +- .../media/platform/apple/isp/isp-v4l2-test.c | 14 +++- drivers/media/platform/apple/isp/isp-v4l2.c | 70 ++++++------------- 7 files changed, 111 insertions(+), 65 deletions(-) diff --git a/Documentation/driver-api/media/drivers/apple-isp-j616s.rst b/Documentation/driver-api/media/drivers/apple-isp-j616s.rst index f92cecf30f3e5..d5342c1d796af 100644 --- a/Documentation/driver-api/media/drivers/apple-isp-j616s.rst +++ b/Documentation/driver-api/media/drivers/apple-isp-j616s.rst @@ -46,9 +46,9 @@ Mode and format selection On T6040/IMX958, probe queries every advertised configuration, bounded at 64, using the 0x120-byte H17 ``CH_CAMERA_CONFIG_GET`` exchange. The complete list -is available through dynamic debug at probe and cached for -``VIDIOC_LOG_STATUS`` (``v4l2-ctl --log-status``), without sending commands -during a stream. The log includes +is available through dynamic debug at probe and cached in full for +``VIDIOC_LOG_STATUS`` (``v4l2-ctl --log-status``), without sending firmware +commands. Collect this diagnostic dump before starting a stress run. It includes input/output dimensions and the sensor clock word; that word is not FPS. The board's existing config-5 1920x1080 and 1280x720 presets stay first and @@ -68,9 +68,9 @@ the runtime input geometry matches the source profile. An unspecified P010 size selects 1280x720 first. Larger P010 modes require a consistent firmware output reply. P010 uses two little-endian 16-bit component planes with ten useful MSB-aligned bits, and equal 64-byte-aligned Y/UV strides. UV immediately -follows Y, as required for contiguous P010. The optional multi-planar V4L2 -API also reports P010 as **one memory plane** containing both component -planes. The firmware still receives two component addresses. +follows Y, as required for contiguous P010. The standard multi-planar V4L2 +API on T6040/IMX958 reports both NV12 and P010 as **one memory plane** +containing both component planes. The firmware still receives two component addresses. At 1920x1080, stride is 3840, Y is 4147200 bytes, UV is 2073600 bytes and ``sizeimage`` is 6220800. At 1280x720, stride is 2560 and ``sizeimage`` is @@ -86,8 +86,17 @@ or both zero-extended low-32-bit plane addresses, plus plane count, pool 9 and a fresh 64-bit submission tag; an old tag cannot release a new lease. Buffer completion is published to videobuf2 only after the firmware report ACK and doorbell have been published. -NV12 pool sizes and the T8140/Neo formats, presets and command path retain -their existing behavior. +NV12 pool sizes and the T8140/Neo default formats, presets and command path +retain their existing behavior. + +The inherited ``multiplanar`` module parameter is removed. T6040/IMX958 +supports standard single-planar and multi-planar V4L2 negotiation for these +contiguous formats without a parameter; the non-contiguous NV12M alias is not +advertised. Other sensors retain their shipped single-planar API. No driver +module/kernel parameter, platform flag or per-platform Kconfig option selects +support. NV12 stays first and is the initial format because browsers/WebRTC +and many applications do not support P010 or simply choose the first format. +Applications can select P010 explicitly with ``VIDIOC_S_FMT``. Endpoints --------- diff --git a/drivers/media/platform/apple/isp/isp-cam-test.c b/drivers/media/platform/apple/isp/isp-cam-test.c index aef545b27c051..27c9e23f348aa 100644 --- a/drivers/media/platform/apple/isp/isp-cam-test.c +++ b/drivers/media/platform/apple/isp/isp-cam-test.c @@ -186,9 +186,15 @@ static int isp_test_config_reply(struct apple_isp *isp, void *args, u32 insize, config->out_height2 = config->out_height; if (config->preset == 2) config->out_height2 = 2160; + reply->unknown[sizeof(reply->unknown) - 1] = 0x5a; return 0; } +static void isp_test_free_configs(void *ptr) +{ + kfree(ptr); +} + static void isp_test_sensor_config_list(struct kunit *test) { struct apple_isp *isp = isp_test_capture_alloc(test); @@ -199,6 +205,7 @@ static void isp_test_sensor_config_list(struct kunit *test) .output_dim = {1280, 720}, .crop_size = {1280, 720} }, }; u32 count = 0; + int err; isp->cmd_virt = kunit_kzalloc(test, 0x120, GFP_KERNEL); KUNIT_ASSERT_NOT_NULL(test, isp->cmd_virt); @@ -207,6 +214,10 @@ static void isp_test_sensor_config_list(struct kunit *test) test->priv = &count; kunit_activate_static_stub(test, isp_cmd_send, isp_test_config_reply); KUNIT_ASSERT_EQ(test, isp_ch_cache_sensor_configs(isp, 0, 6), 0); + err = kunit_add_action_or_reset(test, isp_test_free_configs, isp->sensor_configs); + KUNIT_ASSERT_EQ(test, err, 0); + KUNIT_EXPECT_EQ(test, get_unaligned_le32(isp->sensor_configs[5].reply + 0xc), 5); + KUNIT_EXPECT_EQ(test, isp->sensor_configs[5].reply[ISP_SENSOR_CONFIG_SIZE - 1], 0x5a); KUNIT_EXPECT_EQ(test, count, 6); KUNIT_EXPECT_EQ(test, isp->num_sensor_configs, 6); KUNIT_EXPECT_EQ(test, isp->num_presets, 3); @@ -223,6 +234,35 @@ static void isp_test_sensor_config_list(struct kunit *test) KUNIT_EXPECT_EQ(test, count, 6); } +static int isp_test_bad_config_reply(struct apple_isp *isp, void *args, u32 insize, + u32 outsize, int timeout) +{ + struct cmd_ch_camera_config_h17 *reply = isp->cmd_virt; + + isp_test_config_reply(isp, args, insize, outsize, timeout); + reply->config.preset = 65; + return 0; +} + +static void isp_test_sensor_config_failure(struct kunit *test) +{ + struct apple_isp *isp = isp_test_capture_alloc(test); + struct isp_preset preset = {}; + u32 count = 0; + + isp->cmd_virt = kunit_kzalloc(test, ISP_SENSOR_CONFIG_SIZE, GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, isp->cmd_virt); + isp->presets = &preset; + isp->num_presets = 1; + test->priv = &count; + kunit_activate_static_stub(test, isp_cmd_send, isp_test_bad_config_reply); + KUNIT_EXPECT_EQ(test, isp_ch_cache_sensor_configs(isp, 0, 6), -EIO); + KUNIT_EXPECT_EQ(test, count, 1); + KUNIT_EXPECT_EQ(test, isp->num_sensor_configs, 0); + KUNIT_EXPECT_PTR_EQ(test, isp->sensor_configs, NULL); + KUNIT_EXPECT_PTR_EQ(test, isp->presets, &preset); +} + struct isp_test_ack_state { unsigned int fault, calls; }; @@ -282,6 +322,7 @@ static struct kunit_case isp_cam_test_cases[] = { KUNIT_CASE(isp_test_command_ack_failure_latches), KUNIT_CASE(isp_test_sensor_geometry), KUNIT_CASE(isp_test_sensor_config_list), + KUNIT_CASE(isp_test_sensor_config_failure), KUNIT_CASE(isp_test_preview_stream), KUNIT_CASE(isp_test_preview_command_failures), {} diff --git a/drivers/media/platform/apple/isp/isp-cam.c b/drivers/media/platform/apple/isp/isp-cam.c index 59f19e571bb64..a5cbb2b4d8eb0 100644 --- a/drivers/media/platform/apple/isp/isp-cam.c +++ b/drivers/media/platform/apple/isp/isp-cam.c @@ -233,6 +233,7 @@ static bool isp_sensor_config_usable(const struct cmd_ch_camera_config *config) static int isp_ch_cache_sensor_configs(struct apple_isp *isp, u32 ch, u32 count) { struct cmd_ch_camera_config_h17 args; + struct isp_sensor_config *configs; struct isp_preset *presets; unsigned int num = isp->num_presets; int err; @@ -247,19 +248,25 @@ static int isp_ch_cache_sensor_configs(struct apple_isp *isp, u32 ch, u32 count) if (!presets) return -ENOMEM; memcpy(presets, isp->presets, num * sizeof(*presets)); + configs = kcalloc(count, sizeof(*configs), GFP_KERNEL); + if (!configs) + return -ENOMEM; for (u32 i = 0; i < count; i++) { struct cmd_ch_camera_config *config = &args.config; - struct isp_sensor_config *cached = &isp->sensor_configs[i]; + struct isp_sensor_config *cached = &configs[i]; bool duplicate = false; err = isp_cmd_ch_camera_config_get(isp, ch, i, &args); if (err) - return err; + goto free_configs; if (config->opcode != (u64)CISP_CMD_CH_CAMERA_CONFIG_GET << 32 || - config->chan != ch || config->preset != i) - return -EIO; + config->chan != ch || config->preset != i) { + err = -EIO; + goto free_configs; + } + memcpy(cached->reply, &args, sizeof(args)); cached->input_dim = (struct coord){config->in_width, config->in_height}; cached->output_dim = (struct coord){config->out_width, config->out_height}; cached->sensor_clk = config->sensor_clk; @@ -313,10 +320,15 @@ static int isp_ch_cache_sensor_configs(struct apple_isp *isp, u32 ch, u32 count) .p010 = true, }; } + isp->sensor_configs = configs; isp->num_sensor_configs = count; isp->presets = presets; isp->num_presets = num; return 0; + +free_configs: + kfree(configs); + return err; } static int isp_ch_cache_sensor_info(struct apple_isp *isp, u32 ch) diff --git a/drivers/media/platform/apple/isp/isp-drv.c b/drivers/media/platform/apple/isp/isp-drv.c index 04c50011abda1..084cca4dc3707 100644 --- a/drivers/media/platform/apple/isp/isp-drv.c +++ b/drivers/media/platform/apple/isp/isp-drv.c @@ -586,6 +586,7 @@ static int apple_isp_probe(struct platform_device *pdev) detach_genpd: apple_isp_detach_genpd(isp); free_isp: + kfree(isp->sensor_configs); /* Nothing else can reach it before the video device is registered. */ kfree(isp); return err; diff --git a/drivers/media/platform/apple/isp/isp-drv.h b/drivers/media/platform/apple/isp/isp-drv.h index db042f94aed47..dbe6b6ed5a2c4 100644 --- a/drivers/media/platform/apple/isp/isp-drv.h +++ b/drivers/media/platform/apple/isp/isp-drv.h @@ -31,6 +31,7 @@ /* used to limit the user space buffers to the buffer_pool_config */ #define ISP_MAX_BUFFERS 16 #define ISP_MAX_SENSOR_CONFIGS 64 +#define ISP_SENSOR_CONFIG_SIZE 0x120 /* capture metadata buffers, for firmware that has that pool */ #define ISP_CAPMETA_BUFFERS 8 @@ -265,6 +266,7 @@ struct isp_sensor_config { struct coord output_dim; u32 sensor_clk; bool usable; + u8 reply[ISP_SENSOR_CONFIG_SIZE]; }; struct isp_format { @@ -289,7 +291,7 @@ struct apple_isp { int num_presets; /* Read-only sensor configuration replies, cached before video registration. */ unsigned int num_sensor_configs; - struct isp_sensor_config sensor_configs[ISP_MAX_SENSOR_CONFIGS]; + struct isp_sensor_config *sensor_configs; int num_channels; struct isp_format fmts[ISP_MAX_CHANNELS]; @@ -303,7 +305,6 @@ struct apple_isp { struct vb2_queue vbq; struct mutex video_lock; unsigned int sequence; - bool multiplanar; /* capture rate in frames per second, changes under video_lock */ unsigned int frame_rate; diff --git a/drivers/media/platform/apple/isp/isp-v4l2-test.c b/drivers/media/platform/apple/isp/isp-v4l2-test.c index 6af295a7da23e..b3c8d26dfd4aa 100644 --- a/drivers/media/platform/apple/isp/isp-v4l2-test.c +++ b/drivers/media/platform/apple/isp/isp-v4l2-test.c @@ -102,6 +102,12 @@ static void isp_test_create_bufs_limit(struct kunit *test) KUNIT_EXPECT_EQ(test, isp_test_setup(test, ISP_MAX_BUFFERS, &nbuffers, &num_planes, sizes), -ENOBUFS); + /* The contiguous API must refuse a second independent memory plane. */ + nbuffers = 1; + num_planes = 2; + KUNIT_EXPECT_EQ(test, isp_test_setup(test, 0, &nbuffers, &num_planes, + sizes), -EINVAL); + /* A plane smaller than a frame is still refused. */ nbuffers = 1; num_planes = 1; @@ -158,6 +164,10 @@ static void isp_test_p010_format(struct kunit *test) KUNIT_EXPECT_EQ(test, fmt->total_size, (size_t)1382400); KUNIT_EXPECT_EQ(test, isp_num_rendered(isp), ISP_MAX_BUFFERS); KUNIT_EXPECT_EQ(test, isp_num_meta(isp), ISP_MAX_BUFFERS); + isp_get_mp_pix_format(isp, &f, fmt); + KUNIT_EXPECT_EQ(test, f.fmt.pix_mp.pixelformat, V4L2_PIX_FMT_NV12); + KUNIT_EXPECT_EQ(test, f.fmt.pix_mp.num_planes, 1); + KUNIT_EXPECT_EQ(test, f.fmt.pix_mp.plane_fmt[0].sizeimage, 1382400); } static void isp_test_p010_admission(struct kunit *test) @@ -179,9 +189,7 @@ static void isp_test_p010_admission(struct kunit *test) KUNIT_EXPECT_EQ(test, desc.pixelformat, V4L2_PIX_FMT_P010); desc.index = 2; KUNIT_EXPECT_EQ(test, isp_enum_pixelformat(isp, &desc), -EINVAL); - isp->multiplanar = true; - KUNIT_ASSERT_EQ(test, isp_enum_pixelformat(isp, &desc), 0); - KUNIT_EXPECT_EQ(test, desc.pixelformat, V4L2_PIX_FMT_P010); + desc.index = 1; isp->hw = &neo; KUNIT_EXPECT_EQ(test, isp_enum_pixelformat(isp, &desc), -EINVAL); KUNIT_ASSERT_EQ(test, isp_set_preset(isp, fmt, &preset, V4L2_PIX_FMT_P010), 0); diff --git a/drivers/media/platform/apple/isp/isp-v4l2.c b/drivers/media/platform/apple/isp/isp-v4l2.c index b5bfea73e2d82..bc270e06be21f 100644 --- a/drivers/media/platform/apple/isp/isp-v4l2.c +++ b/drivers/media/platform/apple/isp/isp-v4l2.c @@ -27,10 +27,6 @@ /* Capture rates offered to userspace, in frames per second */ static const unsigned int isp_frame_rates[] = { 30, 25, 24, 20, 15 }; -static bool multiplanar = false; -module_param(multiplanar, bool, 0644); -MODULE_PARM_DESC(multiplanar, "Enable multiplanar API"); - struct isp_buflist_buffer { u64 iovas[ISP_MAX_PLANES]; u32 flags[ISP_MAX_PLANES]; @@ -362,7 +358,7 @@ static int isp_vb2_queue_setup(struct vb2_queue *vq, unsigned int *nbuffers, isp_num_rendered(isp) - q_num_bufs); if (*num_planes) { - if (sizes[0] < fmt->total_size) + if (*num_planes != 1 || sizes[0] < fmt->total_size) return -EINVAL; return 0; @@ -693,25 +689,13 @@ static int isp_vidioc_querycap(struct file *file, void *priv, static int isp_enum_pixelformat(struct apple_isp *isp, struct v4l2_fmtdesc *f) { - switch (f->index) { - case 0: + /* Browser/WebRTC compatibility: the shipped first format remains NV12. */ + if (!f->index) f->pixelformat = V4L2_PIX_FMT_NV12; - break; - case 1: - if (isp->multiplanar) { - f->pixelformat = V4L2_PIX_FMT_NV12M; - break; - } - fallthrough; - case 2: - if (!isp_has_p010(isp) || f->index != (isp->multiplanar ? 2 : 1)) - return -EINVAL; + else if (f->index == 1 && isp_has_p010(isp)) f->pixelformat = V4L2_PIX_FMT_P010; - break; - default: + else return -EINVAL; - } - return 0; } @@ -729,7 +713,6 @@ static int isp_vidioc_enum_framesizes(struct file *file, void *fh, u32 index = 0; if (f->pixel_format != V4L2_PIX_FMT_NV12 && - f->pixel_format != V4L2_PIX_FMT_NV12M && !(f->pixel_format == V4L2_PIX_FMT_P010 && isp_has_p010(isp))) return -EINVAL; @@ -760,7 +743,6 @@ static int isp_vidioc_enum_frameintervals(struct file *file, void *fh, return -EINVAL; if (interval->pixel_format != V4L2_PIX_FMT_NV12 && - interval->pixel_format != V4L2_PIX_FMT_NV12M && !(interval->pixel_format == V4L2_PIX_FMT_P010 && isp_has_p010(isp))) return -EINVAL; @@ -827,27 +809,17 @@ static inline void isp_get_mp_pix_format(struct apple_isp *isp, { f->fmt.pix_mp.width = fmt->preset->output_dim.x; f->fmt.pix_mp.height = fmt->preset->output_dim.y; - f->fmt.pix_mp.num_planes = fmt->num_planes; - for (int i = 0; i < fmt->num_planes; i++) { - f->fmt.pix_mp.plane_fmt[i].sizeimage = fmt->plane_size[i]; - f->fmt.pix_mp.plane_fmt[i].bytesperline = fmt->strides[i]; - } - + /* Both contiguous formats have one memory plane and two ISP components. */ + f->fmt.pix_mp.num_planes = 1; + memset(f->fmt.pix_mp.plane_fmt, 0, sizeof(f->fmt.pix_mp.plane_fmt)); + f->fmt.pix_mp.plane_fmt[0].sizeimage = fmt->total_size; + f->fmt.pix_mp.plane_fmt[0].bytesperline = fmt->strides[0]; f->fmt.pix_mp.field = V4L2_FIELD_NONE; - f->fmt.pix_mp.pixelformat = V4L2_PIX_FMT_NV12M; - /* P010 is contiguous, including through the multi-planar API. */ - if (fmt->pixelformat == V4L2_PIX_FMT_P010) { - f->fmt.pix_mp.pixelformat = V4L2_PIX_FMT_P010; - f->fmt.pix_mp.num_planes = 1; - f->fmt.pix_mp.plane_fmt[0].sizeimage = fmt->total_size; - memset(&f->fmt.pix_mp.plane_fmt[1], 0, - sizeof(f->fmt.pix_mp.plane_fmt[1])); - } + f->fmt.pix_mp.pixelformat = fmt->pixelformat; f->fmt.pix_mp.colorspace = V4L2_COLORSPACE_REC709; f->fmt.pix_mp.ycbcr_enc = V4L2_YCBCR_ENC_709; f->fmt.pix_mp.xfer_func = V4L2_XFER_FUNC_709; - if (fmt->pixelformat == V4L2_PIX_FMT_P010) - f->fmt.pix_mp.quantization = V4L2_QUANTIZATION_LIM_RANGE; + f->fmt.pix_mp.quantization = V4L2_QUANTIZATION_LIM_RANGE; } static int isp_vidioc_get_format(struct file *file, void *fh, @@ -911,7 +883,7 @@ static int isp_vidioc_get_format_mplane(struct file *file, void *fh, struct apple_isp *isp = video_drvdata(file); struct isp_format *fmt = isp_get_current_format(isp); - if (!isp->multiplanar) + if (!isp_supports_p010(isp)) return -ENOTTY; isp_get_mp_pix_format(isp, f, fmt); @@ -927,7 +899,7 @@ static int isp_vidioc_set_format_mplane(struct file *file, void *fh, struct isp_preset *preset; int err; - if (!isp->multiplanar) + if (!isp_supports_p010(isp)) return -ENOTTY; if (vb2_is_busy(&isp->vbq)) @@ -954,7 +926,7 @@ static int isp_vidioc_try_format_mplane(struct file *file, void *fh, struct isp_preset *preset; int err; - if (!isp->multiplanar) + if (!isp_supports_p010(isp)) return -ENOTTY; preset = isp_select_preset(isp, f->fmt.pix_mp.width, f->fmt.pix_mp.height, @@ -1001,7 +973,7 @@ static int isp_vidioc_get_param(struct file *file, void *fh, struct apple_isp *isp = video_drvdata(file); if (a->type != V4L2_BUF_TYPE_VIDEO_CAPTURE && - (!isp->multiplanar || + (!isp_supports_p010(isp) || a->type != V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE)) return -EINVAL; @@ -1019,7 +991,7 @@ static int isp_vidioc_set_param(struct file *file, void *fh, struct v4l2_fract *tpf = &a->parm.capture.timeperframe; if (a->type != V4L2_BUF_TYPE_VIDEO_CAPTURE && - (!isp->multiplanar || + (!isp_supports_p010(isp) || a->type != V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE)) return -EINVAL; @@ -1055,6 +1027,9 @@ static int isp_vidioc_log_status(struct file *file, void *fh) config->output_dim.x, config->output_dim.y, config->sensor_clk, config->usable ? "" : " (unverified geometry)"); + /* This explicit diagnostic ioctl replaces a probe-time parameter. */ + print_hex_dump(KERN_INFO, "apple-isp config: ", DUMP_PREFIX_OFFSET, + 16, 4, config->reply, sizeof(config->reply), false); } return 0; } @@ -1120,6 +1095,7 @@ static void apple_isp_release(struct v4l2_device *v4l2_dev) v4l2_ctrl_handler_free(&isp->ctrl_handler); media_device_cleanup(&isp->mdev); + kfree(isp->sensor_configs); kfree(isp); } @@ -1206,8 +1182,6 @@ int apple_isp_setup_video(struct apple_isp *isp) goto media_cleanup; } - isp->multiplanar = multiplanar; - /* * Auto exposure is always on. By default it holds the frame rate, as * V4L2 specifies; with exposure priority it may slow down in low light. @@ -1239,7 +1213,7 @@ int apple_isp_setup_video(struct apple_isp *isp) vdev->fops = &isp_v4l2_fops; vdev->ioctl_ops = &isp_v4l2_ioctl_ops; vdev->device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING; - if (isp->multiplanar) + if (isp_supports_p010(isp)) vdev->device_caps |= V4L2_CAP_VIDEO_CAPTURE_MPLANE; vdev->entity.flags = MEDIA_ENT_FL_DEFAULT; vdev->v4l2_dev = &isp->v4l2_dev; From 905dff77456191245d1aff3d093c2f41947983ff Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 14:06:00 +1100 Subject: [PATCH 151/388] drm/apple: inspect desktop scalar and service panel property setters Inspect the remaining public property setter wire shapes at callback ingress, without printing scalar values or changing acknowledgements. Use the existing miscellaneous-property budget and KMS debug category. This also catches identity keys delivered outside the dictionary setters. Receive service-prefixed D413 dictionaries through the same checked panel identity and EDID path as D552/D561. Validate their full envelope and exercise the service offset, scalar types, null values and short records with the production parser and host dispatcher fixtures. Assisted-by: LLM Signed-off-by: Ryan Murray --- Documentation/gpu/apple-dcp-t6041-panel.rst | 7 +- drivers/gpu/drm/apple/iomfb.c | 66 +++++++++++++++++++ drivers/gpu/drm/apple/iomfb_v26_6_desktop.c | 7 +- .../drivers/gpu/apple-dcp/desktop.py | 22 ++++++- .../drivers/gpu/apple-dcp/lifecycle.py | 1 + 5 files changed, 99 insertions(+), 4 deletions(-) diff --git a/Documentation/gpu/apple-dcp-t6041-panel.rst b/Documentation/gpu/apple-dcp-t6041-panel.rst index aa15af0f59913..2566c3fd36a94 100644 --- a/Documentation/gpu/apple-dcp-t6041-panel.rst +++ b/Documentation/gpu/apple-dcp-t6041-panel.rst @@ -85,7 +85,12 @@ are dictionary 1, array 2, integer 4, string 9, blob 10 and boolean 11. Values are never printed. Each property walk stops at depth 4 or 64 objects, and the DCP stops after 128 diagnostic records per callback class (512 total), with bounded inspection-status lines in addition. Atomic reservations include framing records. D552, D561, D114 and chunked delivery have -separate budgets so periodic statistics cannot hide the panel setters. +separate budgets so periodic statistics cannot hide the panel setters. Service +dictionary setter D413 is inspected and can supply identity or a raw EDID too. +Scalar setters D102/D104/D414/D415/D563/D565/D567 report keys and types at +ingress when their public wire lengths match. They share the +fourth budget with chunked delivery. Null scalar values report size zero; no +scalar value or service handle is printed, and acknowledgements are unchanged. When KMS debugging is disabled no inspection budget is consumed. Malformed serialization only stops inspection. diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index fb6d435787cae..67a6ff3dfedb1 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -88,6 +88,70 @@ void dcp_trace_panel_property(struct apple_dcp *dcp, int tag, const char *key, dev_info(dcp->dev, "panel identity inspection stopped: %d\n", ret); } +/* Other property setters use fixed scalar records, not serialization. */ +static void dcp_trace_panel_setter(struct apple_dcp *dcp, int tag, + const void *data, size_t len) +{ + struct dcp_identity_trace trace = { .dcp = dcp }; + const char *in = data; + u32 offset = 0, type, size, expected; + bool is_null = false; + + if (!drm_debug_enabled(DRM_UT_KMS) || + dcp->fw_compat != DCP_FIRMWARE_V_26_6_DESKTOP || !dcp_has_panel(dcp)) + return; + switch (tag) { + case 414: + offset = 4; + fallthrough; + case 563: + type = 4; + size = 8; + expected = offset + 76; + break; + case 415: + offset = 4; + fallthrough; + case 565: + type = 11; + size = 4; + expected = offset + 72; + break; + case 567: + type = 9; + size = 64; + expected = 128; + break; + case 102: + type = 4; + size = 4; + expected = 68; + break; + case 104: + type = 11; + size = 1; + expected = 68; + break; + default: + return; + } + /* Inspect only the public wire shape, without changing its handler. */ + if (len != expected) + return; + if (tag == 414 || tag == 415 || tag == 563 || tag == 565) + is_null = in[offset + 64 + size] & 1; + if (tag == 567) + size = strnlen(in + 64, 64); + /* Scalar/service setters share the bounded miscellaneous-property budget. */ + trace.count = &dcp->desktop->identity_log_count[3]; + if (!atomic_add_unless(trace.count, 1, 128)) + return; + dev_info(dcp->dev, "panel identity callback=D%03d bytes=%zu null=%d\n", + tag, len, is_null); + dcp_identity_key_log(&trace, in + offset, strnlen(in + offset, 64), + type, is_null ? 0 : size, 0); +} + static int dcp_tx_offset(enum dcp_context_id id) { switch (id) { @@ -1379,6 +1443,8 @@ static void dcpep_handle_cb(struct apple_dcp *dcp, enum dcp_context_id context, in = data + sizeof(*hdr); out = in + hdr->in_len; + dcp_trace_panel_setter(dcp, tag, in, hdr->in_len); + if (!handled) { zero_reply = dcp_external_zero_reply(dcp, tag, context, hdr->in_len, hdr->out_len); diff --git a/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c b/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c index b9fba6f5656d1..de8783f3a3694 100644 --- a/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c +++ b/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c @@ -100,7 +100,8 @@ static bool trampoline_passthrough_dict_v26_6(struct apple_dcp *dcp, int tag, static bool trampoline_panel_dictionary_v26_6(struct apple_dcp *dcp, int tag, void *out, void *in) { - const struct dcp_dict_passthrough_req_v26_6 *req = in; + /* Service dictionary setters prefix the same record with a handle. */ + const struct dcp_dict_passthrough_req_v26_6 *req = tag == 413 ? in + 4 : in; const char *key = (const char *)req->key; struct dcp_display_metadata metadata; struct dcp_parse_ctx ctx; @@ -144,6 +145,8 @@ bool iomfb_check_callback_v26_6_desktop(int tag, u32 in_len, u32 out_len) return in_len == 24 && out_len == 20; case 454: return in_len == 4 && out_len == 4; + case 413: + return in_len == 4168 && (out_len == 1 || out_len == 4); case 552: case 561: return in_len == 4164 && (out_len == 1 || out_len == 4); @@ -235,7 +238,7 @@ static const iomfb_cb_handler cb_handlers[IOMFB_MAX_CB] = { [406] = trampoline_nop, /* set_fx_prop: a property notification */ [408] = trampoline_get_frequency, [411] = trampoline_map_reg, - [413] = trampoline_true, /* sr_set_property_dict */ + [413] = trampoline_panel_dictionary_v26_6, /* sr_set_property_dict */ [414] = trampoline_sr_set_property_int, [415] = trampoline_true, /* sr_set_property_bool */ [451] = trampoline_allocate_buffer, diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py index b7125295bcc9d..c5e655ee46da7 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py @@ -391,7 +391,7 @@ def panel_identity(root, output=None): dictionary += block(backend, 'static bool trampoline_panel_dictionary_v26_6(') dictionary += block(backend, 'bool iomfb_check_callback_v26_6_desktop(') bindings = '\n'.join(line for line in block(backend, 'static const iomfb_cb_handler cb_handlers[').splitlines() - if '[552]' in line or '[561]' in line) + if '[413]' in line or '[552]' in line or '[561]' in line) unit = code + r''' #define DCP_FIRMWARE_V_26_6_DESKTOP 5 #define trace_iomfb_callback(...) ((void)0) @@ -453,6 +453,10 @@ def panel_identity(root, output=None): assert(handlers[tag](&d,tag,short_reply,&wire)); assert(short_reply[0]==1 && short_reply[1]==0xa5); assert(handlers[tag](&d,tag,&result,&wire) && result==1); + u8 service[sizeof(wire)+4]={0};memcpy(service+4,&wire,sizeof(wire)); + assert(iomfb_check_callback_v26_6_desktop(413,sizeof(service),4)); + assert(!iomfb_check_callback_v26_6_desktop(413,sizeof(service)-1,4)); + assert(handlers[413](&d,413,&result,service) && result==1); assert(desktop.panel_metadata_valid && notifications==1); assert(desktop.panel_metadata.serial==(form==2?0:metadata.serial)); assert(!strcmp(desktop.panel_metadata.manufacturer,"TST")); @@ -865,6 +869,8 @@ def identity_trace_budget(root): struct apple_dcp {int fw_compat;bool has_panel;void *dev;struct {atomic_t identity_log_count[4];} *desktop;}; static bool kms_debug; #define DRM_UT_KMS 4 +#define fallthrough __attribute__((fallthrough)) +typedef uint8_t u8; static bool drm_debug_enabled(int category) {assert(category==DRM_UT_KMS);return kms_debug;} static unsigned messages; static char output[256]; @@ -884,6 +890,7 @@ def identity_trace_budget(root): code+=block(source,'struct dcp_identity_trace {')+';\n' code+=block(source,'static void dcp_identity_key_log(') code+=block(source,'void dcp_trace_panel_property(') + code+=block(source,'static void dcp_trace_panel_setter(') return code+r''' int main(void) { @@ -891,11 +898,24 @@ def identity_trace_budget(root): __typeof__(*d.desktop) desktop={0};d.desktop=&desktop; const char secret[]="SYNTHETIC_VALUE_NOT_FOR_LOGS"; dcp_trace_panel_property(&d,561,"Property",8,secret,sizeof(secret));assert(!messages); + dcp_trace_panel_setter(&d,563,NULL,76);assert(!messages); kms_debug=true;d.fw_compat=1; dcp_trace_panel_property(&d,561,"Property",8,secret,sizeof(secret));assert(!messages); d.fw_compat=5;d.has_panel=false; dcp_trace_panel_property(&d,561,"Property",8,secret,sizeof(secret));assert(!messages); d.has_panel=true; + u8 setter[4168]={0};strcpy((char *)setter,"ScalarKey"); + memcpy(setter+64,secret,sizeof(secret));setter[72]=0; + dcp_trace_panel_setter(&d,563,setter,75);assert(!messages); + dcp_trace_panel_setter(&d,563,setter,76); + assert(strstr(output,"ScalarKey type=4 size=8") && !strstr(output,secret)); + dcp_trace_panel_setter(&d,565,setter,72);assert(strstr(output,"type=11")); + dcp_trace_panel_setter(&d,567,setter,128);assert(strstr(output,"type=9")); + setter[72]=1;dcp_trace_panel_setter(&d,563,setter,76);assert(strstr(output,"size=0")); + memset(setter,0,sizeof(setter));strcpy((char *)setter+4,"ServiceKey"); + dcp_trace_panel_setter(&d,414,setter,80);assert(strstr(output,"ServiceKey type=4")); + dcp_trace_panel_setter(&d,415,setter,76);assert(strstr(output,"ServiceKey type=11")); + dcp_trace_panel_setter(&d,102,setter,68);dcp_trace_panel_setter(&d,104,setter,68); for(unsigned i=0;i<100;i++)dcp_trace_panel_property(&d,114,"Statistics",10,secret,sizeof(secret)); assert(atomic_read(&desktop.identity_log_count[2])==128 && !atomic_read(&desktop.identity_log_count[0]) && !atomic_read(&desktop.identity_log_count[1])); unsigned before=messages; diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py index cc82ade333fb3..9569b89844fb6 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py @@ -1850,6 +1850,7 @@ def dispatcher(root): #define IOMFB_MAX_CB 600 #define DCP_PACKET_ALIGNMENT 64 #define ALIGN(n, a) (((n) + (a) - 1) & ~((a) - 1)) +#define dcp_trace_panel_setter(...) ((void)0) #define dev_err(...) ((void)dev) #define dev_info(...) ((void)dev) #define dev_warn(...) ((void)dev) From 78367b8499e0d69fff7eaa5111071bff43631151 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 25 Oct 2026 22:03:08 -0400 Subject: [PATCH 152/388] soc: apple: clpc: hold the T8140 GPU floor only while GPU work streams The T8140 GPU floor was held whenever graphics was powered. On J700 that costs 0.38 W (about 8%) during 1080p video playback in Firefox and 0.16 W during light browsing, where the floor gains nothing. The firmware powers graphics off a few milliseconds after the GPU idles. Sampled every millisecond, 1080p video powered graphics for 56% of the time and light browsing for 34%, but never for more than 63 ms at a time, while light compute and render submission streams kept it powered throughout. Count graphics as GPU demand only once it has stayed powered for 64 ms, with no sample finding it off and no gap in sampling long enough to have missed that, and sample on time while it is powered. A failed read still counts as powered. The debugfs status shows how long graphics has been powered. --- drivers/soc/apple/clpc.c | 65 ++++++++++++++++++++++++++++++---------- 1 file changed, 50 insertions(+), 15 deletions(-) diff --git a/drivers/soc/apple/clpc.c b/drivers/soc/apple/clpc.c index cc7a620c1c772..eb8dc72309fd7 100644 --- a/drivers/soc/apple/clpc.c +++ b/drivers/soc/apple/clpc.c @@ -48,6 +48,8 @@ enum apple_clpc_rail { #define APPLE_CLPC_BUSY_PCT 50 /* Time a rail keeps its last request after demand stops */ #define APPLE_CLPC_HOLD_MS 32 +/* Time graphics stays powered without a break before it counts as GPU demand */ +#define APPLE_CLPC_GPU_SUSTAIN_MS 64 /** * struct apple_clpc_hw - per-SoC CLPC parameters @@ -101,6 +103,10 @@ struct apple_clpc { struct dentry *debugfs; struct regmap *gpu_pmgr; u32 gpu_offset; + /* Graphics powered at the last sample, since when, and that sample's time */ + bool gpu_on; + unsigned long gpu_on_since; + unsigned long gpu_sampled; }; /* cpufreq policies can appear after probe; pick up new ones as they do. */ @@ -227,6 +233,36 @@ static void apple_clpc_queue(struct apple_clpc *clpc, bool idle) msecs_to_jiffies(APPLE_CLPC_SAMPLE_MS)); } +/* + * Whether the GPU streams work: graphics has stayed powered for + * APPLE_CLPC_GPU_SUSTAIN_MS, with no sample in between that found it off and + * no gap in sampling long enough to have missed that. The firmware powers + * graphics off a few milliseconds after the GPU idles, so a frame-paced load + * such as video playback powers it up and down with each frame and does not + * count. A failed read counts as powered, so that it cannot starve a running + * graphics consumer. + */ +static bool apple_clpc_gpu_streaming(struct apple_clpc *clpc, unsigned long now) +{ + bool gap = time_after(now, clpc->gpu_sampled + + msecs_to_jiffies(2 * APPLE_CLPC_SAMPLE_MS)); + u32 status; + int err; + + clpc->gpu_sampled = now; + err = regmap_read(clpc->gpu_pmgr, clpc->gpu_offset, &status); + if (!err && (status & GENMASK(7, 4)) != GENMASK(7, 4)) { + clpc->gpu_on = false; + return false; + } + + if (!clpc->gpu_on || gap) + clpc->gpu_on_since = now; + clpc->gpu_on = true; + return time_after_eq(now, clpc->gpu_on_since + + msecs_to_jiffies(APPLE_CLPC_GPU_SUSTAIN_MS)); +} + static void apple_clpc_sample(struct apple_clpc *clpc) { unsigned int demand[APPLE_CLPC_RAILS] = {}; @@ -248,17 +284,9 @@ static void apple_clpc_sample(struct apple_clpc *clpc) for (rail = 0; rail < APPLE_CLPC_RAILS; rail++) demand[rail] = max(demand[rail], cl->request[rail]); } - if (clpc->gpu_pmgr) { - u32 status; - - /* Keep the qualified floor while graphics is powered. A failed - * read must not silently starve a running graphics consumer. - */ - if (regmap_read(clpc->gpu_pmgr, clpc->gpu_offset, &status) || - (status & GENMASK(7, 4)) == GENMASK(7, 4)) - for (rail = 0; rail < APPLE_CLPC_RAILS; rail++) - demand[rail] = clpc->hw->map[rail][APPLE_CLPC_LEVELS - 1]; - } + if (clpc->gpu_pmgr && apple_clpc_gpu_streaming(clpc, now)) + for (rail = 0; rail < APPLE_CLPC_RAILS; rail++) + demand[rail] = clpc->hw->map[rail][APPLE_CLPC_LEVELS - 1]; for (rail = 0; rail < APPLE_CLPC_RAILS; rail++) state[rail] = apple_clpc_vote(&clpc->vote[rail], demand[rail], @@ -268,7 +296,8 @@ static void apple_clpc_sample(struct apple_clpc *clpc) apple_clpc_floor(clpc, state))) clpc->update_errors++; - apple_clpc_queue(clpc, !apple_clpc_floor(clpc, state)); + /* Sample on time while graphics is powered, to see it power off. */ + apple_clpc_queue(clpc, !apple_clpc_floor(clpc, state) && !clpc->gpu_on); } static void apple_clpc_work(struct work_struct *work) @@ -301,6 +330,9 @@ static int apple_clpc_status_show(struct seq_file *s, void *unused) clpc->hw->map[rail][2], clpc->hw->map[rail][3]); seq_printf(s, "samples: %lu update errors: %lu\n", clpc->samples, clpc->update_errors); + if (clpc->gpu_pmgr) + seq_printf(s, "gpu: powered %u for %u ms\n", clpc->gpu_on, + clpc->gpu_on ? jiffies_to_msecs(jiffies - clpc->gpu_on_since) : 0); for (i = 0; i < clpc->nr_clusters; i++) seq_printf(s, "cluster cpu%u: busy %u%% level %u%% -> dcs %u soc %u\n", clpc->clusters[i].policy_cpu, clpc->clusters[i].busy, @@ -483,9 +515,12 @@ static const struct apple_clpc_hw apple_clpc_hw_t8132 = { * and completes every command over the fabric. With the fabric left at * state 0, light GPU submissions take 30-50% longer than at state 3, and * macOS holds the fabric at its top states while such work streams. The - * graphics power state is on only while the GPU has work (the firmware - * powers it off when idle), so the floor follows GPU activity and is - * released HOLD_MS after it stops. + * firmware powers graphics off a few milliseconds after the GPU idles, so + * graphics that stays powered for GPU_SUSTAIN_MS marks such a stream. Loads + * paced by frames power it up and down with each frame instead: on J700, + * 1080p video playback and light browsing keep it powered for at most 63 ms + * at a time, and leave the floor down. The floor is released HOLD_MS after + * graphics powers off. */ static const struct apple_clpc_hw apple_clpc_hw_t8140 = { .rail = { [APPLE_CLPC_DCS] = 0, [APPLE_CLPC_SOC] = 1 }, From c7c0bc5cb34bbe2c67b7f064f3255416fcaab70a Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 25 Oct 2026 22:07:24 -0400 Subject: [PATCH 153/388] soc: apple: clpc: release the T8140 GPU floor while the PMP limits the DCS Under sustained heavy rendering on J700, the GPU floor ended 1.8% slower and 1.3 C hotter than no floor over 90 s, for 0.14 W more. The PMP, voter agent 0, votes the DCS to its top state for streams of light GPU submissions: it did for every millisecond sample of light compute and render streams. Under heavy rendering it lowers the DCS, to state 3 for 62% of the samples, to stay within the GPU's power budget. The floor then only overrides that and takes headroom from the GPU. macOS also lets the fabric drop for heavy GPU work. While GPU work streams, read the PMP's floor word. When it votes the DCS below the GPU floor's state, release the GPU floor and keep it released for 1 s after the PMP last did so. The debugfs status shows when that was. Measured on J700 with both changes, one boot each on battery, against no GPU floor and the floor held whenever graphics is powered: no floor powered this 1080p video, W 4.903 5.151 4.975 light browsing, W 3.971 4.085 3.970 render.heavy d4 at 90 s, us 659.5 671.1 656.8 compute.light d1, us 373.7 320.5 319.3 render.light d2, us 87.2 65.6 66.6 --- drivers/soc/apple/clpc.c | 52 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/drivers/soc/apple/clpc.c b/drivers/soc/apple/clpc.c index eb8dc72309fd7..c2f45979dc485 100644 --- a/drivers/soc/apple/clpc.c +++ b/drivers/soc/apple/clpc.c @@ -50,6 +50,8 @@ enum apple_clpc_rail { #define APPLE_CLPC_HOLD_MS 32 /* Time graphics stays powered without a break before it counts as GPU demand */ #define APPLE_CLPC_GPU_SUSTAIN_MS 64 +/* Time the GPU floor stays released after the PMP last limited the DCS */ +#define APPLE_CLPC_GPU_BACKOFF_MS 1000 /** * struct apple_clpc_hw - per-SoC CLPC parameters @@ -58,12 +60,17 @@ enum apple_clpc_rail { * 0 or 1 for the first * @map: state of each rail for a busy cluster below 25, 50, 75 and 100 * percent of its maximum frequency + * @gpu_demand: streaming GPU work requests both rails' top states + * @pmp_backoff: release that GPU floor while the PMP, voter agent + * @pmp_agent, limits the DCS, see apple_clpc_gpu_limited() */ struct apple_clpc_hw { unsigned int rail[APPLE_CLPC_RAILS]; unsigned int sustain; u8 map[APPLE_CLPC_RAILS][APPLE_CLPC_LEVELS]; bool gpu_demand; + bool pmp_backoff; + unsigned int pmp_agent; }; struct apple_clpc_cluster { @@ -107,6 +114,9 @@ struct apple_clpc { bool gpu_on; unsigned long gpu_on_since; unsigned long gpu_sampled; + /* Whether, and when, the PMP last limited the DCS while GPU work streamed */ + bool gpu_limited; + unsigned long gpu_limited_at; }; /* cpufreq policies can appear after probe; pick up new ones as they do. */ @@ -263,6 +273,32 @@ static bool apple_clpc_gpu_streaming(struct apple_clpc *clpc, unsigned long now) msecs_to_jiffies(APPLE_CLPC_GPU_SUSTAIN_MS)); } +/* + * Whether the PMP holds the GPU to a power budget: while GPU work streams, it + * votes the DCS below the top state, which it does under sustained heavy + * rendering but not for streams of light submissions. The GPU floor would + * then only take power and thermal headroom from the GPU, so it stays + * released for GPU_BACKOFF_MS after the PMP last did so. + */ +static bool apple_clpc_gpu_limited(struct apple_clpc *clpc, unsigned long now) +{ + unsigned int rail = clpc->hw->rail[APPLE_CLPC_DCS]; + unsigned int dcs; + + if (!clpc->hw->pmp_backoff) + return false; + + dcs = (apple_pmc_floor_read(clpc->pmc, clpc->hw->pmp_agent) >> + APPLE_PMC_RAIL_SHIFT(rail)) & 0xf; + if (dcs < clpc->hw->map[APPLE_CLPC_DCS][APPLE_CLPC_LEVELS - 1]) { + clpc->gpu_limited = true; + clpc->gpu_limited_at = now; + } + return clpc->gpu_limited && + time_before(now, clpc->gpu_limited_at + + msecs_to_jiffies(APPLE_CLPC_GPU_BACKOFF_MS)); +} + static void apple_clpc_sample(struct apple_clpc *clpc) { unsigned int demand[APPLE_CLPC_RAILS] = {}; @@ -284,7 +320,8 @@ static void apple_clpc_sample(struct apple_clpc *clpc) for (rail = 0; rail < APPLE_CLPC_RAILS; rail++) demand[rail] = max(demand[rail], cl->request[rail]); } - if (clpc->gpu_pmgr && apple_clpc_gpu_streaming(clpc, now)) + if (clpc->gpu_pmgr && apple_clpc_gpu_streaming(clpc, now) && + !apple_clpc_gpu_limited(clpc, now)) for (rail = 0; rail < APPLE_CLPC_RAILS; rail++) demand[rail] = clpc->hw->map[rail][APPLE_CLPC_LEVELS - 1]; @@ -331,8 +368,11 @@ static int apple_clpc_status_show(struct seq_file *s, void *unused) seq_printf(s, "samples: %lu update errors: %lu\n", clpc->samples, clpc->update_errors); if (clpc->gpu_pmgr) - seq_printf(s, "gpu: powered %u for %u ms\n", clpc->gpu_on, - clpc->gpu_on ? jiffies_to_msecs(jiffies - clpc->gpu_on_since) : 0); + seq_printf(s, "gpu: powered %u for %u ms, pmp limited %u ms ago\n", + clpc->gpu_on, + clpc->gpu_on ? jiffies_to_msecs(jiffies - clpc->gpu_on_since) : 0, + clpc->gpu_limited ? + jiffies_to_msecs(jiffies - clpc->gpu_limited_at) : 0); for (i = 0; i < clpc->nr_clusters; i++) seq_printf(s, "cluster cpu%u: busy %u%% level %u%% -> dcs %u soc %u\n", clpc->clusters[i].policy_cpu, clpc->clusters[i].busy, @@ -521,6 +561,10 @@ static const struct apple_clpc_hw apple_clpc_hw_t8132 = { * 1080p video playback and light browsing keep it powered for at most 63 ms * at a time, and leave the floor down. The floor is released HOLD_MS after * graphics powers off. + * + * The PMP (agent 0) votes the DCS to its top state for streams of light GPU + * submissions, and lowers it under sustained heavy rendering, which needs + * neither rail's floor; see apple_clpc_gpu_limited(). */ static const struct apple_clpc_hw apple_clpc_hw_t8140 = { .rail = { [APPLE_CLPC_DCS] = 0, [APPLE_CLPC_SOC] = 1 }, @@ -529,6 +573,8 @@ static const struct apple_clpc_hw apple_clpc_hw_t8140 = { [APPLE_CLPC_SOC] = { 0, 0, 0, 3 }, }, .gpu_demand = true, + .pmp_backoff = true, + .pmp_agent = 0, }; /* From e4a518ae0e28c8a227397485fe19b573bdc3a013 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 14:13:12 +1100 Subject: [PATCH 154/388] arm64: configs: trim redundant Asahi requests and enable shared backends Resolve each candidate removal over both arm64 defconfig and the Fedora Asahi base. The 36 removed requests cause no resolved configuration changes on either recipe: the bases or the remaining selections retain those drivers and capabilities. Keep the default thermal governor available over the Fedora base by enabling ENERGY_MODEL and THERMAL_GOV_POWER_ALLOCATOR, as identified in #438. Request 48-bit VA and mixed-granule userspace as in #394, and build the native G15 backend alongside the other supported GPUs. Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/configs/asahi.config | 41 ++++----------------------------- 1 file changed, 5 insertions(+), 36 deletions(-) diff --git a/arch/arm64/configs/asahi.config b/arch/arm64/configs/asahi.config index 783e41230238c..d07b70afd9294 100644 --- a/arch/arm64/configs/asahi.config +++ b/arch/arm64/configs/asahi.config @@ -3,40 +3,28 @@ # olddefconfig and verify that the requested settings survive resolution. # Device-specific boot, debug and distribution policy stays in the base. CONFIG_RUST=y -CONFIG_ARM64_ACTLR_STATE=y -CONFIG_ARCH_APPLE=y -# CONFIG_ARM64_4K_PAGES is not set CONFIG_ARM64_16K_PAGES=y -# CONFIG_ARM64_64K_PAGES is not set +CONFIG_ARM64_VA_BITS_48=y +CONFIG_ARM64_USER4K_EXPERIMENTAL=y CONFIG_ARM64_MEMORY_MODEL_CONTROL=y CONFIG_ARM_APPLE_CPUIDLE=y -CONFIG_ARM_APPLE_SOC_CPUFREQ=m +CONFIG_ENERGY_MODEL=y +CONFIG_THERMAL_GOV_POWER_ALLOCATOR=y CONFIG_THERMAL_DEFAULT_GOV_POWER_ALLOCATOR=y CONFIG_BT_BRCMEXT=y CONFIG_BT_HCIBCM4377=m CONFIG_BT_MTK7932_PCIE=m CONFIG_PCIE_APPLE=y -CONFIG_NVME_APPLE=m CONFIG_MMC_SDHCI_PCI=m -CONFIG_BRCMFMAC=m CONFIG_BRCMFMAC_PCIE=y CONFIG_PCIE_APPLE_PIODMA_DIAG=y CONFIG_PCIEASPM=y CONFIG_NET_VENDOR_INTEL=y -CONFIG_IGB=m CONFIG_IGC=m CONFIG_MT7932_FULLMAC=m -CONFIG_TOUCHSCREEN_APPLE_Z2=m CONFIG_INPUT_MACSMC_INPUT=m -CONFIG_I2C_APPLE=m -CONFIG_SPI_APPLE=m CONFIG_SPI_APPLE_QSPI=m -CONFIG_SPMI_APPLE=m -CONFIG_PINCTRL_APPLE_GPIO=m -CONFIG_GPIO_MACSMC=m -CONFIG_POWER_RESET_MACSMC=m CONFIG_MACSMC_POWER=m -CONFIG_SENSORS_MACSMC_HWMON=m CONFIG_SENSORS_APPLE_PMP_J613=m CONFIG_THERMAL_NETLINK=y CONFIG_NVME_HWMON=y @@ -45,6 +33,7 @@ CONFIG_VIDEO_APPLE_AVD=m CONFIG_VIDEO_APPLE_ISP=m CONFIG_DRM=y CONFIG_DRM_ASAHI=m +CONFIG_DRM_ASAHI_G15=y CONFIG_DRM_ASAHI_G16=y CONFIG_DRM_ADP=m CONFIG_DRM_APPLE=m @@ -56,7 +45,6 @@ CONFIG_DRM_SIMPLEDRM=y CONFIG_SYSFB_SIMPLEFB=y CONFIG_SND_SOC_APPLE_AOP_AUDIO=m CONFIG_SND_SOC_APPLE_T8140_AOP_AUDIO=m -CONFIG_SND_SOC_APPLE_MCA=m CONFIG_SND_SOC_APPLE_MACAUDIO=m CONFIG_SND_SOC_CS42L83=m CONFIG_SND_SOC_CS42L84=m @@ -66,24 +54,16 @@ CONFIG_SND_SOC_TAS2770=m CONFIG_HID_APPLE=m CONFIG_HID_MAGICMOUSE=m CONFIG_HID_APPLE_MTP_HAPTIC=m -CONFIG_SERIAL_SAMSUNG=y -CONFIG_SERIAL_SAMSUNG_CONSOLE=y CONFIG_HID_DOCKCHANNEL=m CONFIG_SPI_HID_APPLE_OF=m -CONFIG_SPI_HID_APPLE_CORE=m CONFIG_USB_DWC3_APPLE=m -CONFIG_TYPEC=m CONFIG_USB4=m CONFIG_USB4_APPLE_SOC=m CONFIG_USB_XHCI_PCI_ASMEDIA=y CONFIG_TYPEC_SN201202X=m -CONFIG_RTC_DRV_MACSMC=m CONFIG_RTC_DRV_APPLE_PMU=m -CONFIG_APPLE_ADMAC=m CONFIG_APPLE_SIO=m CONFIG_APPLE_SIO_T6030=y -CONFIG_MFD_MACSMC=m -CONFIG_COMMON_CLK_APPLE_NCO=m CONFIG_APPLE_DART=y CONFIG_APPLE_DART_J713_DCP=y CONFIG_APPLE_DOCKCHANNEL=m @@ -93,10 +73,7 @@ CONFIG_APPLE_MAILBOX=y CONFIG_APPLE_PMGR_MISC=y CONFIG_APPLE_RTKIT=y CONFIG_APPLE_RTKIT_HELPER=m -CONFIG_APPLE_SART=m -CONFIG_RUST_APPLE_RTKIT=y CONFIG_APPLE_AOP=m -CONFIG_HW_RANDOM=y CONFIG_KEYS=y CONFIG_TRUSTED_KEYS=y # CONFIG_TRUSTED_KEYS_TPM is not set @@ -104,28 +81,20 @@ CONFIG_TRUSTED_KEYS=y CONFIG_APPLE_SEP=m CONFIG_APPLE_PMGR_PWRSTATE=y CONFIG_APPLE_PMP=m -CONFIG_APPLE_PMP_REPORT=y CONFIG_APPLE_PMP_THERMAL=m CONFIG_APPLE_T8132_PLATFORM_HANDOFF=y CONFIG_IIO_AOP_SENSOR_LAS=m CONFIG_IIO_AOP_SENSOR_ALS=m CONFIG_RUST_FW_LOADER_ABSTRACTIONS=y -CONFIG_PWM_APPLE=m -CONFIG_APPLE_AIC=y CONFIG_PHY_APPLE_ATC=m CONFIG_PHY_APPLE_T6040_USB2=m CONFIG_PHY_APPLE_DPTX=m -CONFIG_APPLE_M1_CPU_PMU=y -CONFIG_NVMEM_APPLE_EFUSES=m -CONFIG_NVMEM_APPLE_SPMI=m CONFIG_MUX_APPLE_DPXBAR=m -CONFIG_TYPEC_TPS6598X=m CONFIG_RESET_APPLE_CIO=y CONFIG_INFINIBAND=m CONFIG_INFINIBAND_USER_ACCESS=m CONFIG_INFINIBAND_USER_MAD=m CONFIG_RDMA_RXE=m CONFIG_RDMA_SIW=m -CONFIG_DMABUF_HEAPS=y CONFIG_DMABUF_HEAPS_SYSTEM=y CONFIG_UDMABUF=y From eb4b4269f3c8b1c8c2c070d83f22b36a9b0b7059 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 14:13:12 +1100 Subject: [PATCH 155/388] selftests: apple-dcp: initialise the probe retry budget fixture The probe fixture calls cd321x_pm_init() on an uninitialised stack object. That helper sets the initial phase and link-event budget but expects the other fields to have the zeroed allocation used by the real driver. The fixture can read arbitrary setup_pending values and fail its retry assertions under UBSan. Start it from the same zeroed state as the other fixtures. Assisted-by: LLM Signed-off-by: Ryan Murray --- tools/testing/selftests/drivers/gpu/apple-dcp/pm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py b/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py index d5b3890d30861..664ba046dad35 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py @@ -547,7 +547,7 @@ def function(text, name): int main(void) { (void)cd321x_pm_notifications; struct cd321x c; - struct cd321x_pm_state budget; + struct cd321x_pm_state budget={0}; cd321x_pm_init(&budget);assert(cd321x_pm_ready(&budget)); assert(cd321x_display_taken(-ENOSPC,true)&&!cd321x_display_taken(-EBUSY,true)&&!cd321x_display_taken(0,true)&&cd321x_display_taken(-EBUSY,false)&&!cd321x_display_taken(-ENOSPC,false)&&!cd321x_display_taken(0,false)); for(unsigned int i=0;i Date: Mon, 26 Oct 2026 14:13:12 +1100 Subject: [PATCH 156/388] arm64: configs: trim redundant Asahi requests and enable shared backends Resolve each candidate removal over both arm64 defconfig and the Fedora Asahi base. The 36 removed requests cause no resolved configuration changes on either recipe: the bases or the remaining selections retain those drivers and capabilities. Keep the default thermal governor available over the Fedora base by enabling ENERGY_MODEL and THERMAL_GOV_POWER_ALLOCATOR, as identified in #438. Request 48-bit VA and mixed-granule userspace as in #394, and build the native G15 backend alongside the other supported GPUs. Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/configs/asahi.config | 41 ++++----------------------------- 1 file changed, 5 insertions(+), 36 deletions(-) diff --git a/arch/arm64/configs/asahi.config b/arch/arm64/configs/asahi.config index 783e41230238c..d07b70afd9294 100644 --- a/arch/arm64/configs/asahi.config +++ b/arch/arm64/configs/asahi.config @@ -3,40 +3,28 @@ # olddefconfig and verify that the requested settings survive resolution. # Device-specific boot, debug and distribution policy stays in the base. CONFIG_RUST=y -CONFIG_ARM64_ACTLR_STATE=y -CONFIG_ARCH_APPLE=y -# CONFIG_ARM64_4K_PAGES is not set CONFIG_ARM64_16K_PAGES=y -# CONFIG_ARM64_64K_PAGES is not set +CONFIG_ARM64_VA_BITS_48=y +CONFIG_ARM64_USER4K_EXPERIMENTAL=y CONFIG_ARM64_MEMORY_MODEL_CONTROL=y CONFIG_ARM_APPLE_CPUIDLE=y -CONFIG_ARM_APPLE_SOC_CPUFREQ=m +CONFIG_ENERGY_MODEL=y +CONFIG_THERMAL_GOV_POWER_ALLOCATOR=y CONFIG_THERMAL_DEFAULT_GOV_POWER_ALLOCATOR=y CONFIG_BT_BRCMEXT=y CONFIG_BT_HCIBCM4377=m CONFIG_BT_MTK7932_PCIE=m CONFIG_PCIE_APPLE=y -CONFIG_NVME_APPLE=m CONFIG_MMC_SDHCI_PCI=m -CONFIG_BRCMFMAC=m CONFIG_BRCMFMAC_PCIE=y CONFIG_PCIE_APPLE_PIODMA_DIAG=y CONFIG_PCIEASPM=y CONFIG_NET_VENDOR_INTEL=y -CONFIG_IGB=m CONFIG_IGC=m CONFIG_MT7932_FULLMAC=m -CONFIG_TOUCHSCREEN_APPLE_Z2=m CONFIG_INPUT_MACSMC_INPUT=m -CONFIG_I2C_APPLE=m -CONFIG_SPI_APPLE=m CONFIG_SPI_APPLE_QSPI=m -CONFIG_SPMI_APPLE=m -CONFIG_PINCTRL_APPLE_GPIO=m -CONFIG_GPIO_MACSMC=m -CONFIG_POWER_RESET_MACSMC=m CONFIG_MACSMC_POWER=m -CONFIG_SENSORS_MACSMC_HWMON=m CONFIG_SENSORS_APPLE_PMP_J613=m CONFIG_THERMAL_NETLINK=y CONFIG_NVME_HWMON=y @@ -45,6 +33,7 @@ CONFIG_VIDEO_APPLE_AVD=m CONFIG_VIDEO_APPLE_ISP=m CONFIG_DRM=y CONFIG_DRM_ASAHI=m +CONFIG_DRM_ASAHI_G15=y CONFIG_DRM_ASAHI_G16=y CONFIG_DRM_ADP=m CONFIG_DRM_APPLE=m @@ -56,7 +45,6 @@ CONFIG_DRM_SIMPLEDRM=y CONFIG_SYSFB_SIMPLEFB=y CONFIG_SND_SOC_APPLE_AOP_AUDIO=m CONFIG_SND_SOC_APPLE_T8140_AOP_AUDIO=m -CONFIG_SND_SOC_APPLE_MCA=m CONFIG_SND_SOC_APPLE_MACAUDIO=m CONFIG_SND_SOC_CS42L83=m CONFIG_SND_SOC_CS42L84=m @@ -66,24 +54,16 @@ CONFIG_SND_SOC_TAS2770=m CONFIG_HID_APPLE=m CONFIG_HID_MAGICMOUSE=m CONFIG_HID_APPLE_MTP_HAPTIC=m -CONFIG_SERIAL_SAMSUNG=y -CONFIG_SERIAL_SAMSUNG_CONSOLE=y CONFIG_HID_DOCKCHANNEL=m CONFIG_SPI_HID_APPLE_OF=m -CONFIG_SPI_HID_APPLE_CORE=m CONFIG_USB_DWC3_APPLE=m -CONFIG_TYPEC=m CONFIG_USB4=m CONFIG_USB4_APPLE_SOC=m CONFIG_USB_XHCI_PCI_ASMEDIA=y CONFIG_TYPEC_SN201202X=m -CONFIG_RTC_DRV_MACSMC=m CONFIG_RTC_DRV_APPLE_PMU=m -CONFIG_APPLE_ADMAC=m CONFIG_APPLE_SIO=m CONFIG_APPLE_SIO_T6030=y -CONFIG_MFD_MACSMC=m -CONFIG_COMMON_CLK_APPLE_NCO=m CONFIG_APPLE_DART=y CONFIG_APPLE_DART_J713_DCP=y CONFIG_APPLE_DOCKCHANNEL=m @@ -93,10 +73,7 @@ CONFIG_APPLE_MAILBOX=y CONFIG_APPLE_PMGR_MISC=y CONFIG_APPLE_RTKIT=y CONFIG_APPLE_RTKIT_HELPER=m -CONFIG_APPLE_SART=m -CONFIG_RUST_APPLE_RTKIT=y CONFIG_APPLE_AOP=m -CONFIG_HW_RANDOM=y CONFIG_KEYS=y CONFIG_TRUSTED_KEYS=y # CONFIG_TRUSTED_KEYS_TPM is not set @@ -104,28 +81,20 @@ CONFIG_TRUSTED_KEYS=y CONFIG_APPLE_SEP=m CONFIG_APPLE_PMGR_PWRSTATE=y CONFIG_APPLE_PMP=m -CONFIG_APPLE_PMP_REPORT=y CONFIG_APPLE_PMP_THERMAL=m CONFIG_APPLE_T8132_PLATFORM_HANDOFF=y CONFIG_IIO_AOP_SENSOR_LAS=m CONFIG_IIO_AOP_SENSOR_ALS=m CONFIG_RUST_FW_LOADER_ABSTRACTIONS=y -CONFIG_PWM_APPLE=m -CONFIG_APPLE_AIC=y CONFIG_PHY_APPLE_ATC=m CONFIG_PHY_APPLE_T6040_USB2=m CONFIG_PHY_APPLE_DPTX=m -CONFIG_APPLE_M1_CPU_PMU=y -CONFIG_NVMEM_APPLE_EFUSES=m -CONFIG_NVMEM_APPLE_SPMI=m CONFIG_MUX_APPLE_DPXBAR=m -CONFIG_TYPEC_TPS6598X=m CONFIG_RESET_APPLE_CIO=y CONFIG_INFINIBAND=m CONFIG_INFINIBAND_USER_ACCESS=m CONFIG_INFINIBAND_USER_MAD=m CONFIG_RDMA_RXE=m CONFIG_RDMA_SIW=m -CONFIG_DMABUF_HEAPS=y CONFIG_DMABUF_HEAPS_SYSTEM=y CONFIG_UDMABUF=y From 79c848b22719fe24ade98cf3091685a4a83e8c25 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 14:13:12 +1100 Subject: [PATCH 157/388] selftests: apple-dcp: initialise the probe retry budget fixture The probe fixture calls cd321x_pm_init() on an uninitialised stack object. That helper sets the initial phase and link-event budget but expects the other fields to have the zeroed allocation used by the real driver. The fixture can read arbitrary setup_pending values and fail its retry assertions under UBSan. Start it from the same zeroed state as the other fixtures. Assisted-by: LLM Signed-off-by: Ryan Murray --- tools/testing/selftests/drivers/gpu/apple-dcp/pm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py b/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py index 75fe10221fcc9..af3a590b375a4 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py @@ -547,7 +547,7 @@ def function(text, name): int main(void) { (void)cd321x_pm_notifications; struct cd321x c; - struct cd321x_pm_state budget; + struct cd321x_pm_state budget={0}; cd321x_pm_init(&budget);assert(cd321x_pm_ready(&budget)); assert(cd321x_display_taken(-ENOSPC,true)&&!cd321x_display_taken(-EBUSY,true)&&!cd321x_display_taken(0,true)&&cd321x_display_taken(-EBUSY,false)&&!cd321x_display_taken(-ENOSPC,false)&&!cd321x_display_taken(0,false)); for(unsigned int i=0;i Date: Mon, 26 Oct 2026 14:14:30 +1100 Subject: [PATCH 158/388] drm/apple: drop an unused probe switch and the M3 notch alias dcpext_probe has no consumer; the external display paths already use their compatible and firmware admission rules. Remove the unused registration. The generic show_notch preference already applies to the 14.x and 26.6 internal displays. Use it directly and remove t6030_show_notch, which only duplicated that choice for those firmware variants. Document the command-line migration while retaining the common notch preference and the matching m1n1 framebuffer preference. Assisted-by: LLM Signed-off-by: Ryan Murray --- Documentation/gpu/apple-drm-parameters.rst | 12 +++++++++++ Documentation/gpu/drivers.rst | 1 + drivers/gpu/drm/apple/dcp.c | 25 +--------------------- drivers/gpu/drm/apple/iomfb_v26_6.c | 2 +- 4 files changed, 15 insertions(+), 25 deletions(-) create mode 100644 Documentation/gpu/apple-drm-parameters.rst diff --git a/Documentation/gpu/apple-drm-parameters.rst b/Documentation/gpu/apple-drm-parameters.rst new file mode 100644 index 0000000000000..b1e8bcaa4d163 --- /dev/null +++ b/Documentation/gpu/apple-drm-parameters.rst @@ -0,0 +1,12 @@ +Apple display module parameters +=============================== + +Use ``appledrm.show_notch=1`` to include the notch rows in the panel mode. +This common preference also applies to the M3 internal display firmware; +it replaces the platform-specific ``appledrm.t6030_show_notch`` alias. +When the boot framebuffer should cover those rows too, keep the useful +``chosen.asahi,show-notch=1`` m1n1 display preference alongside it. + +The old ``appledrm.dcpext_probe`` parameter had no consumer and has been +removed. External display support follows the device tree and the driver's +compatible and firmware admission checks. diff --git a/Documentation/gpu/drivers.rst b/Documentation/gpu/drivers.rst index 8351e25ba8ef1..ced2721b96c75 100644 --- a/Documentation/gpu/drivers.rst +++ b/Documentation/gpu/drivers.rst @@ -7,6 +7,7 @@ GPU Driver Documentation amdgpu/index apple-dcp-backlight + apple-drm-parameters asahi-reclaim asahi-m4-bringup apple-dcp-m4 diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 8a2e2c5bcbf10..eb5108fe05a04 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -198,15 +198,6 @@ static bool show_notch; module_param(show_notch, bool, 0644); MODULE_PARM_DESC(show_notch, "Use the full display height and shows the notch"); -static bool dcpext_probe; -module_param(dcpext_probe, bool, 0444); -MODULE_PARM_DESC(dcpext_probe, "Opt into experimental T6030 external DCP probe (may hang)"); - -static bool t6030_show_notch; -module_param(t6030_show_notch, bool, 0444); -MODULE_PARM_DESC(t6030_show_notch, - "M3 internal display (14.x and 26.6 IOMFB): use the full height and show the notch"); - bool hdmi_audio; module_param(hdmi_audio, bool, 0644); MODULE_PARM_DESC(hdmi_audio, "Enable unstable HDMI audio support"); @@ -4510,20 +4501,6 @@ static bool dcp_external_is_idle(struct apple_dcp *dcp) (status & APPLE_DCP_COPROC_CPU_STATUS_IDLE); } -/* - * The M3 internal displays (14.x and 26.6 IOMFB) also accept - * appledrm.t6030_show_notch=1, the switch existing J613/T6030 installs pass, - * so the panel comes up at its full height (2560x1664 on the J613) instead - * of the module refusing an unknown parameter. - */ -static bool dcp_show_notch(struct apple_dcp *dcp) -{ - if (dcp->fw_compat != DCP_FIRMWARE_V_14_7 && - dcp->fw_compat != DCP_FIRMWARE_V_26_6) - return show_notch; - return show_notch || t6030_show_notch; -} - /* * T8122 dcpext (DCP_FIRMWARE_V_14_7_TEMPLATE): if the PMP runs, the display * gate added its DISPEXT request and pointed the dcpext at it @@ -4670,7 +4647,7 @@ static int dcp_comp_bind_pipe(struct device *dev, struct device *main, void *dat dev_info(dev, "DCP index:%u dptx target phy: %u dptx die: %u\n", dcp->index, dcp->dptx_phy, dcp->dptx_die); - if (!dcp_show_notch(dcp)) + if (!show_notch) ret = of_property_read_u32(dev->of_node, "apple,notch-height", &dcp->notch_height); diff --git a/drivers/gpu/drm/apple/iomfb_v26_6.c b/drivers/gpu/drm/apple/iomfb_v26_6.c index 3a33ae65d2018..bf2f0c4f610c7 100644 --- a/drivers/gpu/drm/apple/iomfb_v26_6.c +++ b/drivers/gpu/drm/apple/iomfb_v26_6.c @@ -28,7 +28,7 @@ * Scanout is one plane, XRGB8888 or ARGB8888, linear, exactly the mode size. * Each flush is one swap, completed when the firmware reports that swap id, * and then the page-flip event is sent. The notch rows (apple,notch-height, - * unless appledrm.t6030_show_notch=1) are outside the mode and stay black. + * unless appledrm.show_notch=1) are outside the mode and stay black. */ #include From c3a5cff12d7f6915c4c6fd119a77d3f1331b71e8 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 14:21:51 +1100 Subject: [PATCH 159/388] docs: gpu: identify the Apple parameter migration license Assisted-by: LLM Signed-off-by: Ryan Murray --- Documentation/gpu/apple-drm-parameters.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Documentation/gpu/apple-drm-parameters.rst b/Documentation/gpu/apple-drm-parameters.rst index b1e8bcaa4d163..599cf050f7c81 100644 --- a/Documentation/gpu/apple-drm-parameters.rst +++ b/Documentation/gpu/apple-drm-parameters.rst @@ -1,3 +1,5 @@ +.. SPDX-License-Identifier: GPL-2.0-only + Apple display module parameters =============================== From e3653e6e1a563902ef2d32e25ac1d6e3b9f69a7d Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 14:18:53 +1100 Subject: [PATCH 160/388] dmaengine: apple-sio: select firmware behavior from compatible data The T6030 backend is already enabled in the shared kernel fragment, but it has a separate build option and repeated conditional code. Compile it with the common SIO driver and select its boot-table validator, FIFO threshold and descriptor handling through the existing apple,t6030-sio compatible. The generic apple,sio profile retains the M1/M2 behavior. Keep the protocol-10 validation and refuse incompatible tables before DMA or hardware setup. Remove APPLE_SIO_T6030 and its shared fragment request. Update the binding description, synthetic DT overlay and KUnit probe cases to cover the specific match, missing/older tables and an unknown compatible. Assisted-by: LLM Signed-off-by: Ryan Murray --- .../devicetree/bindings/dma/apple,sio.yaml | 4 +- arch/arm64/configs/asahi.config | 1 - drivers/dma/Kconfig | 20 +---- drivers/dma/apple-sio-probe-test.c | 46 +++++++++-- drivers/dma/apple-sio-probe-test.dtso | 10 +++ drivers/dma/apple-sio.c | 79 +++++++++---------- 6 files changed, 93 insertions(+), 67 deletions(-) diff --git a/Documentation/devicetree/bindings/dma/apple,sio.yaml b/Documentation/devicetree/bindings/dma/apple,sio.yaml index dbaaa2d3d1a1a..708d7bb8bed96 100644 --- a/Documentation/devicetree/bindings/dma/apple,sio.yaml +++ b/Documentation/devicetree/bindings/dma/apple,sio.yaml @@ -9,7 +9,9 @@ title: Apple SIO Coprocessor description: SIO is a coprocessor on Apple M1 and later chips (and maybe also on earlier chips). Its role is to offload SPI, UART and DisplayPort audio transfers, - being a pretend DMA controller. + being a pretend DMA controller. The SoC-specific compatible selects the + firmware protocol and channel configuration. In particular, apple,t6030-sio + requires protocol-10 boot tables before the coprocessor can be started. maintainers: - Martin Povišer diff --git a/arch/arm64/configs/asahi.config b/arch/arm64/configs/asahi.config index d07b70afd9294..590df15222c8b 100644 --- a/arch/arm64/configs/asahi.config +++ b/arch/arm64/configs/asahi.config @@ -63,7 +63,6 @@ CONFIG_USB_XHCI_PCI_ASMEDIA=y CONFIG_TYPEC_SN201202X=m CONFIG_RTC_DRV_APPLE_PMU=m CONFIG_APPLE_SIO=m -CONFIG_APPLE_SIO_T6030=y CONFIG_APPLE_DART=y CONFIG_APPLE_DART_J713_DCP=y CONFIG_APPLE_DOCKCHANNEL=m diff --git a/drivers/dma/Kconfig b/drivers/dma/Kconfig index 94f9c2e2860d9..6ba3a178c636b 100644 --- a/drivers/dma/Kconfig +++ b/drivers/dma/Kconfig @@ -111,26 +111,12 @@ config APPLE_SIO_KUNIT_TEST default KUNIT_ALL_TESTS help Check which SIO device-tree nodes the probe goes on with: the - generic SIO of the M1 and M2 in every configuration, and a T6030 - SIO only with APPLE_SIO_T6030 and protocol-10 boot tables. Run it - both with and without APPLE_SIO_T6030. - -config APPLE_SIO_T6030 - bool "Apple SIO on the M3 Pro (T6030)" - depends on APPLE_SIO - help - Configure the SIO channels of the T6030 (apple,t6030-sio) the way - its firmware expects them: the DisplayPort audio channels refill - their FIFO at the threshold the system's device tree records for - them (0x600 bytes, not 0x800), and every period descriptor is - published whole. The firmware's boot tables come from the boot - loader, as on the other SoCs; the SIO is only started when they - are protocol-10 tables, which a boot loader without T6030 support - does not build. Without this option a T6030 SIO is left stopped. + generic SIO of the M1 and M2, and a T6030 SIO only with validated + protocol-10 boot tables. The compatible selects the firmware layout. config APPLE_SIO_T6030_KUNIT_TEST bool "KUnit tests for the T6030 SIO boot-table check" if !KUNIT_ALL_TESTS - depends on APPLE_SIO=y && APPLE_SIO_T6030 && KUNIT=y + depends on APPLE_SIO=y && KUNIT=y depends on OF_OVERLAY default KUNIT_ALL_TESTS help diff --git a/drivers/dma/apple-sio-probe-test.c b/drivers/dma/apple-sio-probe-test.c index 7049c8acb532a..c486c5072f07c 100644 --- a/drivers/dma/apple-sio-probe-test.c +++ b/drivers/dma/apple-sio-probe-test.c @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-only OR MIT /* - * Included by apple-sio.c: which SIO nodes the probe goes on with, with and - * without APPLE_SIO_T6030. The test devices have no register range, so a probe + * Included by apple-sio.c: the compatible selects which firmware tables a + * node needs. The test devices have no register range, so a probe * that goes on toward the hardware stops there with -EINVAL; a refused node * returns -ENODEV first. */ @@ -13,16 +13,17 @@ OF_OVERLAY_DECLARE(apple_sio_probe_test); static void sio_probe_nodes_test(struct kunit *test) { - const bool t6030 = IS_ENABLED(CONFIG_APPLE_SIO_T6030); const struct { const char *path; int err; } cases[] = { - /* M1/M2: the generic SIO, either way */ + /* M1/M2: the generic SIO */ { "/sio-probe-test/sio-probe-test-t8103", -EINVAL }, - /* T6030: only with its support built, and with protocol-10 tables */ - { "/sio-probe-test/sio-probe-test-t6030-v10", t6030 ? -EINVAL : -ENODEV }, + /* T6030: protocol-10 tables select the admitted path */ + { "/sio-probe-test/sio-probe-test-t6030-v10", -EINVAL }, { "/sio-probe-test/sio-probe-test-t6030-earlier", -ENODEV }, + { "/sio-probe-test/sio-probe-test-t6030-missing", -ENODEV }, + { "/sio-probe-test/sio-probe-test-unknown", -ENODEV }, }; unsigned int i; @@ -38,15 +39,44 @@ static void sio_probe_nodes_test(struct kunit *test) KUNIT_ASSERT_NOT_NULL_MSG(test, pdev->dev.of_node, "%s", cases[i].path); mask = pdev->dev.coherent_dma_mask; KUNIT_EXPECT_EQ_MSG(test, sio_probe(pdev), cases[i].err, "%s", cases[i].path); - /* Without its support, a T6030 is refused before its DMA is set up */ - if (!t6030 && cases[i].err == -ENODEV) + /* Bad protocol tables are refused before DMA setup. */ + if (cases[i].err == -ENODEV) KUNIT_EXPECT_EQ_MSG(test, pdev->dev.coherent_dma_mask, mask, "%s", cases[i].path); } } +static void sio_match_data_test(struct kunit *test) +{ + const struct of_device_id *match; + struct device_node *np; + const struct sio_hw_data *hw; + + KUNIT_ASSERT_EQ(test, 0, of_overlay_apply_kunit(test, apple_sio_probe_test)); + np = of_find_node_by_path("/sio-probe-test/sio-probe-test-t6030-v10"); + KUNIT_ASSERT_NOT_NULL(test, np); + match = of_match_node(sio_of_match, np); + of_node_put(np); + KUNIT_ASSERT_NOT_NULL(test, match); + hw = match->data; + KUNIT_EXPECT_EQ(test, hw->fifo_threshold, 0x600U); + KUNIT_EXPECT_TRUE(test, hw->clear_period_desc); + KUNIT_EXPECT_TRUE(test, hw->check_boot_tables == sio_t6030_check_boot_tables); + + np = of_find_node_by_path("/sio-probe-test/sio-probe-test-t8103"); + KUNIT_ASSERT_NOT_NULL(test, np); + match = of_match_node(sio_of_match, np); + of_node_put(np); + KUNIT_ASSERT_NOT_NULL(test, match); + hw = match->data; + KUNIT_EXPECT_EQ(test, hw->fifo_threshold, 0x800U); + KUNIT_EXPECT_FALSE(test, hw->clear_period_desc); + KUNIT_EXPECT_FALSE(test, hw->check_boot_tables); +} + static struct kunit_case sio_probe_test_cases[] = { KUNIT_CASE(sio_probe_nodes_test), + KUNIT_CASE(sio_match_data_test), {} }; diff --git a/drivers/dma/apple-sio-probe-test.dtso b/drivers/dma/apple-sio-probe-test.dtso index 79f3346beaad8..93816e9f6b202 100644 --- a/drivers/dma/apple-sio-probe-test.dtso +++ b/drivers/dma/apple-sio-probe-test.dtso @@ -17,6 +17,16 @@ dma-channels = <8>; }; + sio-probe-test-t6030-missing { + compatible = "apple,t6030-sio", "apple,sio"; + dma-channels = <8>; + }; + + sio-probe-test-unknown { + compatible = "apple,sio-probe-test-unknown"; + dma-channels = <8>; + }; + v10_w: v10-w { iommu-addresses = <&v10 0x0 0x30000 0x0 0x4000>, <&v10 0x0 0x34000 0x0 0x20000>, <&v10 0x0 0x54000 0x0 0x4000>, <&v10 0x0 0x58000 0x0 0x4000>; diff --git a/drivers/dma/apple-sio.c b/drivers/dma/apple-sio.c index 0c06e0264b7b6..65ba4511501c7 100644 --- a/drivers/dma/apple-sio.c +++ b/drivers/dma/apple-sio.c @@ -108,7 +108,14 @@ struct sio_chan { typedef void (*sio_ack_callback)(struct sio_chan *, void *, bool); +struct sio_hw_data { + u32 fifo_threshold; + bool clear_period_desc; + int (*check_boot_tables)(struct device *dev); +}; + struct sio_data { + const struct sio_hw_data *hw; void __iomem *base; struct dma_device dma; struct device *dev; @@ -130,9 +137,6 @@ struct sio_data { } tags; int nchannels; -#if IS_ENABLED(CONFIG_APPLE_SIO_T6030) - bool t6030; -#endif struct sio_chan channels[]; }; @@ -155,16 +159,6 @@ static int sio_send_siomsg_atomic(struct sio_data *sio, u64 msg, void *cookie); static int sio_call(struct sio_data *sio, u64 msg); -static inline bool sio_is_t6030(struct sio_data *sio) -{ -#if IS_ENABLED(CONFIG_APPLE_SIO_T6030) - return sio->t6030; -#else - return false; -#endif -} - -#if IS_ENABLED(CONFIG_APPLE_SIO_T6030) /* * Boot tables of the T6030 SIO firmware, protocol 10. The boot loader builds them from the ADT * and passes each one as two apple,sio-firmware-params: the table's IOVA page under its key and @@ -320,7 +314,23 @@ static int sio_t6030_check_boot_tables(struct device *dev) } return 0; } -#endif + +static const struct sio_hw_data sio_hw_generic = { + .fifo_threshold = 0x800, +}; + +static const struct sio_hw_data sio_hw_t6030 = { + .fifo_threshold = 0x600, + .clear_period_desc = true, + .check_boot_tables = sio_t6030_check_boot_tables, +}; + +static const struct of_device_id sio_of_match[] = { + { .compatible = "apple,t6030-sio", .data = &sio_hw_t6030 }, + { .compatible = "apple,sio", .data = &sio_hw_generic }, + { } +}; +MODULE_DEVICE_TABLE(of, sio_of_match); static struct sio_chan *to_sio_chan(struct dma_chan *chan) { @@ -459,7 +469,7 @@ static struct dma_async_tx_descriptor *sio_prep_dma_cyclic( * T6030: publish the whole descriptor, its other fields zero; * a slot may come back from an earlier transfer. */ - if (sio_is_t6030(siochan->host)) + if (siochan->host->hw->clear_period_desc) memset(d, 0, sizeof(*d)); d->flag = 1; /* not sure what's up with this */ d->iova = buf_addr + period_len * i; @@ -839,7 +849,7 @@ static int sio_device_config(struct dma_chan *chan, * dp-audioN dma-channels: channel, data shape, timeout, FIFO size, * threshold). T6030's DisplayPort audio channels refill at 0x600. */ - cfg.threshold = sio_is_t6030(sio) ? 0x600 : 0x800; + cfg.threshold = sio->hw->fifo_threshold; /* * Dmaengine prescribes we ought to apply the new configuration only @@ -967,20 +977,23 @@ static int sio_send_dt_params(struct sio_data *sio) static int sio_probe(struct platform_device *pdev) { struct device_node *np = pdev->dev.of_node; + const struct of_device_id *match; + const struct sio_hw_data *hw; struct sio_data *sio; struct dma_device *dma; int nchannels; int err, i; - /* - * Only APPLE_SIO_T6030 checks a T6030's boot tables and configures its - * channels: without it, leave the SIO stopped rather than start it as - * the generic one. - */ - if (!IS_ENABLED(CONFIG_APPLE_SIO_T6030) && - of_device_is_compatible(np, "apple,t6030-sio")) - return dev_err_probe(&pdev->dev, -ENODEV, - "T6030 SIO support (APPLE_SIO_T6030) not built; leaving the SIO stopped\n"); + match = of_match_node(sio_of_match, np); + if (!match) + return -ENODEV; + hw = match->data; + /* Refuse incompatible firmware tables before DMA or hardware setup. */ + if (hw->check_boot_tables) { + err = hw->check_boot_tables(&pdev->dev); + if (err) + return err; + } err = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(42)); if (err) @@ -998,15 +1011,7 @@ static int sio_probe(struct platform_device *pdev) platform_set_drvdata(pdev, sio); sio->dev = &pdev->dev; sio->nchannels = nchannels; -#if IS_ENABLED(CONFIG_APPLE_SIO_T6030) - sio->t6030 = of_device_is_compatible(np, "apple,t6030-sio"); - /* Before any register, power or firmware access */ - if (sio->t6030) { - err = sio_t6030_check_boot_tables(&pdev->dev); - if (err) - return err; - } -#endif + sio->hw = hw; sio->base = devm_platform_ioremap_resource(pdev, 0); if (IS_ERR(sio->base)) @@ -1107,12 +1112,6 @@ static void sio_remove(struct platform_device *pdev) dma_async_device_unregister(&sio->dma); } -static const struct of_device_id sio_of_match[] = { - { .compatible = "apple,sio", }, - { } -}; -MODULE_DEVICE_TABLE(of, sio_of_match); - static __maybe_unused int sio_suspend(struct device *dev) { /* From c7aed9d6140f061b4af2e437c3c4a8008aaa81f2 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 14:48:02 +1100 Subject: [PATCH 161/388] arm64: configs: retire the superseded J700 fragments on aurora-wip Integration #388 already replaced these four files with the shared asahi.config recipe. Remove the stale copies from aurora-wip after carrying the common configuration and compatible-data cleanup, so the branch has the same hardware recipe as integration. Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/configs/aurora_j700_defconfig | 2895 ------------------ arch/arm64/configs/j700-device-parity.config | 1053 ------- arch/arm64/configs/j700-radio.config | 20 - arch/arm64/configs/j700.config | 113 - 4 files changed, 4081 deletions(-) delete mode 100644 arch/arm64/configs/aurora_j700_defconfig delete mode 100644 arch/arm64/configs/j700-device-parity.config delete mode 100644 arch/arm64/configs/j700-radio.config delete mode 100644 arch/arm64/configs/j700.config diff --git a/arch/arm64/configs/aurora_j700_defconfig b/arch/arm64/configs/aurora_j700_defconfig deleted file mode 100644 index 49e5c03d106ea..0000000000000 --- a/arch/arm64/configs/aurora_j700_defconfig +++ /dev/null @@ -1,2895 +0,0 @@ -CONFIG_WERROR=y -CONFIG_LOCALVERSION="-aurora" -# CONFIG_LOCALVERSION_AUTO is not set -CONFIG_SYSVIPC=y -CONFIG_POSIX_MQUEUE=y -CONFIG_AUDIT=y -CONFIG_NO_HZ_IDLE=y -CONFIG_HIGH_RES_TIMERS=y -CONFIG_BPF_SYSCALL=y -CONFIG_BPF_JIT=y -CONFIG_PREEMPT=y -CONFIG_IRQ_TIME_ACCOUNTING=y -CONFIG_BSD_PROCESS_ACCT=y -CONFIG_BSD_PROCESS_ACCT_V3=y -CONFIG_TASKSTATS=y -CONFIG_TASK_XACCT=y -CONFIG_TASK_IO_ACCOUNTING=y -CONFIG_IKCONFIG=y -CONFIG_IKCONFIG_PROC=y -CONFIG_UCLAMP_TASK=y -CONFIG_NUMA_BALANCING=y -CONFIG_MEMCG=y -CONFIG_BLK_CGROUP=y -CONFIG_UCLAMP_TASK_GROUP=y -CONFIG_CGROUP_PIDS=y -CONFIG_CGROUP_FREEZER=y -CONFIG_CGROUP_HUGETLB=y -CONFIG_CPUSETS=y -CONFIG_CGROUP_DEVICE=y -CONFIG_CGROUP_CPUACCT=y -CONFIG_CGROUP_PERF=y -CONFIG_CGROUP_BPF=y -CONFIG_CGROUP_MISC=y -CONFIG_USER_NS=y -CONFIG_CHECKPOINT_RESTORE=y -CONFIG_SCHED_AUTOGROUP=y -CONFIG_BLK_DEV_INITRD=y -CONFIG_KALLSYMS_ALL=y -CONFIG_PROFILING=y -CONFIG_RUST=y -CONFIG_ARCH_ACTIONS=y -CONFIG_ARCH_AIROHA=y -CONFIG_ARCH_SUNXI=y -CONFIG_ARCH_ALPINE=y -CONFIG_ARCH_APPLE=y -CONFIG_ARCH_ARTPEC=y -CONFIG_ARCH_AXIADO=y -CONFIG_ARCH_BCM=y -CONFIG_ARCH_BCM2835=y -CONFIG_ARCH_BCM_IPROC=y -CONFIG_ARCH_BCMBCA=y -CONFIG_ARCH_BRCMSTB=y -CONFIG_ARCH_BERLIN=y -CONFIG_ARCH_BLAIZE=y -CONFIG_ARCH_BST=y -CONFIG_ARCH_CIX=y -CONFIG_ARCH_EXYNOS=y -CONFIG_ARCH_K3=y -CONFIG_ARCH_LG1K=y -CONFIG_ARCH_HISI=y -CONFIG_ARCH_KEEMBAY=y -CONFIG_ARCH_MEDIATEK=y -CONFIG_ARCH_MESON=y -CONFIG_ARCH_MICROCHIP=y -CONFIG_ARCH_SPARX5=y -CONFIG_ARCH_MVEBU=y -CONFIG_ARCH_NXP=y -CONFIG_ARCH_LAYERSCAPE=y -CONFIG_ARCH_MXC=y -CONFIG_ARCH_S32=y -CONFIG_ARCH_MA35=y -CONFIG_ARCH_NPCM=y -CONFIG_ARCH_QCOM=y -CONFIG_ARCH_REALTEK=y -CONFIG_ARCH_RENESAS=y -CONFIG_ARCH_ROCKCHIP=y -CONFIG_ARCH_SEATTLE=y -CONFIG_ARCH_INTEL_SOCFPGA=y -CONFIG_ARCH_SOPHGO=y -CONFIG_ARCH_STM32=y -CONFIG_ARCH_SYNQUACER=y -CONFIG_ARCH_TEGRA=y -CONFIG_ARCH_TESLA_FSD=y -CONFIG_ARCH_SPRD=y -CONFIG_ARCH_THUNDER=y -CONFIG_ARCH_THUNDER2=y -CONFIG_ARCH_UNIPHIER=y -CONFIG_ARCH_VEXPRESS=y -CONFIG_ARCH_VISCONTI=y -CONFIG_ARCH_XGENE=y -CONFIG_ARCH_ZYNQMP=y -CONFIG_ARM64_16K_PAGES=y -CONFIG_ARM64_VA_BITS_48=y -CONFIG_NUMA=y -CONFIG_XEN=y -CONFIG_ARM64_MEMORY_MODEL_CONTROL=y -CONFIG_HIBERNATION=y -CONFIG_PM_DEBUG=y -CONFIG_PM_ADVANCED_DEBUG=y -CONFIG_WQ_POWER_EFFICIENT_DEFAULT=y -CONFIG_ENERGY_MODEL=y -CONFIG_ARM_PSCI_CPUIDLE=y -CONFIG_CPU_FREQ=y -CONFIG_CPU_FREQ_STAT=y -CONFIG_CPU_FREQ_GOV_POWERSAVE=m -CONFIG_CPU_FREQ_GOV_USERSPACE=y -CONFIG_CPU_FREQ_GOV_ONDEMAND=y -CONFIG_CPU_FREQ_GOV_CONSERVATIVE=m -CONFIG_CPUFREQ_DT=y -CONFIG_ARM_ALLWINNER_SUN50I_CPUFREQ_NVMEM=m -CONFIG_ARM_APPLE_SOC_CPUFREQ=y -CONFIG_ARM_ARMADA_37XX_CPUFREQ=y -CONFIG_ARM_SCPI_CPUFREQ=y -CONFIG_ARM_IMX_CPUFREQ_DT=m -CONFIG_ARM_MEDIATEK_CPUFREQ=y -CONFIG_ARM_QCOM_CPUFREQ_NVMEM=y -CONFIG_ARM_QCOM_CPUFREQ_HW=y -CONFIG_ARM_RASPBERRYPI_CPUFREQ=m -CONFIG_ARM_SCMI_CPUFREQ=y -CONFIG_ARM_TEGRA186_CPUFREQ=y -CONFIG_QORIQ_CPUFREQ=y -CONFIG_ACPI_CPPC_CPUFREQ=m -CONFIG_ACPI=y -CONFIG_ACPI_HOTPLUG_MEMORY=y -CONFIG_ACPI_HMAT=y -CONFIG_ACPI_APEI=y -CONFIG_ACPI_APEI_GHES=y -CONFIG_ACPI_APEI_PCIEAER=y -CONFIG_ACPI_APEI_MEMORY_FAILURE=y -CONFIG_ACPI_APEI_EINJ=y -CONFIG_VIRTUALIZATION=y -CONFIG_KVM=y -CONFIG_COMPAT_32BIT_TIME=y -# CONFIG_RELR is not set -CONFIG_MODULES=y -CONFIG_MODULE_UNLOAD=y -CONFIG_PARTITION_ADVANCED=y -CONFIG_AIX_PARTITION=y -CONFIG_OSF_PARTITION=y -CONFIG_MAC_PARTITION=y -CONFIG_BSD_DISKLABEL=y -CONFIG_MINIX_SUBPARTITION=y -CONFIG_SOLARIS_X86_PARTITION=y -CONFIG_UNIXWARE_DISKLABEL=y -CONFIG_LDM_PARTITION=y -CONFIG_SGI_PARTITION=y -CONFIG_SUN_PARTITION=y -# CONFIG_CORE_DUMP_DEFAULT_ELF_HEADERS is not set -CONFIG_BINFMT_MISC=m -# CONFIG_COMPAT_BRK is not set -CONFIG_MEMORY_HOTPLUG=y -CONFIG_MEMORY_HOTREMOVE=y -CONFIG_KSM=y -CONFIG_MEMORY_FAILURE=y -CONFIG_TRANSPARENT_HUGEPAGE=y -CONFIG_PAGE_BLOCK_MAX_ORDER=10 -CONFIG_USERFAULTFD=y -CONFIG_NET=y -CONFIG_PACKET=y -CONFIG_PACKET_DIAG=m -CONFIG_UNIX_DIAG=m -CONFIG_TLS=m -CONFIG_XFRM_USER=m -CONFIG_XFRM_INTERFACE=m -CONFIG_XFRM_STATISTICS=y -CONFIG_NET_KEY=m -CONFIG_NET_KEY_MIGRATE=y -CONFIG_IP_MULTICAST=y -CONFIG_IP_ADVANCED_ROUTER=y -CONFIG_IP_MULTIPLE_TABLES=y -CONFIG_IP_ROUTE_MULTIPATH=y -CONFIG_IP_PNP=y -CONFIG_IP_PNP_DHCP=y -CONFIG_IP_PNP_BOOTP=y -CONFIG_NET_IPIP=m -CONFIG_NET_IPGRE_DEMUX=m -CONFIG_NET_IPGRE=m -CONFIG_NET_IPGRE_BROADCAST=y -CONFIG_NET_IPVTI=m -CONFIG_NET_FOU_IP_TUNNELS=y -CONFIG_INET_AH=m -CONFIG_INET_ESP=m -CONFIG_INET_ESP_OFFLOAD=m -CONFIG_INET_ESPINTCP=y -CONFIG_INET_IPCOMP=m -CONFIG_INET_UDP_DIAG=m -CONFIG_INET_RAW_DIAG=m -CONFIG_INET_DIAG_DESTROY=y -CONFIG_TCP_CONG_ADVANCED=y -CONFIG_TCP_CONG_HSTCP=m -CONFIG_TCP_CONG_HYBLA=m -CONFIG_TCP_CONG_NV=m -CONFIG_TCP_CONG_SCALABLE=m -CONFIG_TCP_CONG_LP=m -CONFIG_TCP_CONG_VENO=m -CONFIG_TCP_CONG_YEAH=m -CONFIG_TCP_CONG_ILLINOIS=m -CONFIG_TCP_CONG_DCTCP=m -CONFIG_TCP_CONG_CDG=m -CONFIG_TCP_CONG_BBR=m -CONFIG_INET6_AH=m -CONFIG_INET6_ESP=m -CONFIG_INET6_ESP_OFFLOAD=m -CONFIG_INET6_ESPINTCP=y -CONFIG_INET6_IPCOMP=m -CONFIG_IPV6_VTI=m -CONFIG_IPV6_GRE=m -CONFIG_IPV6_MULTIPLE_TABLES=y -CONFIG_NETFILTER=y -CONFIG_BRIDGE_NETFILTER=m -CONFIG_NETFILTER_NETLINK_HOOK=m -CONFIG_NF_CONNTRACK=m -CONFIG_NF_CONNTRACK_ZONES=y -CONFIG_NF_CONNTRACK_PROCFS=y -CONFIG_NF_CONNTRACK_EVENTS=y -CONFIG_NF_CONNTRACK_TIMEOUT=y -CONFIG_NF_CONNTRACK_TIMESTAMP=y -CONFIG_NF_CONNTRACK_AMANDA=m -CONFIG_NF_CONNTRACK_FTP=m -CONFIG_NF_CONNTRACK_H323=m -CONFIG_NF_CONNTRACK_IRC=m -CONFIG_NF_CONNTRACK_NETBIOS_NS=m -CONFIG_NF_CONNTRACK_SNMP=m -CONFIG_NF_CONNTRACK_PPTP=m -CONFIG_NF_CONNTRACK_SANE=m -CONFIG_NF_CONNTRACK_SIP=m -CONFIG_NF_CONNTRACK_TFTP=m -CONFIG_NF_CT_NETLINK=m -CONFIG_NF_CT_NETLINK_TIMEOUT=m -CONFIG_NF_CT_NETLINK_HELPER=m -CONFIG_NETFILTER_NETLINK_GLUE_CT=y -CONFIG_NF_TABLES=m -CONFIG_NF_TABLES_INET=y -CONFIG_NF_TABLES_NETDEV=y -CONFIG_NFT_NUMGEN=m -CONFIG_NFT_CT=m -CONFIG_NFT_FLOW_OFFLOAD=m -CONFIG_NFT_CONNLIMIT=m -CONFIG_NFT_LOG=m -CONFIG_NFT_LIMIT=m -CONFIG_NFT_MASQ=m -CONFIG_NFT_REDIR=m -CONFIG_NFT_NAT=m -CONFIG_NFT_TUNNEL=m -CONFIG_NFT_QUEUE=m -CONFIG_NFT_QUOTA=m -CONFIG_NFT_REJECT=m -CONFIG_NFT_COMPAT=m -CONFIG_NFT_HASH=m -CONFIG_NFT_FIB_INET=m -CONFIG_NFT_XFRM=m -CONFIG_NFT_SOCKET=m -CONFIG_NFT_OSF=m -CONFIG_NFT_TPROXY=m -CONFIG_NFT_DUP_NETDEV=m -CONFIG_NFT_FWD_NETDEV=m -CONFIG_NFT_FIB_NETDEV=m -CONFIG_NFT_REJECT_NETDEV=m -CONFIG_NF_FLOW_TABLE_INET=m -CONFIG_NF_FLOW_TABLE=m -CONFIG_NF_FLOW_TABLE_PROCFS=y -CONFIG_NETFILTER_XTABLES_LEGACY=y -CONFIG_NETFILTER_XT_SET=m -CONFIG_NETFILTER_XT_TARGET_AUDIT=m -CONFIG_NETFILTER_XT_TARGET_CHECKSUM=m -CONFIG_NETFILTER_XT_TARGET_CLASSIFY=m -CONFIG_NETFILTER_XT_TARGET_CONNMARK=m -CONFIG_NETFILTER_XT_TARGET_DSCP=m -CONFIG_NETFILTER_XT_TARGET_HMARK=m -CONFIG_NETFILTER_XT_TARGET_IDLETIMER=m -CONFIG_NETFILTER_XT_TARGET_LED=m -CONFIG_NETFILTER_XT_TARGET_LOG=m -CONFIG_NETFILTER_XT_TARGET_MARK=m -CONFIG_NETFILTER_XT_TARGET_NFLOG=m -CONFIG_NETFILTER_XT_TARGET_NFQUEUE=m -CONFIG_NETFILTER_XT_TARGET_NOTRACK=m -CONFIG_NETFILTER_XT_TARGET_TEE=m -CONFIG_NETFILTER_XT_TARGET_TPROXY=m -CONFIG_NETFILTER_XT_TARGET_TRACE=m -CONFIG_NETFILTER_XT_TARGET_TCPMSS=m -CONFIG_NETFILTER_XT_TARGET_TCPOPTSTRIP=m -CONFIG_NETFILTER_XT_MATCH_ADDRTYPE=m -CONFIG_NETFILTER_XT_MATCH_BPF=m -CONFIG_NETFILTER_XT_MATCH_CGROUP=m -CONFIG_NETFILTER_XT_MATCH_CLUSTER=m -CONFIG_NETFILTER_XT_MATCH_COMMENT=m -CONFIG_NETFILTER_XT_MATCH_CONNBYTES=m -CONFIG_NETFILTER_XT_MATCH_CONNLABEL=m -CONFIG_NETFILTER_XT_MATCH_CONNLIMIT=m -CONFIG_NETFILTER_XT_MATCH_CONNMARK=m -CONFIG_NETFILTER_XT_MATCH_CONNTRACK=m -CONFIG_NETFILTER_XT_MATCH_CPU=m -CONFIG_NETFILTER_XT_MATCH_DCCP=m -CONFIG_NETFILTER_XT_MATCH_DEVGROUP=m -CONFIG_NETFILTER_XT_MATCH_DSCP=m -CONFIG_NETFILTER_XT_MATCH_ESP=m -CONFIG_NETFILTER_XT_MATCH_HASHLIMIT=m -CONFIG_NETFILTER_XT_MATCH_HELPER=m -CONFIG_NETFILTER_XT_MATCH_IPCOMP=m -CONFIG_NETFILTER_XT_MATCH_IPRANGE=m -CONFIG_NETFILTER_XT_MATCH_IPVS=m -CONFIG_NETFILTER_XT_MATCH_LENGTH=m -CONFIG_NETFILTER_XT_MATCH_LIMIT=m -CONFIG_NETFILTER_XT_MATCH_MAC=m -CONFIG_NETFILTER_XT_MATCH_MARK=m -CONFIG_NETFILTER_XT_MATCH_MULTIPORT=m -CONFIG_NETFILTER_XT_MATCH_NFACCT=m -CONFIG_NETFILTER_XT_MATCH_OSF=m -CONFIG_NETFILTER_XT_MATCH_OWNER=m -CONFIG_NETFILTER_XT_MATCH_POLICY=m -CONFIG_NETFILTER_XT_MATCH_PHYSDEV=m -CONFIG_NETFILTER_XT_MATCH_PKTTYPE=m -CONFIG_NETFILTER_XT_MATCH_QUOTA=m -CONFIG_NETFILTER_XT_MATCH_RATEEST=m -CONFIG_NETFILTER_XT_MATCH_REALM=m -CONFIG_NETFILTER_XT_MATCH_RECENT=m -CONFIG_NETFILTER_XT_MATCH_SCTP=m -CONFIG_NETFILTER_XT_MATCH_SOCKET=m -CONFIG_NETFILTER_XT_MATCH_STATE=m -CONFIG_NETFILTER_XT_MATCH_STATISTIC=m -CONFIG_NETFILTER_XT_MATCH_STRING=m -CONFIG_NETFILTER_XT_MATCH_TCPMSS=m -CONFIG_NETFILTER_XT_MATCH_TIME=m -CONFIG_NETFILTER_XT_MATCH_U32=m -CONFIG_IP_SET=m -CONFIG_IP_SET_BITMAP_IP=m -CONFIG_IP_SET_BITMAP_IPMAC=m -CONFIG_IP_SET_BITMAP_PORT=m -CONFIG_IP_SET_HASH_IP=m -CONFIG_IP_SET_HASH_IPMARK=m -CONFIG_IP_SET_HASH_IPPORT=m -CONFIG_IP_SET_HASH_IPPORTIP=m -CONFIG_IP_SET_HASH_IPPORTNET=m -CONFIG_IP_SET_HASH_IPMAC=m -CONFIG_IP_SET_HASH_MAC=m -CONFIG_IP_SET_HASH_NETPORTNET=m -CONFIG_IP_SET_HASH_NET=m -CONFIG_IP_SET_HASH_NETNET=m -CONFIG_IP_SET_HASH_NETPORT=m -CONFIG_IP_SET_HASH_NETIFACE=m -CONFIG_IP_SET_LIST_SET=m -CONFIG_IP_VS=m -CONFIG_NFT_DUP_IPV4=m -CONFIG_NFT_FIB_IPV4=m -CONFIG_NF_TABLES_ARP=y -CONFIG_NF_LOG_ARP=m -CONFIG_NF_LOG_IPV4=m -CONFIG_IP_NF_IPTABLES=m -CONFIG_IP_NF_MATCH_AH=m -CONFIG_IP_NF_MATCH_ECN=m -CONFIG_IP_NF_MATCH_RPFILTER=m -CONFIG_IP_NF_MATCH_TTL=m -CONFIG_IP_NF_TARGET_REJECT=m -CONFIG_IP_NF_NAT=m -CONFIG_IP_NF_TARGET_MASQUERADE=m -CONFIG_IP_NF_TARGET_NETMAP=m -CONFIG_IP_NF_TARGET_REDIRECT=m -CONFIG_IP_NF_TARGET_ECN=m -CONFIG_IP_NF_TARGET_TTL=m -CONFIG_IP_NF_RAW=m -CONFIG_IP_NF_ARPFILTER=m -CONFIG_IP_NF_ARP_MANGLE=m -CONFIG_NFT_DUP_IPV6=m -CONFIG_NFT_FIB_IPV6=m -CONFIG_IP6_NF_IPTABLES=m -CONFIG_IP6_NF_MATCH_AH=m -CONFIG_IP6_NF_MATCH_EUI64=m -CONFIG_IP6_NF_MATCH_FRAG=m -CONFIG_IP6_NF_MATCH_OPTS=m -CONFIG_IP6_NF_MATCH_HL=m -CONFIG_IP6_NF_MATCH_IPV6HEADER=m -CONFIG_IP6_NF_MATCH_MH=m -CONFIG_IP6_NF_MATCH_RPFILTER=m -CONFIG_IP6_NF_MATCH_RT=m -CONFIG_IP6_NF_MATCH_SRH=m -CONFIG_IP6_NF_TARGET_HL=m -CONFIG_IP6_NF_TARGET_REJECT=m -CONFIG_IP6_NF_RAW=m -CONFIG_IP6_NF_NAT=m -CONFIG_IP6_NF_TARGET_MASQUERADE=m -CONFIG_IP6_NF_TARGET_NPT=m -CONFIG_NF_TABLES_BRIDGE=m -CONFIG_NFT_BRIDGE_META=m -CONFIG_NFT_BRIDGE_REJECT=m -CONFIG_NF_CONNTRACK_BRIDGE=m -CONFIG_BRIDGE_NF_EBTABLES_LEGACY=m -CONFIG_BRIDGE_NF_EBTABLES=m -CONFIG_BRIDGE_EBT_BROUTE=m -CONFIG_BRIDGE_EBT_T_FILTER=m -CONFIG_BRIDGE_EBT_T_NAT=m -CONFIG_BRIDGE_EBT_802_3=m -CONFIG_BRIDGE_EBT_AMONG=m -CONFIG_BRIDGE_EBT_ARP=m -CONFIG_BRIDGE_EBT_IP=m -CONFIG_BRIDGE_EBT_IP6=m -CONFIG_BRIDGE_EBT_LIMIT=m -CONFIG_BRIDGE_EBT_MARK=m -CONFIG_BRIDGE_EBT_PKTTYPE=m -CONFIG_BRIDGE_EBT_STP=m -CONFIG_BRIDGE_EBT_VLAN=m -CONFIG_BRIDGE_EBT_ARPREPLY=m -CONFIG_BRIDGE_EBT_DNAT=m -CONFIG_BRIDGE_EBT_MARK_T=m -CONFIG_BRIDGE_EBT_REDIRECT=m -CONFIG_BRIDGE_EBT_SNAT=m -CONFIG_BRIDGE_EBT_LOG=m -CONFIG_BRIDGE_EBT_NFLOG=m -CONFIG_L2TP=m -CONFIG_L2TP_V3=y -CONFIG_L2TP_IP=m -CONFIG_L2TP_ETH=m -CONFIG_BRIDGE=m -CONFIG_BRIDGE_VLAN_FILTERING=y -CONFIG_NET_DSA=m -CONFIG_VLAN_8021Q=m -CONFIG_VLAN_8021Q_GVRP=y -CONFIG_VLAN_8021Q_MVRP=y -CONFIG_NET_SCHED=y -CONFIG_NET_SCH_HTB=m -CONFIG_NET_SCH_HFSC=m -CONFIG_NET_SCH_PRIO=m -CONFIG_NET_SCH_MULTIQ=m -CONFIG_NET_SCH_RED=m -CONFIG_NET_SCH_SFB=m -CONFIG_NET_SCH_SFQ=m -CONFIG_NET_SCH_TEQL=m -CONFIG_NET_SCH_TBF=m -CONFIG_NET_SCH_CBS=m -CONFIG_NET_SCH_ETF=m -CONFIG_NET_SCH_TAPRIO=m -CONFIG_NET_SCH_GRED=m -CONFIG_NET_SCH_NETEM=m -CONFIG_NET_SCH_DRR=m -CONFIG_NET_SCH_MQPRIO=m -CONFIG_NET_SCH_CHOKE=m -CONFIG_NET_SCH_QFQ=m -CONFIG_NET_SCH_CODEL=m -CONFIG_NET_SCH_FQ_CODEL=m -CONFIG_NET_SCH_CAKE=m -CONFIG_NET_SCH_FQ=m -CONFIG_NET_SCH_HHF=m -CONFIG_NET_SCH_PIE=m -CONFIG_NET_SCH_FQ_PIE=m -CONFIG_NET_SCH_INGRESS=m -CONFIG_NET_SCH_PLUG=m -CONFIG_NET_SCH_ETS=m -CONFIG_NET_SCH_DUALPI2=m -CONFIG_NET_CLS_BASIC=m -CONFIG_NET_CLS_ROUTE4=m -CONFIG_NET_CLS_FW=m -CONFIG_NET_CLS_U32=m -CONFIG_CLS_U32_PERF=y -CONFIG_CLS_U32_MARK=y -CONFIG_NET_CLS_FLOW=m -CONFIG_NET_CLS_BPF=m -CONFIG_NET_CLS_FLOWER=m -CONFIG_NET_CLS_MATCHALL=m -CONFIG_NET_EMATCH=y -CONFIG_NET_EMATCH_CMP=m -CONFIG_NET_EMATCH_NBYTE=m -CONFIG_NET_EMATCH_U32=m -CONFIG_NET_EMATCH_META=m -CONFIG_NET_EMATCH_TEXT=m -CONFIG_NET_EMATCH_IPSET=m -CONFIG_NET_EMATCH_IPT=m -CONFIG_NET_CLS_ACT=y -CONFIG_NET_ACT_POLICE=m -CONFIG_NET_ACT_GACT=m -CONFIG_GACT_PROB=y -CONFIG_NET_ACT_MIRRED=m -CONFIG_NET_ACT_SAMPLE=m -CONFIG_NET_ACT_NAT=m -CONFIG_NET_ACT_PEDIT=m -CONFIG_NET_ACT_SIMP=m -CONFIG_NET_ACT_SKBEDIT=m -CONFIG_NET_ACT_CSUM=m -CONFIG_NET_ACT_MPLS=m -CONFIG_NET_ACT_VLAN=m -CONFIG_NET_ACT_BPF=m -CONFIG_NET_ACT_CONNMARK=m -CONFIG_NET_ACT_CTINFO=m -CONFIG_NET_ACT_SKBMOD=m -CONFIG_NET_ACT_TUNNEL_KEY=m -CONFIG_NET_ACT_CT=m -CONFIG_NET_ACT_GATE=m -CONFIG_VSOCKETS=m -CONFIG_NETLINK_DIAG=m -CONFIG_HSR=m -CONFIG_QRTR_SMD=m -CONFIG_QRTR_TUN=m -CONFIG_CAN=m -CONFIG_CAN_J1939=m -CONFIG_CAN_ISOTP=m -CONFIG_BT=m -CONFIG_BT_RFCOMM=m -CONFIG_BT_RFCOMM_TTY=y -CONFIG_BT_BNEP=m -CONFIG_BT_BNEP_MC_FILTER=y -CONFIG_BT_BNEP_PROTO_FILTER=y -CONFIG_BT_HIDP=m -CONFIG_BT_LEDS=y -CONFIG_BT_MSFTEXT=y -CONFIG_BT_AOSPEXT=y -CONFIG_BT_BRCMEXT=y -# CONFIG_BT_DEBUGFS is not set -CONFIG_BT_MTK7932_PCIE=m -CONFIG_BT_HCIBTUSB=m -CONFIG_BT_HCIBTUSB_AUTOSUSPEND=y -CONFIG_BT_HCIBTUSB_MTK=y -CONFIG_BT_HCIUART=m -CONFIG_BT_HCIUART_LL=y -CONFIG_BT_HCIUART_BCM=y -CONFIG_BT_HCIUART_QCA=y -CONFIG_BT_HCIUART_MRVL=y -CONFIG_BT_HCIBCM203X=m -CONFIG_BT_HCIBCM4377=m -CONFIG_BT_HCIBPA10X=m -CONFIG_BT_HCIBFUSB=m -CONFIG_BT_HCIVHCI=m -CONFIG_BT_MRVL=m -CONFIG_BT_MRVL_SDIO=m -CONFIG_BT_ATH3K=m -CONFIG_BT_QCOMSMD=m -CONFIG_BT_NXPUART=m -CONFIG_CFG80211=m -CONFIG_MAC80211=m -CONFIG_RFKILL=y -CONFIG_RFKILL_GPIO=m -CONFIG_NET_9P=y -CONFIG_NET_9P_VIRTIO=y -CONFIG_NFC=m -CONFIG_NFC_NCI=m -CONFIG_NFC_NXP_NCI=m -CONFIG_NFC_NXP_NCI_I2C=m -CONFIG_NFC_S3FWRN5_I2C=m -CONFIG_PCI=y -CONFIG_PCIEPORTBUS=y -CONFIG_PCIEAER=y -CONFIG_PCI_IOV=y -CONFIG_PCI_PASID=y -CONFIG_HOTPLUG_PCI=y -CONFIG_HOTPLUG_PCI_ACPI=y -CONFIG_PCI_AARDVARK=y -CONFIG_PCIE_ALTERA=y -CONFIG_PCIE_ALTERA_MSI=y -CONFIG_PCIE_APPLE=y -CONFIG_PCIE_APPLE_PIODMA_DIAG=y -CONFIG_PCIE_BRCMSTB=m -CONFIG_PCI_HOST_THUNDER_PEM=y -CONFIG_PCI_HOST_THUNDER_ECAM=y -CONFIG_PCI_HOST_GENERIC=y -CONFIG_PCIE_MEDIATEK_GEN3=m -CONFIG_PCI_TEGRA=y -CONFIG_PCIE_RCAR_HOST=y -CONFIG_PCIE_RCAR_EP=y -CONFIG_PCIE_RENESAS_RZG3S_HOST=y -CONFIG_PCIE_ROCKCHIP_HOST=m -CONFIG_PCI_XGENE=y -CONFIG_PCIE_XILINX=y -CONFIG_PCIE_XILINX_DMA_PL=y -CONFIG_PCIE_XILINX_NWL=y -CONFIG_PCIE_XILINX_CPM=y -CONFIG_PCI_J721E_HOST=m -CONFIG_PCI_IMX6_HOST=y -CONFIG_PCI_LAYERSCAPE=y -CONFIG_PCI_HISI=y -CONFIG_PCIE_KIRIN=y -CONFIG_PCIE_HISI_STB=y -CONFIG_PCIE_ARMADA_8K=y -CONFIG_PCIE_TEGRA194_HOST=m -CONFIG_PCIE_TEGRA194_EP=m -CONFIG_PCIE_QCOM=y -CONFIG_PCIE_RCAR_GEN4_HOST=m -CONFIG_PCIE_RCAR_GEN4_EP=m -CONFIG_PCIE_ROCKCHIP_DW_HOST=y -CONFIG_PCIE_VISCONTI_HOST=y -CONFIG_PCIE_LAYERSCAPE_GEN4=y -CONFIG_PCI_ENDPOINT=y -CONFIG_PCI_ENDPOINT_CONFIGFS=y -CONFIG_PCI_EPF_TEST=m -CONFIG_DEVTMPFS=y -CONFIG_DEVTMPFS_MOUNT=y -CONFIG_FW_LOADER_USER_HELPER=y -CONFIG_FW_LOADER_COMPRESS=y -CONFIG_FW_LOADER_COMPRESS_ZSTD=y -CONFIG_HISILICON_LPC=y -CONFIG_IMX_AIPSTZ=m -CONFIG_TEGRA_ACONNECT=m -CONFIG_MHI_BUS_PCI_GENERIC=m -CONFIG_ARM_SCPI_PROTOCOL=y -CONFIG_RASPBERRYPI_FIRMWARE=y -CONFIG_INTEL_STRATIX10_SERVICE=y -CONFIG_INTEL_STRATIX10_RSU=m -CONFIG_MTK_ADSP_IPC=m -CONFIG_SYSFB_SIMPLEFB=y -CONFIG_GOOGLE_FIRMWARE=y -CONFIG_GOOGLE_CBMEM=m -CONFIG_GOOGLE_COREBOOT_TABLE=m -CONFIG_EFI_CAPSULE_LOADER=y -CONFIG_IMX_SCU=y -CONFIG_QCOM_TZMEM_MODE_SHMBRIDGE=y -CONFIG_QCOM_QSEECOM=y -CONFIG_QCOM_QSEECOM_UEFISECAPP=y -CONFIG_EXYNOS_ACPM_PROTOCOL=m -CONFIG_TEGRA_BPMP=y -CONFIG_ZYNQMP_FIRMWARE_DEBUG=y -CONFIG_GNSS=m -CONFIG_GNSS_MTK_SERIAL=m -CONFIG_GNSS_USB=m -CONFIG_MTD=y -CONFIG_MTD_BLOCK=y -CONFIG_MTD_CFI=y -CONFIG_MTD_CFI_ADV_OPTIONS=y -CONFIG_MTD_CFI_INTELEXT=y -CONFIG_MTD_CFI_AMDSTD=y -CONFIG_MTD_CFI_STAA=y -CONFIG_MTD_PHYSMAP=y -CONFIG_MTD_PHYSMAP_OF=y -CONFIG_MTD_DATAFLASH=y -CONFIG_MTD_SST25L=y -CONFIG_MTD_RAW_NAND=y -CONFIG_MTD_NAND_DENALI_DT=y -CONFIG_MTD_NAND_MARVELL=y -CONFIG_MTD_NAND_BRCMNAND=m -CONFIG_MTD_NAND_FSL_IFC=y -CONFIG_MTD_NAND_QCOM=y -CONFIG_MTD_SPI_NAND=m -CONFIG_MTD_SPI_NOR=y -CONFIG_MTD_UBI=m -CONFIG_MTD_HYPERBUS=m -CONFIG_HBMC_AM654=m -CONFIG_OF_OVERLAY=y -CONFIG_ZRAM=y -CONFIG_BLK_DEV_LOOP=y -CONFIG_BLK_DEV_NBD=m -CONFIG_VIRTIO_BLK=y -CONFIG_BLK_DEV_NVME=y -CONFIG_NVME_HWMON=y -CONFIG_NVME_APPLE=y -CONFIG_QCOM_COINCELL=m -CONFIG_QCOM_FASTRPC=m -CONFIG_SRAM=y -CONFIG_PCI_ENDPOINT_TEST=m -CONFIG_XILINX_SDFEC=m -CONFIG_NTSYNC=m -CONFIG_EEPROM_AT24=m -CONFIG_EEPROM_AT25=m -CONFIG_MISC_RTSX_USB=m -CONFIG_UACCE=m -CONFIG_MISC_RP1=m -# CONFIG_SCSI_PROC_FS is not set -CONFIG_BLK_DEV_SD=y -CONFIG_BLK_DEV_SR=m -CONFIG_CHR_DEV_SG=m -CONFIG_SCSI_SAS_ATA=y -CONFIG_SCSI_HISI_SAS=y -CONFIG_SCSI_HISI_SAS_PCI=y -CONFIG_MEGARAID_SAS=y -CONFIG_SCSI_MPT3SAS=m -CONFIG_ATA=y -CONFIG_SATA_AHCI=y -CONFIG_SATA_AHCI_PLATFORM=y -CONFIG_AHCI_BRCM=m -CONFIG_AHCI_DWC=m -CONFIG_AHCI_CEVA=y -CONFIG_AHCI_MVEBU=y -CONFIG_AHCI_XGENE=y -CONFIG_AHCI_QORIQ=y -CONFIG_SATA_SIL24=y -CONFIG_SATA_RCAR=y -CONFIG_PATA_OF_PLATFORM=y -CONFIG_MD=y -CONFIG_MD_LINEAR=m -CONFIG_BLK_DEV_DM=m -CONFIG_DM_CRYPT=m -CONFIG_DM_SNAPSHOT=m -CONFIG_DM_THIN_PROVISIONING=m -CONFIG_DM_CACHE=m -CONFIG_DM_WRITECACHE=m -CONFIG_DM_MIRROR=m -CONFIG_DM_RAID=m -CONFIG_DM_ZERO=m -CONFIG_DM_UEVENT=y -CONFIG_DM_VERITY=m -CONFIG_DM_VERITY_FEC=y -CONFIG_DM_INTEGRITY=m -CONFIG_NETDEVICES=y -CONFIG_BONDING=m -CONFIG_DUMMY=m -CONFIG_WIREGUARD=m -CONFIG_OVPN=m -CONFIG_IFB=m -CONFIG_NET_TEAM=m -CONFIG_NET_TEAM_MODE_BROADCAST=m -CONFIG_NET_TEAM_MODE_ROUNDROBIN=m -CONFIG_NET_TEAM_MODE_RANDOM=m -CONFIG_NET_TEAM_MODE_ACTIVEBACKUP=m -CONFIG_NET_TEAM_MODE_LOADBALANCE=m -CONFIG_MACVLAN=m -CONFIG_MACVTAP=m -CONFIG_VXLAN=m -CONFIG_GENEVE=m -CONFIG_TUN=y -CONFIG_VETH=m -CONFIG_VIRTIO_NET=y -CONFIG_MHI_NET=m -CONFIG_NET_DSA_BCM_SF2=m -CONFIG_NET_DSA_MSCC_FELIX=m -CONFIG_ENA_ETHERNET=m -CONFIG_AMD_XGBE=y -CONFIG_NET_XGENE=y -CONFIG_ATL1C=m -CONFIG_BCMGENET=m -CONFIG_BNX2X=m -CONFIG_SYSTEMPORT=m -CONFIG_MACB=y -CONFIG_THUNDER_NIC_PF=y -CONFIG_FEC=y -CONFIG_FSL_FMAN=y -CONFIG_FSL_DPAA_ETH=y -CONFIG_FSL_DPAA2_ETH=y -CONFIG_FSL_ENETC=y -CONFIG_NXP_ENETC4=m -CONFIG_FSL_ENETC_VF=y -CONFIG_FSL_ENETC_QOS=y -CONFIG_NXP_NETC_BLK_CTRL=m -CONFIG_HIX5HD2_GMAC=y -CONFIG_HNS_DSAF=y -CONFIG_HNS_ENET=y -CONFIG_HNS3=y -CONFIG_HNS3_HCLGE=y -CONFIG_HNS3_ENET=y -CONFIG_E1000=y -CONFIG_E1000E=y -CONFIG_IGB=m -CONFIG_IGBVF=y -CONFIG_IGC=m -CONFIG_MVNETA=y -CONFIG_MVPP2=y -CONFIG_SKY2=y -CONFIG_NET_VENDOR_MEDIATEK=y -CONFIG_NET_MEDIATEK_STAR_EMAC=m -CONFIG_MLX4_EN=m -CONFIG_MLX5_CORE=m -CONFIG_MLX5_CORE_EN=y -CONFIG_QCOM_EMAC=m -CONFIG_RMNET=m -CONFIG_R8169=m -CONFIG_SH_ETH=y -CONFIG_RAVB=y -CONFIG_RENESAS_ETHER_SWITCH=y -CONFIG_RTSN=y -CONFIG_SMC91X=y -CONFIG_SMSC911X=y -CONFIG_SNI_AVE=y -CONFIG_SNI_NETSEC=y -CONFIG_STMMAC_ETH=m -CONFIG_DWMAC_MEDIATEK=m -CONFIG_DWMAC_TEGRA=m -CONFIG_TI_K3_AM65_CPSW_NUSS=y -CONFIG_TI_ICSSG_PRUETH=m -CONFIG_XILINX_AXI_EMAC=m -CONFIG_QCOM_IPA=m -CONFIG_MESON_GXL_PHY=m -CONFIG_AQUANTIA_PHY=y -CONFIG_BCM54140_PHY=m -CONFIG_MARVELL_PHY=m -CONFIG_MARVELL_10G_PHY=y -CONFIG_MARVELL_88Q2XXX_PHY=y -CONFIG_MICREL_PHY=y -CONFIG_MICROSEMI_PHY=y -CONFIG_AT803X_PHY=y -CONFIG_QCA808X_PHY=m -CONFIG_REALTEK_PHY=y -CONFIG_ROCKCHIP_PHY=y -CONFIG_DP83867_PHY=y -CONFIG_DP83869_PHY=m -CONFIG_DP83TD510_PHY=y -CONFIG_DP83TG720_PHY=m -CONFIG_VITESSE_PHY=y -CONFIG_XILINX_GMII2RGMII=m -CONFIG_CAN_FLEXCAN=m -CONFIG_CAN_XILINXCAN=m -CONFIG_CAN_M_CAN=m -CONFIG_CAN_M_CAN_PLATFORM=m -CONFIG_CAN_RCAR=m -CONFIG_CAN_RCAR_CANFD=m -CONFIG_CAN_MCP251XFD=m -CONFIG_CAN_8DEV_USB=m -CONFIG_CAN_EMS_USB=m -CONFIG_CAN_ESD_USB=m -CONFIG_CAN_F81604=m -CONFIG_CAN_GS_USB=m -CONFIG_CAN_KVASER_USB=m -CONFIG_CAN_MCBA_USB=m -CONFIG_CAN_PEAK_USB=m -CONFIG_MDIO_GPIO=y -CONFIG_MDIO_BUS_MUX_MULTIPLEXER=y -CONFIG_MDIO_BUS_MUX_MMIOREG=y -CONFIG_PPP=m -CONFIG_PPP_BSDCOMP=m -CONFIG_PPP_DEFLATE=m -CONFIG_PPP_FILTER=y -CONFIG_PPP_MPPE=m -CONFIG_PPP_MULTILINK=y -CONFIG_PPPOE=m -CONFIG_PPTP=m -CONFIG_PPPOL2TP=m -CONFIG_PPP_ASYNC=m -CONFIG_PPP_SYNC_TTY=m -CONFIG_USB_CATC=m -CONFIG_USB_KAWETH=m -CONFIG_USB_PEGASUS=m -CONFIG_USB_RTL8150=m -CONFIG_USB_RTL8152=y -CONFIG_USB_LAN78XX=m -CONFIG_USB_USBNET=y -CONFIG_USB_NET_CDC_EEM=m -CONFIG_USB_NET_HUAWEI_CDC_NCM=m -CONFIG_USB_NET_CDC_MBIM=m -CONFIG_USB_NET_DM9601=m -CONFIG_USB_NET_SR9700=m -CONFIG_USB_NET_SR9800=m -CONFIG_USB_NET_SMSC75XX=m -CONFIG_USB_NET_SMSC95XX=m -CONFIG_USB_NET_GL620A=m -CONFIG_USB_NET_NET1080=m -CONFIG_USB_NET_PLUSB=m -CONFIG_USB_NET_MCS7830=m -CONFIG_USB_NET_RNDIS_HOST=m -CONFIG_USB_NET_CDC_SUBSET=m -CONFIG_USB_ALI_M5632=y -CONFIG_USB_AN2720=y -CONFIG_USB_EPSON2888=y -CONFIG_USB_KC2190=y -CONFIG_USB_NET_ZAURUS=m -CONFIG_USB_NET_CX82310_ETH=m -CONFIG_USB_NET_KALMIA=m -CONFIG_USB_NET_QMI_WWAN=m -CONFIG_USB_HSO=m -CONFIG_USB_NET_INT51X1=m -CONFIG_USB_IPHETH=m -CONFIG_USB_SIERRA_NET=m -CONFIG_USB_VL600=m -CONFIG_USB_NET_CH9200=m -CONFIG_USB_NET_AQC111=m -CONFIG_ATH9K_HTC=m -CONFIG_CARL9170=m -CONFIG_ATH6KL=m -CONFIG_ATH6KL_USB=m -CONFIG_AR5523=m -CONFIG_ATH10K=m -CONFIG_ATH10K_PCI=m -CONFIG_ATH10K_SDIO=m -CONFIG_ATH10K_USB=m -CONFIG_ATH10K_SNOC=m -CONFIG_WCN36XX=m -CONFIG_ATH11K=m -CONFIG_ATH11K_AHB=m -CONFIG_ATH11K_PCI=m -CONFIG_ATH12K=m -CONFIG_BRCMFMAC=m -CONFIG_BRCMFMAC_USB=y -CONFIG_BRCMFMAC_PCIE=y -CONFIG_IWLWIFI=m -CONFIG_IWLDVM=m -CONFIG_IWLMVM=m -CONFIG_LIBERTAS_THINFIRM=m -CONFIG_LIBERTAS_THINFIRM_USB=m -CONFIG_MWIFIEX=m -CONFIG_MWIFIEX_SDIO=m -CONFIG_MWIFIEX_PCIE=m -CONFIG_MWIFIEX_USB=m -CONFIG_MT7601U=m -CONFIG_MT76x0U=m -CONFIG_MT76x2U=m -CONFIG_MT7663U=m -CONFIG_MT7921E=m -CONFIG_MT7921U=m -CONFIG_MT7925U=m -CONFIG_MT7932_FULLMAC=m -CONFIG_RT2X00=m -CONFIG_RT2500USB=m -CONFIG_RT73USB=m -CONFIG_RT2800USB=m -CONFIG_RT2800USB_RT3573=y -CONFIG_RT2800USB_RT53XX=y -CONFIG_RT2800USB_RT55XX=y -CONFIG_RT2800USB_UNKNOWN=y -CONFIG_RTL8187=m -CONFIG_RTL8192DU=m -CONFIG_RTL8XXXU=m -CONFIG_RTW88=m -CONFIG_RTW88_8822BU=m -CONFIG_RTW88_8822CU=m -CONFIG_RTW88_8723DU=m -CONFIG_RTW88_8821CU=m -CONFIG_RTW88_8821AU=m -CONFIG_RTW88_8812AU=m -CONFIG_RTW88_8814AU=m -CONFIG_RTW89=m -CONFIG_RTW89_8851BU=m -CONFIG_RTW89_8852AU=m -CONFIG_RTW89_8852BU=m -CONFIG_RTW89_8852CU=m -CONFIG_RSI_91X=m -CONFIG_WL18XX=m -CONFIG_WLCORE_SDIO=m -CONFIG_ZD1211RW=m -CONFIG_WWAN=m -CONFIG_MHI_WWAN_CTRL=m -CONFIG_MHI_WWAN_MBIM=m -CONFIG_INPUT_JOYDEV=m -CONFIG_INPUT_EVDEV=y -CONFIG_KEYBOARD_ADC=m -CONFIG_KEYBOARD_GPIO=y -CONFIG_KEYBOARD_GPIO_POLLED=m -CONFIG_KEYBOARD_SNVS_PWRKEY=m -CONFIG_KEYBOARD_IMX_SC_KEY=m -CONFIG_KEYBOARD_CROS_EC=y -CONFIG_KEYBOARD_MTK_PMIC=m -CONFIG_MOUSE_ELAN_I2C=m -CONFIG_MOUSE_SYNAPTICS_USB=m -CONFIG_INPUT_JOYSTICK=y -CONFIG_JOYSTICK_IFORCE=m -CONFIG_JOYSTICK_IFORCE_USB=m -CONFIG_JOYSTICK_XPAD=m -CONFIG_JOYSTICK_XPAD_FF=y -CONFIG_JOYSTICK_XPAD_LEDS=y -CONFIG_JOYSTICK_PXRC=m -CONFIG_INPUT_TABLET=y -CONFIG_TABLET_USB_ACECAD=m -CONFIG_TABLET_USB_AIPTEK=m -CONFIG_TABLET_USB_HANWANG=m -CONFIG_TABLET_USB_KBTAB=m -CONFIG_TABLET_USB_PEGASUS=m -CONFIG_INPUT_TOUCHSCREEN=y -CONFIG_TOUCHSCREEN_APPLE_Z2=m -CONFIG_TOUCHSCREEN_ATMEL_MXT=m -CONFIG_TOUCHSCREEN_GOODIX=m -CONFIG_TOUCHSCREEN_GOODIX_BERLIN_SPI=m -CONFIG_TOUCHSCREEN_ILI210X=m -CONFIG_TOUCHSCREEN_ELAN=m -CONFIG_TOUCHSCREEN_EDT_FT5X06=m -CONFIG_TOUCHSCREEN_USB_COMPOSITE=m -CONFIG_TOUCHSCREEN_HIMAX_HX83112B=m -CONFIG_INPUT_MISC=y -CONFIG_INPUT_AW86927=m -CONFIG_INPUT_BBNSM_PWRKEY=m -CONFIG_INPUT_PM8941_PWRKEY=y -CONFIG_INPUT_PM8XXX_VIBRATOR=m -CONFIG_INPUT_TPS65219_PWRBUTTON=m -CONFIG_INPUT_TPS6594_PWRBUTTON=m -CONFIG_INPUT_UINPUT=m -CONFIG_INPUT_PWM_BEEPER=m -CONFIG_INPUT_PWM_VIBRA=m -CONFIG_INPUT_RK805_PWRKEY=m -CONFIG_INPUT_DA9063_ONKEY=m -CONFIG_INPUT_HISI_POWERKEY=y -CONFIG_INPUT_MACSMC_INPUT=m -CONFIG_RMI4_F1A=y -CONFIG_RMI4_F21=y -CONFIG_RMI4_F34=y -CONFIG_RMI4_F55=y -# CONFIG_SERIO_SERPORT is not set -CONFIG_SERIO_AMBAKMI=y -CONFIG_LEGACY_PTY_COUNT=16 -CONFIG_SERIAL_8250=y -CONFIG_SERIAL_8250_CONSOLE=y -CONFIG_SERIAL_8250_NR_UARTS=8 -CONFIG_SERIAL_8250_EXTENDED=y -CONFIG_SERIAL_8250_SHARE_IRQ=y -CONFIG_SERIAL_8250_BCM2835AUX=y -CONFIG_SERIAL_8250_DW=y -CONFIG_SERIAL_8250_EM=y -CONFIG_SERIAL_8250_OMAP=y -CONFIG_SERIAL_8250_MT6577=y -CONFIG_SERIAL_8250_UNIPHIER=y -CONFIG_SERIAL_OF_PLATFORM=y -CONFIG_SERIAL_AMBA_PL011=y -CONFIG_SERIAL_AMBA_PL011_CONSOLE=y -CONFIG_SERIAL_APPLE_DOCKCHANNEL_EARLYCON=y -CONFIG_SERIAL_APPLE_DOCKCHANNEL=y -CONFIG_SERIAL_MESON=y -CONFIG_SERIAL_MESON_CONSOLE=y -CONFIG_SERIAL_SAMSUNG=y -CONFIG_SERIAL_SAMSUNG_CONSOLE=y -CONFIG_SERIAL_TEGRA=y -CONFIG_SERIAL_TEGRA_TCU=y -CONFIG_SERIAL_IMX=y -CONFIG_SERIAL_IMX_CONSOLE=y -CONFIG_SERIAL_SH_SCI=y -CONFIG_SERIAL_RSCI=y -CONFIG_SERIAL_MSM=y -CONFIG_SERIAL_MSM_CONSOLE=y -CONFIG_SERIAL_QCOM_GENI=y -CONFIG_SERIAL_QCOM_GENI_CONSOLE=y -CONFIG_SERIAL_XILINX_PS_UART=y -CONFIG_SERIAL_XILINX_PS_UART_CONSOLE=y -CONFIG_SERIAL_FSL_LPUART=y -CONFIG_SERIAL_FSL_LPUART_CONSOLE=y -CONFIG_SERIAL_FSL_LINFLEXUART=y -CONFIG_SERIAL_FSL_LINFLEXUART_CONSOLE=y -CONFIG_SERIAL_STM32=y -CONFIG_SERIAL_STM32_CONSOLE=y -CONFIG_SERIAL_MVEBU_UART=y -CONFIG_SERIAL_OWL=y -CONFIG_SERIAL_DEV_BUS=y -CONFIG_VIRTIO_CONSOLE=y -CONFIG_IPMI_HANDLER=m -CONFIG_IPMI_DEVICE_INTERFACE=m -CONFIG_IPMI_SI=m -CONFIG_HW_RANDOM=y -CONFIG_HW_RANDOM_VIRTIO=y -CONFIG_TCG_TPM=y -CONFIG_TCG_TIS=m -CONFIG_TCG_TIS_SPI=m -CONFIG_TCG_TIS_SPI_CR50=y -CONFIG_TCG_TIS_I2C_CR50=m -CONFIG_TCG_TIS_I2C_INFINEON=y -CONFIG_XILLYUSB=m -CONFIG_I2C_CHARDEV=y -CONFIG_I2C_MUX=y -CONFIG_I2C_MUX_PCA954x=y -CONFIG_I2C_MUX_PINCTRL=m -CONFIG_I2C_BCM2835=m -CONFIG_I2C_CADENCE=m -CONFIG_I2C_DESIGNWARE_CORE=y -CONFIG_I2C_GPIO=m -CONFIG_I2C_IMX=y -CONFIG_I2C_IMX_LPI2C=y -CONFIG_I2C_MESON=y -CONFIG_I2C_MT65XX=y -CONFIG_I2C_MV64XXX=y -CONFIG_I2C_OMAP=y -CONFIG_I2C_OWL=y -CONFIG_I2C_APPLE=y -CONFIG_I2C_PXA=y -CONFIG_I2C_QCOM_CCI=m -CONFIG_I2C_QCOM_GENI=m -CONFIG_I2C_QUP=y -CONFIG_I2C_RIIC=y -CONFIG_I2C_RK3X=y -CONFIG_I2C_RZV2M=m -CONFIG_I2C_S3C2410=y -CONFIG_I2C_SH_MOBILE=y -CONFIG_I2C_TEGRA=y -CONFIG_I2C_UNIPHIER_F=y -CONFIG_I2C_XILINX=m -CONFIG_I2C_RCAR=y -CONFIG_I2C_DIOLAN_U2C=m -CONFIG_I2C_DLN2=m -CONFIG_I2C_CP2615=m -CONFIG_I2C_TINY_USB=m -CONFIG_I2C_CROS_EC_TUNNEL=y -CONFIG_SPI=y -CONFIG_SPI_APPLE=m -CONFIG_SPI_APPLE_QSPI=m -CONFIG_SPI_ARMADA_3700=y -CONFIG_SPI_BCM2835=m -CONFIG_SPI_BCM2835AUX=m -CONFIG_SPI_CADENCE_QUADSPI=y -CONFIG_SPI_CH341=m -CONFIG_SPI_DESIGNWARE=m -CONFIG_SPI_DW_DMA=y -CONFIG_SPI_DW_MMIO=m -CONFIG_SPI_DLN2=m -CONFIG_SPI_FSL_LPSPI=y -CONFIG_SPI_FSL_QUADSPI=y -CONFIG_SPI_NXP_FLEXSPI=y -CONFIG_SPI_IMX=m -CONFIG_SPI_FSL_DSPI=y -CONFIG_SPI_MESON_SPICC=m -CONFIG_SPI_MESON_SPIFC=m -CONFIG_SPI_MT65XX=y -CONFIG_SPI_MTK_NOR=m -CONFIG_SPI_OMAP24XX=m -CONFIG_SPI_ORION=y -CONFIG_SPI_PL022=y -CONFIG_SPI_ROCKCHIP=y -CONFIG_SPI_ROCKCHIP_SFC=m -CONFIG_SPI_RPCIF=m -CONFIG_SPI_RSPI=m -CONFIG_SPI_RZV2H_RSPI=m -CONFIG_SPI_RZV2M_CSI=m -CONFIG_SPI_QCOM_QSPI=m -CONFIG_SPI_QUP=y -CONFIG_SPI_QCOM_GENI=m -CONFIG_SPI_S3C64XX=y -CONFIG_SPI_SH_MSIOF=m -CONFIG_SPI_STM32_OSPI=m -CONFIG_SPI_SUN6I=y -CONFIG_SPI_TEGRA210_QUAD=m -CONFIG_SPI_TEGRA114=m -CONFIG_SPI_XILINX=m -CONFIG_SPI_ZYNQMP_GQSPI=m -CONFIG_SPI_SPIDEV=m -CONFIG_SPMI=y -CONFIG_SPMI_APPLE=y -CONFIG_SPMI_MTK_PMIF=m -CONFIG_PINCTRL_APPLE_GPIO=y -CONFIG_PINCTRL_DA9062=m -CONFIG_PINCTRL_MAX77620=y -CONFIG_PINCTRL_RK805=m -CONFIG_PINCTRL_SINGLE=y -CONFIG_PINCTRL_SX150X=m -CONFIG_PINCTRL_OWL=y -CONFIG_PINCTRL_S700=y -CONFIG_PINCTRL_S900=y -CONFIG_PINCTRL_BRCMSTB=y -CONFIG_PINCTRL_BCM2712=y -CONFIG_PINCTRL_IMX_SCMI=y -CONFIG_PINCTRL_IMX91=y -CONFIG_PINCTRL_MSM=y -CONFIG_PINCTRL_ELIZA=y -CONFIG_PINCTRL_GLYMUR=y -CONFIG_PINCTRL_IPQ5018=y -CONFIG_PINCTRL_IPQ5210=y -CONFIG_PINCTRL_IPQ5332=y -CONFIG_PINCTRL_IPQ5424=y -CONFIG_PINCTRL_IPQ8074=y -CONFIG_PINCTRL_IPQ6018=y -CONFIG_PINCTRL_IPQ9574=y -CONFIG_PINCTRL_KAANAPALI=y -CONFIG_PINCTRL_MSM8916=y -CONFIG_PINCTRL_MSM8953=y -CONFIG_PINCTRL_MSM8976=y -CONFIG_PINCTRL_MSM8994=y -CONFIG_PINCTRL_MSM8996=y -CONFIG_PINCTRL_MSM8998=y -CONFIG_PINCTRL_QCM2290=y -CONFIG_PINCTRL_QCS404=y -CONFIG_PINCTRL_QCS615=y -CONFIG_PINCTRL_QCS8300=y -CONFIG_PINCTRL_QDF2XXX=y -CONFIG_PINCTRL_QDU1000=y -CONFIG_PINCTRL_SA8775P=y -CONFIG_PINCTRL_SC7180=y -CONFIG_PINCTRL_SC7280=y -CONFIG_PINCTRL_SC8180X=y -CONFIG_PINCTRL_SC8280XP=y -CONFIG_PINCTRL_SDM660=y -CONFIG_PINCTRL_SDM670=y -CONFIG_PINCTRL_SDM845=y -CONFIG_PINCTRL_SDX75=y -CONFIG_PINCTRL_SM4450=y -CONFIG_PINCTRL_SM6115=y -CONFIG_PINCTRL_SM6125=y -CONFIG_PINCTRL_SM6350=y -CONFIG_PINCTRL_SM6375=y -CONFIG_PINCTRL_MILOS=y -CONFIG_PINCTRL_SM8150=y -CONFIG_PINCTRL_SM8250=y -CONFIG_PINCTRL_SM8350=y -CONFIG_PINCTRL_SM8450=y -CONFIG_PINCTRL_SM8550=y -CONFIG_PINCTRL_SM8650=y -CONFIG_PINCTRL_SM8750=y -CONFIG_PINCTRL_X1E80100=y -CONFIG_PINCTRL_QCOM_SPMI_PMIC=y -CONFIG_PINCTRL_LPASS_LPI=m -CONFIG_PINCTRL_MILOS_LPASS_LPI=m -CONFIG_PINCTRL_SC7280_LPASS_LPI=m -CONFIG_PINCTRL_SM6115_LPASS_LPI=m -CONFIG_PINCTRL_SM8250_LPASS_LPI=m -CONFIG_PINCTRL_SM8450_LPASS_LPI=m -CONFIG_PINCTRL_SC8280XP_LPASS_LPI=m -CONFIG_PINCTRL_SM8550_LPASS_LPI=m -CONFIG_PINCTRL_SM8650_LPASS_LPI=m -CONFIG_PINCTRL_SOPHGO_SG2000=y -CONFIG_GPIO_ALTERA=m -CONFIG_GPIO_DAVINCI=y -CONFIG_GPIO_DWAPB=y -CONFIG_GPIO_MB86S7X=y -CONFIG_GPIO_MPC8XXX=y -CONFIG_GPIO_MXC=y -CONFIG_GPIO_PL061=y -CONFIG_GPIO_RCAR=y -CONFIG_GPIO_SYSCON=y -CONFIG_GPIO_UNIPHIER=y -CONFIG_GPIO_VF610=y -CONFIG_GPIO_VISCONTI=y -CONFIG_GPIO_WCD934X=m -CONFIG_GPIO_XGENE=y -CONFIG_GPIO_XGENE_SB=y -CONFIG_GPIO_XILINX=m -CONFIG_GPIO_ZYNQ=m -CONFIG_GPIO_MAX732X=y -CONFIG_GPIO_PCA953X=y -CONFIG_GPIO_PCA953X_IRQ=y -CONFIG_GPIO_PCF857X=m -CONFIG_GPIO_TPIC2810=m -CONFIG_GPIO_ADP5585=m -CONFIG_GPIO_BD9571MWV=m -CONFIG_GPIO_DLN2=m -CONFIG_GPIO_MACSMC=y -CONFIG_GPIO_MAX77620=y -CONFIG_GPIO_SL28CPLD=m -CONFIG_GPIO_MPSSE=m -CONFIG_GPIO_AGGREGATOR=m -CONFIG_W1=m -CONFIG_W1_MASTER_DS2490=m -CONFIG_W1_SLAVE_THERM=m -CONFIG_W1_SLAVE_SMEM=m -CONFIG_W1_SLAVE_DS2405=m -CONFIG_W1_SLAVE_DS2408=m -CONFIG_W1_SLAVE_DS2413=m -CONFIG_W1_SLAVE_DS2406=m -CONFIG_W1_SLAVE_DS2423=m -CONFIG_W1_SLAVE_DS2805=m -CONFIG_W1_SLAVE_DS2430=m -CONFIG_W1_SLAVE_DS2431=m -CONFIG_W1_SLAVE_DS2433=m -CONFIG_W1_SLAVE_DS2433_CRC=y -CONFIG_W1_SLAVE_DS2438=m -CONFIG_W1_SLAVE_DS2780=m -CONFIG_W1_SLAVE_DS2781=m -CONFIG_W1_SLAVE_DS28E04=m -CONFIG_POWER_RESET_GPIO=y -CONFIG_POWER_RESET_GPIO_RESTART=y -CONFIG_POWER_RESET_MACSMC=y -CONFIG_POWER_RESET_MSM=y -CONFIG_POWER_RESET_QCOM_PON=m -CONFIG_POWER_RESET_TORADEX_EC=m -CONFIG_POWER_RESET_XGENE=y -CONFIG_POWER_RESET_SYSCON=y -CONFIG_POWER_RESET_SYSCON_POWEROFF=y -CONFIG_SYSCON_REBOOT_MODE=y -CONFIG_NVMEM_REBOOT_MODE=m -CONFIG_POWER_SEQUENCING_PCIE_M2=m -CONFIG_BATTERY_QCOM_BATTMGR=m -CONFIG_BATTERY_SBS=m -CONFIG_BATTERY_BQ27XXX=y -CONFIG_BATTERY_MAX17042=m -CONFIG_CHARGER_MT6360=m -CONFIG_CHARGER_BQ25890=m -CONFIG_CHARGER_BQ25980=m -CONFIG_CHARGER_RK817=m -CONFIG_MACSMC_POWER=m -CONFIG_SENSORS_AQUACOMPUTER_D5NEXT=m -CONFIG_SENSORS_ASUS_ROG_RYUJIN=m -CONFIG_SENSORS_ARM_SCMI=y -CONFIG_SENSORS_ARM_SCPI=y -CONFIG_SENSORS_CORSAIR_CPRO=m -CONFIG_SENSORS_CORSAIR_PSU=m -CONFIG_SENSORS_GIGABYTE_WATERFORCE=m -CONFIG_SENSORS_GPIO_FAN=m -CONFIG_SENSORS_JC42=m -CONFIG_SENSORS_POWERZ=m -CONFIG_SENSORS_MACSMC_HWMON=y -CONFIG_SENSORS_LM75=m -CONFIG_SENSORS_LM90=m -CONFIG_SENSORS_NZXT_KRAKEN2=m -CONFIG_SENSORS_NZXT_KRAKEN3=m -CONFIG_SENSORS_NZXT_SMART2=m -CONFIG_SENSORS_PWM_FAN=m -CONFIG_SENSORS_RASPBERRYPI_HWMON=m -CONFIG_SENSORS_SL28CPLD=m -CONFIG_SENSORS_AMC6821=m -CONFIG_SENSORS_INA2XX=m -CONFIG_SENSORS_INA3221=m -CONFIG_SENSORS_TMP102=m -CONFIG_THERMAL_NETLINK=y -CONFIG_THERMAL_DEFAULT_GOV_POWER_ALLOCATOR=y -CONFIG_THERMAL_GOV_STEP_WISE=y -CONFIG_THERMAL_GOV_POWER_ALLOCATOR=y -CONFIG_CPU_THERMAL=y -CONFIG_DEVFREQ_THERMAL=y -CONFIG_THERMAL_EMULATION=y -CONFIG_IMX_SC_THERMAL=m -CONFIG_IMX8MM_THERMAL=m -CONFIG_K3_THERMAL=m -CONFIG_QORIQ_THERMAL=m -CONFIG_SUN8I_THERMAL=y -CONFIG_ROCKCHIP_THERMAL=m -CONFIG_ARMADA_THERMAL=y -CONFIG_MTK_THERMAL=m -CONFIG_MTK_LVTS_THERMAL=m -CONFIG_BCM2711_THERMAL=m -CONFIG_BCM2835_THERMAL=m -CONFIG_BRCMSTB_THERMAL=m -CONFIG_EXYNOS_THERMAL=y -CONFIG_RCAR_THERMAL=y -CONFIG_RCAR_GEN3_THERMAL=y -CONFIG_RZG2L_THERMAL=y -CONFIG_RZG3E_THERMAL=y -CONFIG_RZG3S_THERMAL=m -CONFIG_TEGRA_SOCTHERM=m -CONFIG_TEGRA_BPMP_THERMAL=m -CONFIG_GENERIC_ADC_THERMAL=m -CONFIG_QCOM_TSENS=y -CONFIG_QCOM_SPMI_ADC_TM5=m -CONFIG_QCOM_SPMI_TEMP_ALARM=m -CONFIG_QCOM_LMH=m -CONFIG_UNIPHIER_THERMAL=y -CONFIG_KHADAS_MCU_FAN_THERMAL=m -CONFIG_WATCHDOG=y -CONFIG_XILINX_WATCHDOG=m -CONFIG_XILINX_WINDOW_WATCHDOG=m -CONFIG_SL28CPLD_WATCHDOG=m -CONFIG_ARM_SP805_WATCHDOG=y -CONFIG_ARM_SBSA_WATCHDOG=y -CONFIG_S3C2410_WATCHDOG=y -CONFIG_DW_WATCHDOG=y -CONFIG_K3_RTI_WATCHDOG=m -CONFIG_SUNXI_WATCHDOG=m -CONFIG_NPCM7XX_WATCHDOG=y -CONFIG_IMX2_WDT=y -CONFIG_IMX_SC_WDT=m -CONFIG_IMX7ULP_WDT=m -CONFIG_QCOM_WDT=m -CONFIG_MESON_GXBB_WATCHDOG=m -CONFIG_MESON_WATCHDOG=m -CONFIG_ARM_SMC_WATCHDOG=y -CONFIG_RENESAS_WDT=y -CONFIG_RENESAS_RZG2LWDT=y -CONFIG_RENESAS_RZV2HWDT=y -CONFIG_UNIPHIER_WATCHDOG=y -CONFIG_PM8916_WATCHDOG=m -CONFIG_APPLE_WATCHDOG=y -CONFIG_BCM2835_WDT=y -CONFIG_BCM7038_WDT=m -CONFIG_USBPCWATCHDOG=m -CONFIG_MFD_ADP5585=m -CONFIG_MFD_ALTERA_SYSMGR=y -CONFIG_MFD_BD9571MWV=y -CONFIG_MFD_AXP20X_I2C=y -CONFIG_MFD_AXP20X_RSB=y -CONFIG_MFD_MACSMC=y -CONFIG_MFD_DA9062=m -CONFIG_MFD_DLN2=m -CONFIG_MFD_EXYNOS_LPASS=m -CONFIG_MFD_HI6421_PMIC=y -CONFIG_MFD_HI655X_PMIC=y -CONFIG_MFD_MAX77620=y -CONFIG_MFD_MAX77759=m -CONFIG_MFD_MT6360=y -CONFIG_MFD_MT6397=y -CONFIG_MFD_SPMI_PMIC=y -CONFIG_MFD_RK8XX_I2C=y -CONFIG_MFD_RK8XX_SPI=y -CONFIG_MFD_SEC_ACPM=m -CONFIG_MFD_SEC_I2C=y -CONFIG_MFD_SL28CPLD=y -CONFIG_RZ_MTU3=y -CONFIG_MFD_TI_AM335X_TSCADC=m -CONFIG_MFD_TI_LP873X=m -CONFIG_MFD_TPS65219=y -CONFIG_MFD_TPS6594_I2C=m -CONFIG_MFD_ROHM_BD718XX=y -CONFIG_MFD_STM32_LPTIMER=m -CONFIG_MFD_STM32_TIMERS=m -CONFIG_MFD_WCD934X=m -CONFIG_MFD_KHADAS_MCU=m -CONFIG_MFD_QCOM_PM8008=m -CONFIG_REGULATOR_FIXED_VOLTAGE=y -CONFIG_REGULATOR_AXP20X=y -CONFIG_REGULATOR_BD718XX=y -CONFIG_REGULATOR_BD9571MWV=y -CONFIG_REGULATOR_CROS_EC=y -CONFIG_REGULATOR_DA9211=m -CONFIG_REGULATOR_FAN53555=y -CONFIG_REGULATOR_GPIO=y -CONFIG_REGULATOR_HI6421V530=y -CONFIG_REGULATOR_HI655X=y -CONFIG_REGULATOR_LP873X=m -CONFIG_REGULATOR_MAX77620=y -CONFIG_REGULATOR_MAX8973=y -CONFIG_REGULATOR_MAX20411=m -CONFIG_REGULATOR_MP8859=y -CONFIG_REGULATOR_MT6315=m -CONFIG_REGULATOR_MT6357=y -CONFIG_REGULATOR_MT6358=y -CONFIG_REGULATOR_MT6359=y -CONFIG_REGULATOR_MT6360=y -CONFIG_REGULATOR_MT6397=y -CONFIG_REGULATOR_PCA9450=y -CONFIG_REGULATOR_PF8X00=y -CONFIG_REGULATOR_PFUZE100=y -CONFIG_REGULATOR_PWM=y -CONFIG_REGULATOR_QCOM_PM8008=m -CONFIG_REGULATOR_QCOM_REFGEN=m -CONFIG_REGULATOR_QCOM_RPMH=y -CONFIG_REGULATOR_QCOM_SMD_RPM=y -CONFIG_REGULATOR_QCOM_SPMI=y -CONFIG_REGULATOR_QCOM_USB_VBUS=m -CONFIG_REGULATOR_RAA215300=y -CONFIG_REGULATOR_RK808=y -CONFIG_REGULATOR_S2MPS11=y -CONFIG_REGULATOR_TPS65132=m -CONFIG_REGULATOR_TPS65219=y -CONFIG_REGULATOR_VCTRL=m -CONFIG_RC_CORE=m -CONFIG_RC_DECODERS=y -CONFIG_RC_DEVICES=y -CONFIG_IR_GPIO_CIR=m -CONFIG_IR_MESON=m -CONFIG_IR_SUNXI=m -CONFIG_MEDIA_CEC_SUPPORT=y -CONFIG_CEC_MESON_G12A_AO=m -CONFIG_MEDIA_SUPPORT=m -CONFIG_MEDIA_CAMERA_SUPPORT=y -CONFIG_MEDIA_ANALOG_TV_SUPPORT=y -CONFIG_MEDIA_DIGITAL_TV_SUPPORT=y -CONFIG_MEDIA_SDR_SUPPORT=y -CONFIG_MEDIA_PLATFORM_SUPPORT=y -# CONFIG_DVB_NET is not set -CONFIG_MEDIA_USB_SUPPORT=y -CONFIG_USB_GSPCA=m -CONFIG_USB_GSPCA_BENQ=m -CONFIG_USB_GSPCA_CONEX=m -CONFIG_USB_GSPCA_CPIA1=m -CONFIG_USB_GSPCA_DTCS033=m -CONFIG_USB_GSPCA_ETOMS=m -CONFIG_USB_GSPCA_FINEPIX=m -CONFIG_USB_GSPCA_JEILINJ=m -CONFIG_USB_GSPCA_JL2005BCD=m -CONFIG_USB_GSPCA_KINECT=m -CONFIG_USB_GSPCA_KONICA=m -CONFIG_USB_GSPCA_MARS=m -CONFIG_USB_GSPCA_MR97310A=m -CONFIG_USB_GSPCA_NW80X=m -CONFIG_USB_GSPCA_OV519=m -CONFIG_USB_GSPCA_OV534=m -CONFIG_USB_GSPCA_OV534_9=m -CONFIG_USB_GSPCA_PAC207=m -CONFIG_USB_GSPCA_PAC7302=m -CONFIG_USB_GSPCA_PAC7311=m -CONFIG_USB_GSPCA_SE401=m -CONFIG_USB_GSPCA_SN9C2028=m -CONFIG_USB_GSPCA_SN9C20X=m -CONFIG_USB_GSPCA_SONIXB=m -CONFIG_USB_GSPCA_SONIXJ=m -CONFIG_USB_GSPCA_SPCA1528=m -CONFIG_USB_GSPCA_SPCA500=m -CONFIG_USB_GSPCA_SPCA501=m -CONFIG_USB_GSPCA_SPCA505=m -CONFIG_USB_GSPCA_SPCA506=m -CONFIG_USB_GSPCA_SPCA508=m -CONFIG_USB_GSPCA_SPCA561=m -CONFIG_USB_GSPCA_SQ905=m -CONFIG_USB_GSPCA_SQ905C=m -CONFIG_USB_GSPCA_SQ930X=m -CONFIG_USB_GSPCA_STK014=m -CONFIG_USB_GSPCA_STK1135=m -CONFIG_USB_GSPCA_STV0680=m -CONFIG_USB_GSPCA_SUNPLUS=m -CONFIG_USB_GSPCA_T613=m -CONFIG_USB_GSPCA_TOPRO=m -CONFIG_USB_GSPCA_TOUPTEK=m -CONFIG_USB_GSPCA_TV8532=m -CONFIG_USB_GSPCA_VC032X=m -CONFIG_USB_GSPCA_VICAM=m -CONFIG_USB_GSPCA_XIRLINK_CIT=m -CONFIG_USB_GSPCA_ZC3XX=m -CONFIG_USB_GL860=m -CONFIG_USB_M5602=m -CONFIG_USB_STV06XX=m -CONFIG_USB_PWC=m -CONFIG_USB_VIDEO_CLASS=m -CONFIG_V4L_PLATFORM_DRIVERS=y -CONFIG_SDR_PLATFORM_DRIVERS=y -CONFIG_V4L_MEM2MEM_DRIVERS=y -CONFIG_VIDEO_AMPHION_VPU=m -CONFIG_VIDEO_APPLE_ISP=m -CONFIG_VIDEO_APPLE_AVD=m -CONFIG_VIDEO_CADENCE_CSI2RX=m -CONFIG_VIDEO_WAVE_VPU=m -CONFIG_VIDEO_E5010_JPEG_ENC=m -CONFIG_VIDEO_MEDIATEK_JPEG=m -CONFIG_VIDEO_MEDIATEK_VCODEC=m -CONFIG_VIDEO_MEDIATEK_MDP3=m -CONFIG_VIDEO_IMX7_CSI=m -CONFIG_VIDEO_IMX_MIPI_CSIS=m -CONFIG_VIDEO_IMX8_ISI=m -CONFIG_VIDEO_IMX8_ISI_M2M=y -CONFIG_VIDEO_IMX8_JPEG=m -CONFIG_VIDEO_QCOM_CAMSS=m -CONFIG_VIDEO_QCOM_IRIS=m -CONFIG_VIDEO_QCOM_VENUS=m -CONFIG_VIDEO_RCAR_CSI2=m -CONFIG_VIDEO_RCAR_ISP=m -CONFIG_VIDEO_RCAR_VIN=m -CONFIG_VIDEO_RZG2L_CSI2=m -CONFIG_VIDEO_RZG2L_CRU=m -CONFIG_VIDEO_RENESAS_FCP=m -CONFIG_VIDEO_RENESAS_FDP1=m -CONFIG_VIDEO_RENESAS_VSP1=m -CONFIG_VIDEO_RCAR_DRIF=m -CONFIG_VIDEO_ROCKCHIP_RGA=m -CONFIG_VIDEO_ROCKCHIP_CIF=m -CONFIG_VIDEO_SAMSUNG_EXYNOS_GSC=m -CONFIG_VIDEO_SAMSUNG_S5P_JPEG=m -CONFIG_VIDEO_SAMSUNG_S5P_MFC=m -CONFIG_VIDEO_SUN6I_CSI=m -CONFIG_VIDEO_SYNOPSYS_HDMIRX=m -CONFIG_VIDEO_SYNOPSYS_HDMIRX_LOAD_DEFAULT_EDID=y -CONFIG_VIDEO_TI_J721E_CSI2RX=m -CONFIG_VIDEO_HANTRO=m -CONFIG_VIDEO_IMX219=m -CONFIG_VIDEO_IMX412=m -CONFIG_VIDEO_OV5640=m -CONFIG_VIDEO_OV5645=m -CONFIG_VIDEO_S5KJN1=m -CONFIG_DRM=y -CONFIG_DRM_ADP=m -CONFIG_DRM_APPLE=m -CONFIG_DRM_HDLCD=m -CONFIG_DRM_MALI_DISPLAY=m -CONFIG_DRM_KOMEDA=m -CONFIG_DRM_ASAHI=m -CONFIG_DRM_FSL_LDB=m -CONFIG_DRM_I2C_NXP_TDA998X=m -CONFIG_DRM_ITE_IT6263=m -CONFIG_DRM_LONTIUM_LT8912B=m -CONFIG_DRM_LONTIUM_LT9611=m -CONFIG_DRM_LONTIUM_LT9611UXC=m -CONFIG_DRM_LONTIUM_LT8713SX=m -CONFIG_DRM_ITE_IT66121=m -CONFIG_DRM_NWL_MIPI_DSI=m -CONFIG_DRM_PARADE_PS8640=m -CONFIG_DRM_SII902X=m -CONFIG_DRM_SIMPLE_BRIDGE=m -CONFIG_DRM_THINE_THC63LVD1024=m -CONFIG_DRM_TOSHIBA_TC358767=m -CONFIG_DRM_TOSHIBA_TC358768=m -CONFIG_DRM_TI_TFP410=m -CONFIG_DRM_TI_SN65DSI83=m -CONFIG_DRM_TI_SN65DSI86=m -CONFIG_DRM_ANALOGIX_ANX7625=m -CONFIG_DRM_I2C_ADV7511=m -CONFIG_DRM_I2C_ADV7511_AUDIO=y -CONFIG_DRM_CDNS_DSI=m -CONFIG_DRM_CDNS_MHDP8546=m -CONFIG_DRM_IMX8MP_DW_HDMI_BRIDGE=m -CONFIG_DRM_DW_HDMI_AHB_AUDIO=m -CONFIG_DRM_DW_HDMI_CEC=m -CONFIG_DRM_DW_HDMI_QP_CEC=y -CONFIG_DRM_ETNAVIV=m -CONFIG_DRM_EXYNOS=m -CONFIG_DRM_EXYNOS5433_DECON=y -CONFIG_DRM_EXYNOS7_DECON=y -CONFIG_DRM_EXYNOS_DSI=y -# CONFIG_DRM_EXYNOS_DP is not set -CONFIG_DRM_EXYNOS_HDMI=y -CONFIG_DRM_EXYNOS_MIC=y -CONFIG_DRM_GUD=m -CONFIG_DRM_HISI_HIBMC=m -CONFIG_DRM_HISI_KIRIN=m -CONFIG_DRM_POWERVR=m -CONFIG_DRM_IMX_DCSS=m -CONFIG_DRM_LIMA=m -CONFIG_DRM_MEDIATEK=m -CONFIG_DRM_MEDIATEK_DP=m -CONFIG_DRM_MEDIATEK_HDMI=m -CONFIG_DRM_MEDIATEK_HDMI_V2=m -CONFIG_DRM_MESON=m -CONFIG_DRM_MSM=m -CONFIG_DRM_MXSFB=m -CONFIG_DRM_IMX_LCDIF=m -CONFIG_DRM_NOUVEAU=m -CONFIG_DRM_PANEL_BOE_TV101WUM_NL6=m -CONFIG_DRM_PANEL_LVDS=m -CONFIG_DRM_PANEL_HIMAX_HX8279=m -CONFIG_DRM_PANEL_HIMAX_HX83112A=m -CONFIG_DRM_PANEL_HIMAX_HX83112B=m -CONFIG_DRM_PANEL_ILITEK_ILI9882T=m -CONFIG_DRM_PANEL_KHADAS_TS050=m -CONFIG_DRM_PANEL_MANTIX_MLAF057WE51=m -CONFIG_DRM_PANEL_NOVATEK_NT36672A=m -CONFIG_DRM_PANEL_NOVATEK_NT36672E=m -CONFIG_DRM_PANEL_NOVATEK_NT37801=m -CONFIG_DRM_PANEL_RAYDIUM_RM67191=m -CONFIG_DRM_PANEL_RAYDIUM_RM692E5=m -CONFIG_DRM_PANEL_SAMSUNG_ATNA33XC20=m -CONFIG_DRM_PANEL_SITRONIX_ST7703=m -CONFIG_DRM_PANEL_STARTEK_KD070FHFID015=m -CONFIG_DRM_PANEL_EDP=m -CONFIG_DRM_PANEL_SIMPLE=m -CONFIG_DRM_PANEL_SUMMIT=m -CONFIG_DRM_PANEL_TRULY_NT35597_WQXGA=m -CONFIG_DRM_PANEL_VISIONOX_VTDR6130=m -CONFIG_DRM_PANFROST=m -CONFIG_DRM_PANTHOR=m -CONFIG_DRM_PL111=m -CONFIG_DRM_RCAR_DU=m -CONFIG_DRM_RCAR_DW_HDMI=m -CONFIG_DRM_RZG2L_DU=m -CONFIG_DRM_ROCKCHIP=m -CONFIG_ROCKCHIP_VOP2=y -CONFIG_ROCKCHIP_ANALOGIX_DP=y -CONFIG_ROCKCHIP_CDN_DP=y -CONFIG_ROCKCHIP_DW_DP=y -CONFIG_ROCKCHIP_DW_HDMI=y -CONFIG_ROCKCHIP_DW_HDMI_QP=y -CONFIG_ROCKCHIP_DW_MIPI_DSI=y -CONFIG_ROCKCHIP_INNO_HDMI=y -CONFIG_ROCKCHIP_LVDS=y -CONFIG_DRM_STM=m -CONFIG_DRM_STM_LVDS=m -CONFIG_DRM_SUN4I=m -CONFIG_DRM_SIMPLEDRM=y -CONFIG_DRM_TEGRA=m -CONFIG_DRM_TIDSS=m -CONFIG_DRM_GM12U320=m -CONFIG_DRM_UDL=m -CONFIG_DRM_V3D=m -CONFIG_DRM_VC4=m -CONFIG_DRM_ZYNQMP_DPSUB=m -CONFIG_DRM_ZYNQMP_DPSUB_AUDIO=y -CONFIG_FB=y -CONFIG_FB_EFI=y -CONFIG_BACKLIGHT_CLASS_DEVICE=y -CONFIG_BACKLIGHT_PWM=m -CONFIG_BACKLIGHT_APPLE_DWI=m -CONFIG_BACKLIGHT_QCOM_WLED=m -CONFIG_BACKLIGHT_LP855X=m -CONFIG_BACKLIGHT_GPIO=m -CONFIG_LOGO=y -CONFIG_LOGO_LINUX_CLUT224_FILE="drivers/video/logo/logo_linux_clut224.ppm" -CONFIG_DRM_ACCEL=y -CONFIG_DRM_ACCEL_ANE=m -CONFIG_SOUND=m -CONFIG_SND=m -CONFIG_SND_UMP_LEGACY_RAWMIDI=y -CONFIG_SND_HRTIMER=m -CONFIG_SND_UTIMER=y -CONFIG_SND_SEQUENCER=m -CONFIG_SND_SEQ_DUMMY=m -CONFIG_SND_DUMMY=m -CONFIG_SND_ALOOP=m -CONFIG_SND_VIRMIDI=m -CONFIG_SND_HDA_TEGRA=m -CONFIG_SND_HDA_CODEC_HDMI=m -CONFIG_SND_USB_AUDIO=m -CONFIG_SND_USB_AUDIO_MIDI_V2=y -CONFIG_SND_USB_UA101=m -CONFIG_SND_USB_CAIAQ=m -CONFIG_SND_USB_CAIAQ_INPUT=y -CONFIG_SND_USB_6FIRE=m -CONFIG_SND_USB_HIFACE=m -CONFIG_SND_BCD2000=m -CONFIG_SND_USB_AUDIO_QMI=m -CONFIG_SND_USB_POD=m -CONFIG_SND_USB_PODHD=m -CONFIG_SND_USB_TONEPORT=m -CONFIG_SND_USB_VARIAX=m -CONFIG_SND_SOC=m -CONFIG_SND_SOC_USB=m -CONFIG_SND_SOC_APPLE_AOP_AUDIO=m -CONFIG_SND_SOC_APPLE_T8140_AOP_AUDIO=m -CONFIG_SND_SOC_APPLE_MACAUDIO=m -CONFIG_SND_BCM2835_SOC_I2S=m -CONFIG_SND_SOC_FSL_ASRC=m -CONFIG_SND_SOC_FSL_MICFIL=m -CONFIG_SND_SOC_FSL_EASRC=m -CONFIG_SND_IMX_SOC=m -CONFIG_SND_SOC_IMX_SGTL5000=m -CONFIG_SND_SOC_FSL_ASOC_CARD=m -CONFIG_SND_SOC_IMX_AUDMIX=m -CONFIG_SND_SOC_MT8183=m -CONFIG_SND_SOC_MT8183_MT6358_TS3A227E_MAX98357A=m -CONFIG_SND_SOC_MT8183_DA7219_MAX98357A=m -CONFIG_SND_SOC_MT8188=m -CONFIG_SND_SOC_MT8188_MT6359=m -CONFIG_SND_SOC_MT8192=m -CONFIG_SND_SOC_MT8192_MT6359_RT1015_RT5682=m -CONFIG_SND_SOC_MT8195=m -CONFIG_SND_SOC_MT8195_MT6359=m -CONFIG_SND_SOC_MT8365=m -CONFIG_SND_SOC_MT8365_MT6357=m -CONFIG_SND_MESON_AXG_SOUND_CARD=m -CONFIG_SND_MESON_GX_SOUND_CARD=m -CONFIG_SND_SOC_QCOM=m -CONFIG_SND_SOC_APQ8016_SBC=m -CONFIG_SND_SOC_QDSP6_USB=m -CONFIG_SND_SOC_MSM8996=m -CONFIG_SND_SOC_SDM845=m -CONFIG_SND_SOC_SM8250=m -CONFIG_SND_SOC_SC8280XP=m -CONFIG_SND_SOC_SC7180=m -CONFIG_SND_SOC_SC7280=m -CONFIG_SND_SOC_X1E80100=m -CONFIG_SND_SOC_RCAR=m -CONFIG_SND_SOC_MSIOF=m -CONFIG_SND_SOC_RZ=m -CONFIG_SND_SOC_ROCKCHIP_I2S_TDM=m -CONFIG_SND_SOC_ROCKCHIP_SAI=m -CONFIG_SND_SOC_ROCKCHIP_SPDIF=m -CONFIG_SND_SOC_ROCKCHIP_RT5645=m -CONFIG_SND_SOC_RK3399_GRU_SOUND=m -CONFIG_SND_SOC_SAMSUNG=m -CONFIG_SND_SUN8I_CODEC=m -CONFIG_SND_SUN8I_CODEC_ANALOG=m -CONFIG_SND_SUN50I_CODEC_ANALOG=m -CONFIG_SND_SUN4I_I2S=m -CONFIG_SND_SUN4I_SPDIF=m -CONFIG_SND_SOC_TEGRA=m -CONFIG_SND_SOC_TEGRA210_AHUB=m -CONFIG_SND_SOC_TEGRA210_DMIC=m -CONFIG_SND_SOC_TEGRA210_I2S=m -CONFIG_SND_SOC_TEGRA210_OPE=m -CONFIG_SND_SOC_TEGRA186_ASRC=m -CONFIG_SND_SOC_TEGRA186_DSPK=m -CONFIG_SND_SOC_TEGRA210_ADMAIF=m -CONFIG_SND_SOC_TEGRA210_MVC=m -CONFIG_SND_SOC_TEGRA210_SFC=m -CONFIG_SND_SOC_TEGRA210_AMX=m -CONFIG_SND_SOC_TEGRA210_ADX=m -CONFIG_SND_SOC_TEGRA210_MIXER=m -CONFIG_SND_SOC_TEGRA_AUDIO_GRAPH_CARD=m -CONFIG_SND_SOC_J721E_EVM=m -CONFIG_SND_SOC_XILINX_I2S=m -CONFIG_SND_SOC_XILINX_AUDIO_FORMATTER=m -CONFIG_SND_SOC_XILINX_SPDIF=m -CONFIG_SND_SOC_SOF_TOPLEVEL=y -CONFIG_SND_SOC_SOF_OF=m -CONFIG_SND_SOC_SOF_MTK_TOPLEVEL=y -CONFIG_SND_SOC_SOF_MT8186=m -CONFIG_SND_SOC_SOF_MT8195=m -CONFIG_SND_SOC_AK4613=m -CONFIG_SND_SOC_AK4619=m -CONFIG_SND_SOC_DA7213=m -CONFIG_SND_SOC_ES7134=m -CONFIG_SND_SOC_ES7241=m -CONFIG_SND_SOC_ES8316=m -CONFIG_SND_SOC_ES8328_I2C=m -CONFIG_SND_SOC_GTM601=m -CONFIG_SND_SOC_MAX98090=m -CONFIG_SND_SOC_MSM8916_WCD_ANALOG=m -CONFIG_SND_SOC_MSM8916_WCD_DIGITAL=m -CONFIG_SND_SOC_RK3308=m -CONFIG_SND_SOC_RK817=m -CONFIG_SND_SOC_RT5640=m -CONFIG_SND_SOC_RT5659=m -CONFIG_SND_SOC_SIMPLE_AMPLIFIER=m -CONFIG_SND_SOC_SIMPLE_MUX=m -CONFIG_SND_SOC_TAS2552=m -CONFIG_SND_SOC_TAS571X=m -CONFIG_SND_SOC_TLV320AIC32X4_I2C=m -CONFIG_SND_SOC_TLV320AIC3X_I2C=m -CONFIG_SND_SOC_WCD9335=m -CONFIG_SND_SOC_WCD934X=m -CONFIG_SND_SOC_WCD937X_SDW=m -CONFIG_SND_SOC_WCD939X_SDW=m -CONFIG_SND_SOC_WM8524=m -CONFIG_SND_SOC_WM8904=m -CONFIG_SND_SOC_WM8960=m -CONFIG_SND_SOC_WM8962=m -CONFIG_SND_SOC_WM8978=m -CONFIG_SND_SOC_WSA881X=m -CONFIG_SND_SOC_WSA883X=m -CONFIG_SND_SOC_WSA884X=m -CONFIG_SND_SOC_NAU8822=m -CONFIG_SND_SOC_LPASS_WSA_MACRO=m -CONFIG_SND_SOC_LPASS_VA_MACRO=m -CONFIG_SND_SIMPLE_CARD=m -CONFIG_SND_AUDIO_GRAPH_CARD=m -CONFIG_SND_AUDIO_GRAPH_CARD2=m -CONFIG_HID_BATTERY_STRENGTH=y -CONFIG_HIDRAW=y -CONFIG_UHID=y -CONFIG_HID_A4TECH=m -CONFIG_HID_ACCUTOUCH=m -CONFIG_HID_ACRUX=m -CONFIG_HID_ACRUX_FF=y -CONFIG_HID_APPLE=y -CONFIG_HID_APPLEIR=m -CONFIG_HID_AUREAL=m -CONFIG_HID_BELKIN=m -CONFIG_HID_BETOP_FF=m -CONFIG_HID_BIGBEN_FF=m -CONFIG_HID_CHERRY=m -CONFIG_HID_CHICONY=m -CONFIG_HID_CORSAIR=m -CONFIG_HID_COUGAR=m -CONFIG_HID_MACALLY=m -CONFIG_HID_PRODIKEYS=m -CONFIG_HID_CMEDIA=m -CONFIG_HID_CP2112=m -CONFIG_HID_CREATIVE_SB0540=m -CONFIG_HID_CYPRESS=m -CONFIG_HID_DRAGONRISE=m -CONFIG_DRAGONRISE_FF=y -CONFIG_HID_EMS_FF=m -CONFIG_HID_ELAN=m -CONFIG_HID_ELECOM=m -CONFIG_HID_ELO=m -CONFIG_HID_EVISION=m -CONFIG_HID_EZKEY=m -CONFIG_HID_FT260=m -CONFIG_HID_GEMBIRD=m -CONFIG_HID_GFRM=m -CONFIG_HID_GLORIOUS=m -CONFIG_HID_HOLTEK=m -CONFIG_HOLTEK_FF=y -CONFIG_HID_GOOGLE_STADIA_FF=m -CONFIG_HID_VIVALDI=m -CONFIG_HID_GT683R=m -CONFIG_HID_KEYTOUCH=m -CONFIG_HID_KYE=m -CONFIG_HID_KYSONA=m -CONFIG_HID_UCLOGIC=m -CONFIG_HID_WALTOP=m -CONFIG_HID_VIEWSONIC=m -CONFIG_HID_XIAOMI=m -CONFIG_HID_GYRATION=m -CONFIG_HID_ICADE=m -CONFIG_HID_ITE=m -CONFIG_HID_JABRA=m -CONFIG_HID_TWINHAN=m -CONFIG_HID_KENSINGTON=m -CONFIG_HID_LCPOWER=m -CONFIG_HID_LENOVO=m -CONFIG_HID_LENOVO_GO=m -CONFIG_HID_LENOVO_GO_S=m -CONFIG_HID_LETSKETCH=m -CONFIG_HID_LOGITECH=m -CONFIG_HID_LOGITECH_DJ=m -CONFIG_LOGITECH_FF=y -CONFIG_LOGIRUMBLEPAD2_FF=y -CONFIG_LOGIG940_FF=y -CONFIG_HID_MAGICMOUSE=y -CONFIG_HID_MALTRON=m -CONFIG_HID_MAYFLASH=m -CONFIG_HID_MEGAWORLD_FF=m -CONFIG_HID_MICROSOFT=m -CONFIG_HID_MONTEREY=m -CONFIG_HID_MULTITOUCH=m -CONFIG_HID_NINTENDO=m -CONFIG_NINTENDO_FF=y -CONFIG_HID_NTI=m -CONFIG_HID_NTRIG=m -CONFIG_HID_NVIDIA_SHIELD=m -CONFIG_NVIDIA_SHIELD_FF=y -CONFIG_HID_ORTEK=m -CONFIG_HID_PANTHERLORD=m -CONFIG_PANTHERLORD_FF=y -CONFIG_HID_PENMOUNT=m -CONFIG_HID_PETALYNX=m -CONFIG_HID_PICOLCD=m -CONFIG_HID_PLANTRONICS=m -CONFIG_HID_PLAYSTATION=m -CONFIG_PLAYSTATION_FF=y -CONFIG_HID_PXRC=m -CONFIG_HID_RAPOO=m -CONFIG_HID_RAZER=m -CONFIG_HID_PRIMAX=m -CONFIG_HID_RETRODE=m -CONFIG_HID_ROCCAT=m -CONFIG_HID_SAITEK=m -CONFIG_HID_SAMSUNG=m -CONFIG_HID_SEMITEK=m -CONFIG_HID_SIGMAMICRO=m -CONFIG_HID_SONY=m -CONFIG_SONY_FF=y -CONFIG_HID_SPEEDLINK=m -CONFIG_HID_STEAM=m -CONFIG_STEAM_FF=y -CONFIG_HID_STEELSERIES=m -CONFIG_HID_SUNPLUS=m -CONFIG_HID_RMI=m -CONFIG_HID_GREENASIA=m -CONFIG_GREENASIA_FF=y -CONFIG_HID_SMARTJOYPLUS=m -CONFIG_SMARTJOYPLUS_FF=y -CONFIG_HID_TIVO=m -CONFIG_HID_TOPSEED=m -CONFIG_HID_TOPRE=m -CONFIG_HID_THINGM=m -CONFIG_HID_THRUSTMASTER=m -CONFIG_THRUSTMASTER_FF=y -CONFIG_HID_UDRAW_PS3=m -CONFIG_HID_U2FZERO=m -CONFIG_HID_UNIVERSAL_PIDFF=m -CONFIG_HID_WACOM=m -CONFIG_HID_WIIMOTE=m -CONFIG_HID_WINWING=m -CONFIG_HID_XINMO=m -CONFIG_HID_ZEROPLUS=m -CONFIG_ZEROPLUS_FF=y -CONFIG_HID_ZYDACRON=m -CONFIG_HID_ALPS=m -CONFIG_HID_MCP2221=m -CONFIG_HID_HUAWEI=m -CONFIG_I2C_HID_ACPI=m -CONFIG_I2C_HID_OF=m -CONFIG_I2C_HID_OF_ELAN=m -CONFIG_HID_PID=y -CONFIG_USB_HIDDEV=y -CONFIG_SPI_HID_APPLE_OF=m -CONFIG_HID_DOCKCHANNEL=y -CONFIG_USB=y -CONFIG_USB_OTG=y -CONFIG_USB_MON=m -CONFIG_USB_XHCI_HCD=y -CONFIG_USB_XHCI_PCI_RENESAS=m -CONFIG_USB_XHCI_RZV2M=y -CONFIG_USB_XHCI_SIDEBAND=y -CONFIG_USB_XHCI_TEGRA=y -CONFIG_USB_BRCMSTB=m -CONFIG_USB_EHCI_HCD=y -CONFIG_USB_EHCI_EXYNOS=y -CONFIG_USB_EHCI_HCD_PLATFORM=y -CONFIG_USB_OHCI_HCD=y -CONFIG_USB_OHCI_EXYNOS=y -CONFIG_USB_OHCI_HCD_PLATFORM=y -CONFIG_USB_RENESAS_USBHS_HCD=m -CONFIG_USB_RENESAS_USBHS=m -CONFIG_USB_PRINTER=m -CONFIG_USB_TMC=m -CONFIG_USB_STORAGE=y -CONFIG_USB_STORAGE_REALTEK=m -CONFIG_USB_STORAGE_DATAFAB=m -CONFIG_USB_STORAGE_FREECOM=m -CONFIG_USB_STORAGE_ISD200=m -CONFIG_USB_STORAGE_USBAT=m -CONFIG_USB_STORAGE_SDDR09=m -CONFIG_USB_STORAGE_SDDR55=m -CONFIG_USB_STORAGE_JUMPSHOT=m -CONFIG_USB_STORAGE_ALAUDA=m -CONFIG_USB_STORAGE_ONETOUCH=m -CONFIG_USB_STORAGE_KARMA=m -CONFIG_USB_STORAGE_CYPRESS_ATACB=m -CONFIG_USB_STORAGE_ENE_UB6250=m -CONFIG_USB_UAS=m -CONFIG_USB_MDC800=m -CONFIG_USB_MICROTEK=m -CONFIG_USBIP_CORE=m -CONFIG_USBIP_VHCI_HCD=m -CONFIG_USBIP_HOST=m -CONFIG_USB_CDNS_SUPPORT=m -CONFIG_USB_CDNS3=m -CONFIG_USB_CDNS3_GADGET=y -CONFIG_USB_CDNS3_HOST=y -CONFIG_USB_MTU3=y -CONFIG_USB_MUSB_HDRC=y -CONFIG_USB_MUSB_SUNXI=y -CONFIG_USB_DWC3=y -CONFIG_USB_DWC2=y -CONFIG_USB_CHIPIDEA=y -CONFIG_USB_CHIPIDEA_UDC=y -CONFIG_USB_CHIPIDEA_HOST=y -CONFIG_USB_ISP1760=y -CONFIG_USB_SERIAL=m -CONFIG_USB_SERIAL_GENERIC=y -CONFIG_USB_SERIAL_SIMPLE=m -CONFIG_USB_SERIAL_AIRCABLE=m -CONFIG_USB_SERIAL_ARK3116=m -CONFIG_USB_SERIAL_BELKIN=m -CONFIG_USB_SERIAL_CH341=m -CONFIG_USB_SERIAL_WHITEHEAT=m -CONFIG_USB_SERIAL_DIGI_ACCELEPORT=m -CONFIG_USB_SERIAL_CP210X=m -CONFIG_USB_SERIAL_CYPRESS_M8=m -CONFIG_USB_SERIAL_EMPEG=m -CONFIG_USB_SERIAL_FTDI_SIO=m -CONFIG_USB_SERIAL_VISOR=m -CONFIG_USB_SERIAL_IPAQ=m -CONFIG_USB_SERIAL_IR=m -CONFIG_USB_SERIAL_EDGEPORT=m -CONFIG_USB_SERIAL_EDGEPORT_TI=m -CONFIG_USB_SERIAL_F81232=m -CONFIG_USB_SERIAL_F8153X=m -CONFIG_USB_SERIAL_GARMIN=m -CONFIG_USB_SERIAL_IPW=m -CONFIG_USB_SERIAL_IUU=m -CONFIG_USB_SERIAL_KEYSPAN_PDA=m -CONFIG_USB_SERIAL_KEYSPAN=m -CONFIG_USB_SERIAL_KLSI=m -CONFIG_USB_SERIAL_KOBIL_SCT=m -CONFIG_USB_SERIAL_MCT_U232=m -CONFIG_USB_SERIAL_MOS7720=m -CONFIG_USB_SERIAL_MOS7840=m -CONFIG_USB_SERIAL_NAVMAN=m -CONFIG_USB_SERIAL_PL2303=m -CONFIG_USB_SERIAL_OTI6858=m -CONFIG_USB_SERIAL_QCAUX=m -CONFIG_USB_SERIAL_QUALCOMM=m -CONFIG_USB_SERIAL_SPCP8X5=m -CONFIG_USB_SERIAL_SAFE=m -CONFIG_USB_SERIAL_SAFE_PADDED=y -CONFIG_USB_SERIAL_SIERRAWIRELESS=m -CONFIG_USB_SERIAL_SYMBOL=m -CONFIG_USB_SERIAL_TI=m -CONFIG_USB_SERIAL_CYBERJACK=m -CONFIG_USB_SERIAL_OPTION=m -CONFIG_USB_SERIAL_OMNINET=m -CONFIG_USB_SERIAL_OPTICON=m -CONFIG_USB_SERIAL_XSENS_MT=m -CONFIG_USB_SERIAL_SSU100=m -CONFIG_USB_SERIAL_QT2=m -CONFIG_USB_SERIAL_UPD78F0730=m -CONFIG_USB_SERIAL_XR=m -CONFIG_USB_EMI62=m -CONFIG_USB_EMI26=m -CONFIG_USB_ADUTUX=m -CONFIG_USB_SEVSEG=m -CONFIG_USB_LEGOTOWER=m -CONFIG_USB_LCD=m -CONFIG_USB_IDMOUSE=m -CONFIG_USB_APPLEDISPLAY=m -CONFIG_USB_QCOM_EUD=m -CONFIG_APPLE_MFI_FASTCHARGE=m -CONFIG_USB_SISUSBVGA=m -CONFIG_USB_LD=m -CONFIG_USB_TRANCEVIBRATOR=m -CONFIG_USB_IOWARRIOR=m -CONFIG_USB_ISIGHTFW=m -CONFIG_USB_YUREX=m -CONFIG_USB_HSIC_USB3503=y -CONFIG_USB_CHAOSKEY=m -CONFIG_USB_ONBOARD_DEV=m -CONFIG_USB_ONBOARD_DEV_USB5744=y -CONFIG_NOP_USB_XCEIV=y -CONFIG_USB_MXS_PHY=m -CONFIG_USB_GADGET=y -CONFIG_USB_RENESAS_USBHS_UDC=m -CONFIG_USB_RZV2M_USB3DRD=y -CONFIG_USB_RENESAS_USB3=m -CONFIG_USB_TEGRA_XUDC=m -CONFIG_USB_CONFIGFS=m -CONFIG_USB_CONFIGFS_SERIAL=y -CONFIG_USB_CONFIGFS_ACM=y -CONFIG_USB_CONFIGFS_OBEX=y -CONFIG_USB_CONFIGFS_NCM=y -CONFIG_USB_CONFIGFS_ECM=y -CONFIG_USB_CONFIGFS_ECM_SUBSET=y -CONFIG_USB_CONFIGFS_RNDIS=y -CONFIG_USB_CONFIGFS_EEM=y -CONFIG_USB_CONFIGFS_MASS_STORAGE=y -CONFIG_USB_CONFIGFS_F_FS=y -CONFIG_USB_MASS_STORAGE=m -CONFIG_TYPEC=y -CONFIG_TYPEC_TCPM=m -CONFIG_TYPEC_TCPCI=m -CONFIG_TYPEC_RT1711H=m -CONFIG_TYPEC_MT6360=m -CONFIG_TYPEC_TCPCI_MAXIM=m -CONFIG_TYPEC_FUSB302=m -CONFIG_TYPEC_QCOM_PMIC=m -CONFIG_TYPEC_UCSI=m -CONFIG_UCSI_CCG=m -CONFIG_UCSI_PMIC_GLINK=m -CONFIG_TYPEC_TPS6598X=m -CONFIG_TYPEC_SN201202X=y -CONFIG_TYPEC_HD3SS3220=m -CONFIG_TYPEC_MUX_FSA4480=m -CONFIG_TYPEC_MUX_GPIO_SBU=m -CONFIG_TYPEC_MUX_IT5205=m -CONFIG_TYPEC_MUX_NB7VPQ904M=m -CONFIG_TYPEC_MUX_PS883X=m -CONFIG_TYPEC_MUX_PTN36502=m -CONFIG_TYPEC_MUX_WCD939X_USBSS=m -CONFIG_TYPEC_DP_ALTMODE=m -CONFIG_MMC=y -CONFIG_MMC_BLOCK_MINORS=32 -CONFIG_MMC_ARMMMCI=y -CONFIG_MMC_SDHCI=y -CONFIG_MMC_SDHCI_ACPI=y -CONFIG_MMC_SDHCI_PLTFM=y -CONFIG_MMC_SDHCI_OF_ARASAN=y -CONFIG_MMC_SDHCI_OF_ESDHC=y -CONFIG_MMC_SDHCI_OF_DWCMSHC=y -CONFIG_MMC_SDHCI_OF_SPARX5=y -CONFIG_MMC_SDHCI_CADENCE=y -CONFIG_MMC_SDHCI_ESDHC_IMX=y -CONFIG_MMC_SDHCI_TEGRA=y -CONFIG_MMC_SDHCI_F_SDH30=y -CONFIG_MMC_MESON_GX=y -CONFIG_MMC_SDHCI_MSM=y -CONFIG_MMC_SPI=y -CONFIG_MMC_SDHI=y -CONFIG_MMC_UNIPHIER=y -CONFIG_MMC_DW=y -CONFIG_MMC_DW_EXYNOS=y -CONFIG_MMC_DW_HI3798CV200=y -CONFIG_MMC_DW_K3=y -CONFIG_MMC_DW_ROCKCHIP=y -CONFIG_MMC_VUB300=m -CONFIG_MMC_USHC=m -CONFIG_MMC_REALTEK_USB=m -CONFIG_MMC_SUNXI=y -CONFIG_MMC_BCM2835=y -CONFIG_MMC_MTK=y -CONFIG_MMC_SDHCI_XENON=y -CONFIG_MMC_SDHCI_AM654=y -CONFIG_MMC_OWL=y -CONFIG_SCSI_UFSHCD=y -CONFIG_SCSI_UFS_BSG=y -CONFIG_SCSI_UFS_CRYPTO=y -CONFIG_SCSI_UFSHCD_PLATFORM=y -CONFIG_SCSI_UFS_CDNS_PLATFORM=m -CONFIG_SCSI_UFS_QCOM=m -CONFIG_SCSI_UFS_MEDIATEK=y -CONFIG_SCSI_UFS_HISI=y -CONFIG_SCSI_UFS_RENESAS=m -CONFIG_SCSI_UFS_TI_J721E=m -CONFIG_SCSI_UFS_EXYNOS=y -CONFIG_SCSI_UFS_ROCKCHIP=y -CONFIG_MEMSTICK=m -CONFIG_MSPRO_BLOCK=m -CONFIG_MEMSTICK_REALTEK_USB=m -CONFIG_LEDS_CLASS=y -CONFIG_LEDS_CLASS_FLASH=m -CONFIG_LEDS_CLASS_MULTICOLOR=m -CONFIG_LEDS_LM3692X=m -CONFIG_LEDS_PCA9532=m -CONFIG_LEDS_GPIO=y -CONFIG_LEDS_PWM=y -CONFIG_LEDS_SYSCON=y -CONFIG_LEDS_QCOM_FLASH=m -CONFIG_LEDS_QCOM_LPG=m -CONFIG_LEDS_TRIGGER_TIMER=y -CONFIG_LEDS_TRIGGER_DISK=y -CONFIG_LEDS_TRIGGER_HEARTBEAT=y -CONFIG_LEDS_TRIGGER_CPU=y -CONFIG_LEDS_TRIGGER_DEFAULT_ON=y -CONFIG_LEDS_TRIGGER_PANIC=y -CONFIG_ACCESSIBILITY=y -CONFIG_SPEAKUP=m -CONFIG_SPEAKUP_SYNTH_ACNTSA=m -CONFIG_SPEAKUP_SYNTH_APOLLO=m -CONFIG_SPEAKUP_SYNTH_AUDPTR=m -CONFIG_SPEAKUP_SYNTH_BNS=m -CONFIG_SPEAKUP_SYNTH_DECTLK=m -CONFIG_SPEAKUP_SYNTH_LTLK=m -CONFIG_SPEAKUP_SYNTH_SOFT=m -CONFIG_SPEAKUP_SYNTH_SPKOUT=m -CONFIG_SPEAKUP_SYNTH_TXPRT=m -CONFIG_INFINIBAND=m -CONFIG_INFINIBAND_USER_MAD=m -CONFIG_INFINIBAND_USER_ACCESS=m -CONFIG_RDMA_RXE=m -CONFIG_RDMA_SIW=m -CONFIG_EDAC=y -CONFIG_EDAC_GHES=y -CONFIG_EDAC_LAYERSCAPE=m -CONFIG_EDAC_ZYNQMP=m -CONFIG_EDAC_VERSAL=m -CONFIG_RTC_CLASS=y -CONFIG_RTC_DRV_DS1307=m -CONFIG_RTC_DRV_HYM8563=m -CONFIG_RTC_DRV_MAX77686=y -CONFIG_RTC_DRV_NVIDIA_VRS10=m -CONFIG_RTC_DRV_RK808=m -CONFIG_RTC_DRV_ISL1208=m -CONFIG_RTC_DRV_PCF85363=m -CONFIG_RTC_DRV_PCF8563=m -CONFIG_RTC_DRV_M41T80=m -CONFIG_RTC_DRV_BQ32K=m -CONFIG_RTC_DRV_RX8581=m -CONFIG_RTC_DRV_RV3028=m -CONFIG_RTC_DRV_RV8803=m -CONFIG_RTC_DRV_S5M=y -CONFIG_RTC_DRV_DS3232=y -CONFIG_RTC_DRV_PCF2127=m -CONFIG_RTC_DRV_PCF85063=m -CONFIG_RTC_DRV_DA9063=m -CONFIG_RTC_DRV_EFI=y -CONFIG_RTC_DRV_ZYNQMP=m -CONFIG_RTC_DRV_CROS_EC=y -CONFIG_RTC_DRV_FSL_FTM_ALARM=m -CONFIG_RTC_DRV_S3C=y -CONFIG_RTC_DRV_PL031=y -CONFIG_RTC_DRV_SUN6I=y -CONFIG_RTC_DRV_ARMADA38X=y -CONFIG_RTC_DRV_PM8XXX=m -CONFIG_RTC_DRV_TEGRA=y -CONFIG_RTC_DRV_SNVS=m -CONFIG_RTC_DRV_BBNSM=m -CONFIG_RTC_DRV_IMX_SC=m -CONFIG_RTC_DRV_MT6397=m -CONFIG_RTC_DRV_XGENE=y -CONFIG_RTC_DRV_TI_K3=m -CONFIG_RTC_DRV_RENESAS_RTCA3=m -CONFIG_RTC_DRV_APPLE_PMU=m -CONFIG_RTC_DRV_MACSMC=m -CONFIG_RTC_DRV_S32G=m -CONFIG_DMADEVICES=y -CONFIG_APPLE_SIO=m -CONFIG_DMA_BCM2835=y -CONFIG_DMA_SUN6I=m -CONFIG_FSL_EDMA=y -CONFIG_IMX_SDMA=m -CONFIG_K3_DMA=y -CONFIG_MV_XOR=y -CONFIG_MV_XOR_V2=y -CONFIG_OWL_DMA=y -CONFIG_PL330_DMA=y -CONFIG_TEGRA186_GPC_DMA=y -CONFIG_TEGRA20_APB_DMA=y -CONFIG_TEGRA210_ADMA=m -CONFIG_XILINX_DMA=m -CONFIG_XILINX_ZYNQMP_DMA=m -CONFIG_XILINX_ZYNQMP_DPDMA=m -CONFIG_MTK_UART_APDMA=m -CONFIG_QCOM_BAM_DMA=y -CONFIG_QCOM_GPI_DMA=m -CONFIG_QCOM_HIDMA_MGMT=y -CONFIG_QCOM_HIDMA=y -CONFIG_DW_EDMA=m -CONFIG_RCAR_DMAC=y -CONFIG_RENESAS_USB_DMAC=m -CONFIG_RZ_DMAC=y -CONFIG_TI_K3_UDMA=y -CONFIG_TI_K3_UDMA_GLUE_LAYER=y -CONFIG_STM32_DMA3=m -CONFIG_UDMABUF=y -CONFIG_DMABUF_HEAPS=y -CONFIG_DMABUF_HEAPS_SYSTEM=y -CONFIG_VFIO=y -CONFIG_VFIO_PCI=y -CONFIG_VIRTIO_PCI=y -CONFIG_VIRTIO_BALLOON=y -CONFIG_VIRTIO_MMIO=y -CONFIG_VHOST_NET=m -CONFIG_VHOST_VSOCK=m -CONFIG_XEN_GNTDEV=y -CONFIG_XEN_GRANT_DEV_ALLOC=y -CONFIG_GREYBUS=m -CONFIG_GREYBUS_BEAGLEPLAY=m -CONFIG_GPIB=m -CONFIG_GPIB_AGILENT_82357A=m -CONFIG_GPIB_NI_USB=m -CONFIG_GPIB_LPVO=m -CONFIG_STAGING=y -CONFIG_STAGING_MEDIA=y -CONFIG_VIDEO_MAX96712=m -CONFIG_VIDEO_MESON_VDEC=m -CONFIG_SND_BCM2835=m -CONFIG_CHROME_PLATFORMS=y -CONFIG_CROS_EC=y -CONFIG_CROS_EC_I2C=y -CONFIG_CROS_EC_RPMSG=m -CONFIG_CROS_EC_SPI=y -CONFIG_CROS_KBD_LED_BACKLIGHT=m -CONFIG_CROS_EC_CHARDEV=m -CONFIG_CROS_EC_TYPEC=m -CONFIG_CROS_TYPEC_SWITCH=m -CONFIG_EC_ACER_ASPIRE1=m -CONFIG_EC_HUAWEI_GAOKUN=m -CONFIG_EC_LENOVO_YOGA_C630=m -CONFIG_EC_LENOVO_THINKPAD_T14S=m -CONFIG_COMMON_CLK_RK808=y -CONFIG_COMMON_CLK_SCPI=y -CONFIG_COMMON_CLK_CS2000_CP=y -CONFIG_COMMON_CLK_FSL_SAI=y -CONFIG_COMMON_CLK_S2MPS11=y -CONFIG_COMMON_CLK_PWM=y -CONFIG_COMMON_CLK_RS9_PCIE=y -CONFIG_COMMON_CLK_VC3=y -CONFIG_COMMON_CLK_VC5=y -CONFIG_COMMON_CLK_BD718XX=m -CONFIG_CLK_RASPBERRYPI=y -CONFIG_CLK_IMX8MM=y -CONFIG_CLK_IMX8MN=y -CONFIG_CLK_IMX8MP=y -CONFIG_CLK_IMX8MQ=y -CONFIG_CLK_IMX8QXP=y -CONFIG_CLK_IMX8ULP=y -CONFIG_CLK_IMX93=y -CONFIG_CLK_IMX95_BLK_CTL=y -CONFIG_TI_SCI_CLK=y -CONFIG_COMMON_CLK_QCOM=y -CONFIG_CLK_ELIZA_DISPCC=m -CONFIG_CLK_ELIZA_GCC=y -CONFIG_CLK_ELIZA_TCSRCC=m -CONFIG_CLK_GLYMUR_DISPCC=m -CONFIG_CLK_GLYMUR_GCC=y -CONFIG_CLK_GLYMUR_TCSRCC=m -CONFIG_CLK_KAANAPALI_CAMCC=m -CONFIG_CLK_KAANAPALI_DISPCC=m -CONFIG_CLK_KAANAPALI_GCC=y -CONFIG_CLK_KAANAPALI_GPUCC=m -CONFIG_CLK_KAANAPALI_TCSRCC=m -CONFIG_CLK_KAANAPALI_VIDEOCC=m -CONFIG_CLK_X1E80100_CAMCC=m -CONFIG_CLK_X1E80100_DISPCC=m -CONFIG_CLK_X1E80100_GCC=y -CONFIG_CLK_X1E80100_GPUCC=m -CONFIG_CLK_X1E80100_TCSRCC=y -CONFIG_CLK_X1P42100_GPUCC=m -CONFIG_CLK_QCM2290_GPUCC=m -CONFIG_QCOM_A53PLL=y -CONFIG_QCOM_CLK_APCS_MSM8916=y -CONFIG_QCOM_CLK_APCC_MSM8996=y -CONFIG_QCOM_CLK_SMD_RPM=y -CONFIG_QCOM_CLK_RPMH=y -CONFIG_IPQ_APSS_6018=y -CONFIG_IPQ_CMN_PLL=m -CONFIG_IPQ_GCC_5018=y -CONFIG_IPQ_GCC_5210=y -CONFIG_IPQ_GCC_5332=y -CONFIG_IPQ_GCC_5424=y -CONFIG_IPQ_GCC_6018=y -CONFIG_IPQ_GCC_8074=y -CONFIG_IPQ_GCC_9574=y -CONFIG_IPQ_NSSCC_5424=m -CONFIG_IPQ_NSSCC_9574=m -CONFIG_MSM_GCC_8916=y -CONFIG_MSM_GCC_8953=y -CONFIG_MSM_MMCC_8994=m -CONFIG_MSM_GCC_8994=y -CONFIG_MSM_GCC_8996=y -CONFIG_MSM_MMCC_8996=m -CONFIG_MSM_GCC_8998=y -CONFIG_MSM_MMCC_8998=m -CONFIG_QCM_GCC_2290=y -CONFIG_QCM_DISPCC_2290=m -CONFIG_QCS_DISPCC_615=m -CONFIG_QCS_CAMCC_615=m -CONFIG_QCS_GCC_404=y -CONFIG_SA_CAMCC_8775P=m -CONFIG_QCS_GCC_8300=y -CONFIG_QCS_GCC_615=y -CONFIG_QCS_GPUCC_615=m -CONFIG_QCS_VIDEOCC_615=m -CONFIG_SC_CAMCC_7280=m -CONFIG_SC_CAMCC_8280XP=m -CONFIG_SA_DISPCC_8775P=m -CONFIG_SC_DISPCC_7280=m -CONFIG_SC_DISPCC_8280XP=m -CONFIG_SA_GCC_8775P=y -CONFIG_SA_GPUCC_8775P=m -CONFIG_SC_GCC_7180=y -CONFIG_SC_GCC_7280=y -CONFIG_SC_GCC_8180X=y -CONFIG_SC_GCC_8280XP=y -CONFIG_SC_GPUCC_7280=m -CONFIG_SC_GPUCC_8280XP=m -CONFIG_SC_LPASSCC_8280XP=m -CONFIG_SC_LPASS_CORECC_7280=m -CONFIG_SC_VIDEOCC_7280=m -CONFIG_SDM_CAMCC_845=m -CONFIG_QDU_GCC_1000=y -CONFIG_SDM_GPUCC_845=y -CONFIG_SDM_VIDEOCC_845=y -CONFIG_SDM_DISPCC_845=y -CONFIG_SDM_LPASSCC_845=m -CONFIG_SDX_GCC_75=y -CONFIG_SM_CAMCC_6350=m -CONFIG_SM_CAMCC_MILOS=m -CONFIG_SM_CAMCC_8250=m -CONFIG_SM_CAMCC_8550=m -CONFIG_SM_CAMCC_8650=m -CONFIG_SM_CAMCC_8750=m -CONFIG_SM_DISPCC_6115=m -CONFIG_SM_DISPCC_8250=y -CONFIG_SM_DISPCC_6350=m -CONFIG_SM_DISPCC_MILOS=m -CONFIG_SM_DISPCC_8450=m -CONFIG_SM_DISPCC_8550=m -CONFIG_SM_DISPCC_8750=m -CONFIG_SM_GCC_4450=y -CONFIG_SM_GCC_6115=y -CONFIG_SM_GCC_6350=y -CONFIG_SM_GCC_MILOS=y -CONFIG_SM_GCC_8350=y -CONFIG_SM_GCC_8450=y -CONFIG_SM_GCC_8550=y -CONFIG_SM_GCC_8650=y -CONFIG_SM_GCC_8750=y -CONFIG_SM_GPUCC_6115=m -CONFIG_SM_GPUCC_6350=m -CONFIG_SM_GPUCC_MILOS=m -CONFIG_SM_GPUCC_8150=y -CONFIG_SM_GPUCC_8250=y -CONFIG_SM_GPUCC_8350=m -CONFIG_SM_GPUCC_8450=m -CONFIG_SM_GPUCC_8550=m -CONFIG_SM_GPUCC_8650=m -CONFIG_SM_GPUCC_8750=m -CONFIG_SM_TCSRCC_8550=y -CONFIG_SM_TCSRCC_8650=y -CONFIG_SM_TCSRCC_8750=m -CONFIG_SA_VIDEOCC_8775P=m -CONFIG_SM_VIDEOCC_6350=m -CONFIG_SM_VIDEOCC_MILOS=m -CONFIG_SM_VIDEOCC_8250=y -CONFIG_SM_VIDEOCC_8550=m -CONFIG_SM_VIDEOCC_8750=m -CONFIG_QCOM_HFPLL=y -CONFIG_CLK_GFM_LPASS_SM8250=m -CONFIG_SM_VIDEOCC_8450=m -CONFIG_CLK_RCAR_USB2_CLOCK_SEL=y -CONFIG_CLK_RENESAS_VBATTB=m -CONFIG_EXYNOS_ACPM_CLK=m -CONFIG_CLK_SOPHGO_CV1800=y -CONFIG_XILINX_VCU=m -CONFIG_COMMON_CLK_XLNX_CLKWZRD=m -CONFIG_HWSPINLOCK=y -CONFIG_HWSPINLOCK_OMAP=m -CONFIG_HWSPINLOCK_QCOM=y -CONFIG_TEGRA186_TIMER=y -CONFIG_CLKSRC_STM32_LP=y -CONFIG_RENESAS_OSTM=y -CONFIG_ARM_MHU=y -CONFIG_EXYNOS_MBOX=m -CONFIG_IMX_MBOX=y -CONFIG_PLATFORM_MHU=y -CONFIG_OMAP2PLUS_MBOX=m -CONFIG_BCM2835_MBOX=y -CONFIG_QCOM_APCS_IPC=y -CONFIG_TEGRA_HSP_MBOX=y -CONFIG_MTK_ADSP_MBOX=m -CONFIG_QCOM_CPUCP_MBOX=m -CONFIG_QCOM_IPCC=y -CONFIG_CIX_MBOX=y -CONFIG_ARM_SMMU=y -CONFIG_ARM_SMMU_V3=y -CONFIG_QCOM_IOMMU=y -CONFIG_ROCKCHIP_IOMMU=y -CONFIG_TEGRA_IOMMU_SMMU=y -CONFIG_APPLE_DART=y -CONFIG_MTK_IOMMU=y -CONFIG_REMOTEPROC=y -CONFIG_IMX_REMOTEPROC=y -CONFIG_MTK_SCP=m -CONFIG_QCOM_Q6V5_ADSP=m -CONFIG_QCOM_Q6V5_MSS=m -CONFIG_QCOM_Q6V5_PAS=m -CONFIG_QCOM_SYSMON=m -CONFIG_QCOM_WCNSS_PIL=m -CONFIG_TI_K3_DSP_REMOTEPROC=m -CONFIG_TI_K3_M4_REMOTEPROC=m -CONFIG_TI_K3_R5_REMOTEPROC=m -CONFIG_RPMSG_CHAR=m -CONFIG_RPMSG_CTRL=m -CONFIG_RPMSG_QCOM_GLINK_RPM=y -CONFIG_RPMSG_QCOM_GLINK_SMEM=m -CONFIG_RPMSG_QCOM_SMD=y -CONFIG_RPMSG_VIRTIO=y -CONFIG_SOUNDWIRE=m -CONFIG_SOUNDWIRE_QCOM=m -CONFIG_APPLE_DOCKCHANNEL=y -CONFIG_APPLE_PMGR_MISC=y -CONFIG_APPLE_RTKIT_HELPER=y -CONFIG_APPLE_SART=y -CONFIG_APPLE_CLPC=m -CONFIG_FSL_DPAA=y -CONFIG_FSL_MC_DPIO=y -CONFIG_FSL_RCPM=y -CONFIG_MTK_CMDQ=m -CONFIG_MTK_DEVAPC=m -CONFIG_MTK_PMIC_WRAP=y -CONFIG_MTK_SVS=m -CONFIG_QCOM_AOSS_QMP=y -CONFIG_QCOM_COMMAND_DB=y -CONFIG_QCOM_GENI_SE=y -CONFIG_QCOM_LLCC=m -CONFIG_QCOM_OCMEM=m -CONFIG_QCOM_PMIC_GLINK=m -CONFIG_QCOM_RMTFS_MEM=m -CONFIG_QCOM_RPMH=y -CONFIG_QCOM_SMEM=y -CONFIG_QCOM_SMD_RPM=y -CONFIG_QCOM_SMP2P=y -CONFIG_QCOM_SMSM=y -CONFIG_QCOM_SOCINFO=m -CONFIG_QCOM_SPM=m -CONFIG_QCOM_STATS=m -CONFIG_QCOM_WCNSS_CTRL=m -CONFIG_QCOM_APR=m -CONFIG_QCOM_ICC_BWMON=m -CONFIG_QCOM_PBS=m -CONFIG_ROCKCHIP_IODOMAIN=y -CONFIG_TI_PRUSS=m -CONFIG_OWL_PM_DOMAINS=y -CONFIG_RASPBERRYPI_POWER=y -CONFIG_IMX_SCU_PD=y -CONFIG_QCOM_CPR=y -CONFIG_QCOM_RPMHPD=y -CONFIG_QCOM_RPMPD=y -CONFIG_TI_SCI_PM_DOMAINS=y -CONFIG_ARM_IMX_BUS_DEVFREQ=y -CONFIG_ARM_IMX8M_DDRC_DEVFREQ=m -CONFIG_ARM_MEDIATEK_CCI_DEVFREQ=m -CONFIG_PM_DEVFREQ_EVENT=y -CONFIG_DEVFREQ_EVENT_ROCKCHIP_DFI=m -CONFIG_EXTCON_PTN5150=m -CONFIG_EXTCON_USB_GPIO=y -CONFIG_EXTCON_USBC_CROS_EC=y -CONFIG_FSL_IFC=y -CONFIG_RENESAS_RPCIF=m -CONFIG_STM32_OMM=m -CONFIG_DLN2_ADC=m -CONFIG_EXYNOS_ADC=m -CONFIG_IMX8QXP_ADC=m -CONFIG_IMX93_ADC=m -CONFIG_MAX9611=m -CONFIG_MEDIATEK_MT6359_AUXADC=m -CONFIG_MEDIATEK_MT6577_AUXADC=m -CONFIG_QCOM_SPMI_VADC=m -CONFIG_QCOM_SPMI_ADC5=m -CONFIG_ROCKCHIP_SARADC=m -CONFIG_RZG2L_ADC=m -CONFIG_RZT2H_ADC=m -CONFIG_SOPHGO_CV1800B_ADC=m -CONFIG_TI_ADS1015=m -CONFIG_TI_AM335X_ADC=m -CONFIG_XILINX_XADC=m -CONFIG_XILINX_AMS=m -CONFIG_IIO_AOP_SENSOR_ALS=m -CONFIG_IIO_CROS_EC_SENSORS_CORE=m -CONFIG_IIO_CROS_EC_SENSORS=m -CONFIG_IIO_ST_LSM6DSX=m -CONFIG_IIO_CROS_EC_LIGHT_PROX=m -CONFIG_SENSORS_ISL29018=m -CONFIG_VCNL4000=m -CONFIG_IIO_ST_MAGN_3AXIS=m -CONFIG_IIO_STM32_TIMER_TRIGGER=m -CONFIG_IIO_CROS_EC_BARO=m -CONFIG_MPL3115=m -CONFIG_PWM=y -CONFIG_PWM_ADP5585=m -CONFIG_PWM_APPLE=m -CONFIG_PWM_BCM2835=m -CONFIG_PWM_BRCMSTB=m -CONFIG_PWM_CROS_EC=m -CONFIG_PWM_IMX27=m -CONFIG_PWM_MEDIATEK=m -CONFIG_PWM_MESON=m -CONFIG_PWM_MTK_DISP=m -CONFIG_PWM_RENESAS_RCAR=m -CONFIG_PWM_RENESAS_RZG2L_GPT=m -CONFIG_PWM_RENESAS_RZ_MTU3=m -CONFIG_PWM_RENESAS_TPU=m -CONFIG_PWM_ROCKCHIP=y -CONFIG_PWM_SAMSUNG=y -CONFIG_PWM_SL28CPLD=m -CONFIG_PWM_STM32=m -CONFIG_PWM_SUN4I=m -CONFIG_PWM_TEGRA=m -CONFIG_PWM_TIECAP=m -CONFIG_PWM_TIEHRPWM=m -CONFIG_PWM_VISCONTI=m -CONFIG_PWM_XILINX=m -CONFIG_SL28CPLD_INTC=y -CONFIG_XILINX_INTC=y -CONFIG_QCOM_PDC=y -CONFIG_QCOM_MPM=y -CONFIG_TI_SCI_INTR_IRQCHIP=y -CONFIG_TI_SCI_INTA_IRQCHIP=y -CONFIG_RESET_GPIO=m -CONFIG_RESET_IMX7=y -CONFIG_RESET_QCOM_AOSS=y -CONFIG_RESET_QCOM_PDC=m -CONFIG_RESET_RZG2L_USBPHY_CTRL=y -CONFIG_RESET_RZV2H_USB2PHY=m -CONFIG_RESET_TI_SCI=y -CONFIG_PHY_CAN_TRANSCEIVER=m -CONFIG_PHY_NXP_PTN3222=m -CONFIG_PHY_SNPS_EUSB2=m -CONFIG_PHY_XGENE=y -CONFIG_PHY_SUN4I_USB=y -CONFIG_PHY_APPLE_ATC=y -CONFIG_PHY_APPLE_DPTX=m -CONFIG_PHY_CADENCE_TORRENT=m -CONFIG_PHY_CADENCE_DPHY=m -CONFIG_PHY_CADENCE_DPHY_RX=m -CONFIG_PHY_CADENCE_SIERRA=m -CONFIG_PHY_CADENCE_SALVO=m -CONFIG_PHY_FSL_IMX8MQ_USB=m -CONFIG_PHY_MIXEL_MIPI_DPHY=m -CONFIG_PHY_FSL_IMX8M_PCIE=y -CONFIG_PHY_HI6220_USB=y -CONFIG_PHY_HISTB_COMBPHY=y -CONFIG_PHY_HISI_INNO_USB2=y -CONFIG_PHY_MVEBU_CP110_COMPHY=y -CONFIG_PHY_MTK_PCIE=m -CONFIG_PHY_MTK_TPHY=y -CONFIG_PHY_MTK_HDMI=m -CONFIG_PHY_MTK_MIPI_DSI=m -CONFIG_PHY_MTK_DP=m -CONFIG_PHY_QCOM_EDP=m -CONFIG_PHY_QCOM_PCIE2=m -CONFIG_PHY_QCOM_QMP=m -CONFIG_PHY_QCOM_QUSB2=m -CONFIG_PHY_QCOM_EUSB2_REPEATER=m -CONFIG_PHY_QCOM_M31_USB=m -CONFIG_PHY_QCOM_M31_EUSB=m -CONFIG_PHY_QCOM_USB_HS=m -CONFIG_PHY_QCOM_USB_SNPS_FEMTO_V2=m -CONFIG_PHY_QCOM_USB_HS_28NM=m -CONFIG_PHY_QCOM_USB_SS=m -CONFIG_PHY_QCOM_SGMII_ETH=m -CONFIG_PHY_R8A779F0_ETHERNET_SERDES=y -CONFIG_PHY_RCAR_GEN3_PCIE=y -CONFIG_PHY_RCAR_GEN3_USB2=y -CONFIG_PHY_RCAR_GEN3_USB3=m -CONFIG_PHY_RZ_G3E_USB3=m -CONFIG_PHY_ROCKCHIP_EMMC=y -CONFIG_PHY_ROCKCHIP_INNO_HDMI=m -CONFIG_PHY_ROCKCHIP_INNO_USB2=y -CONFIG_PHY_ROCKCHIP_INNO_DSIDPHY=m -CONFIG_PHY_ROCKCHIP_NANENG_COMBO_PHY=m -CONFIG_PHY_ROCKCHIP_PCIE=m -CONFIG_PHY_ROCKCHIP_SAMSUNG_HDPTX=m -CONFIG_PHY_ROCKCHIP_SNPS_PCIE3=y -CONFIG_PHY_ROCKCHIP_TYPEC=y -CONFIG_PHY_ROCKCHIP_USBDP=m -CONFIG_PHY_SAMSUNG_UFS=y -CONFIG_PHY_EXYNOS5_USBDRD=m -CONFIG_PHY_UNIPHIER_USB2=y -CONFIG_PHY_UNIPHIER_USB3=y -CONFIG_PHY_TEGRA_XUSB=y -CONFIG_PHY_AM654_SERDES=m -CONFIG_PHY_J721E_WIZ=m -CONFIG_OMAP_USB2=m -CONFIG_PHY_XILINX_ZYNQMP=m -CONFIG_ARM_CCI_PMU=m -CONFIG_ARM_CCN=m -CONFIG_ARM_CMN=m -CONFIG_ARM_SMMU_V3_PMU=m -CONFIG_ARM_DSU_PMU=m -CONFIG_FSL_IMX8_DDR_PMU=m -CONFIG_QCOM_L2_PMU=y -CONFIG_QCOM_L3_PMU=y -CONFIG_ARM_SPE_PMU=m -CONFIG_ARM_DMC620_PMU=m -CONFIG_APPLE_M1_CPU_PMU=y -CONFIG_HISI_PMU=y -CONFIG_ARM_CORESIGHT_PMU_ARCH_SYSTEM_PMU=m -CONFIG_NVIDIA_CORESIGHT_PMU_ARCH_SYSTEM_PMU=m -CONFIG_MESON_DDR_PMU=m -CONFIG_NVMEM_LAYOUT_SL28_VPD=m -CONFIG_NVMEM_APPLE_EFUSES=m -CONFIG_NVMEM_APPLE_SPMI=y -CONFIG_NVMEM_IMX_OCOTP=y -CONFIG_NVMEM_IMX_OCOTP_ELE=m -CONFIG_NVMEM_IMX_OCOTP_SCU=y -CONFIG_NVMEM_LAYERSCAPE_SFP=m -CONFIG_NVMEM_MTK_EFUSE=y -CONFIG_NVMEM_QCOM_QFPROM=y -CONFIG_NVMEM_QCOM_SEC_QFPROM=m -CONFIG_NVMEM_RMEM=m -CONFIG_NVMEM_ROCKCHIP_EFUSE=y -CONFIG_NVMEM_ROCKCHIP_OTP=y -CONFIG_NVMEM_SNVS_LPGPR=y -CONFIG_NVMEM_SPMI_SDAM=m -CONFIG_NVMEM_SUNXI_SID=y -CONFIG_NVMEM_UNIPHIER_EFUSE=y -CONFIG_NVMEM_ZYNQMP=m -CONFIG_FPGA=y -CONFIG_FPGA_MGR_ALTERA_CVP=m -CONFIG_FPGA_MGR_STRATIX10_SOC=m -CONFIG_FPGA_BRIDGE=m -CONFIG_ALTERA_FREEZE_BRIDGE=m -CONFIG_XILINX_PR_DECOUPLER=m -CONFIG_FPGA_REGION=m -CONFIG_OF_FPGA_REGION=m -CONFIG_FPGA_MGR_ZYNQMP_FPGA=m -CONFIG_FPGA_MGR_VERSAL_FPGA=m -CONFIG_TEE=y -CONFIG_OPTEE=y -CONFIG_QCOMTEE=m -CONFIG_MUX_APPLE_DPXBAR=y -CONFIG_MUX_GPIO=m -CONFIG_MUX_MMIO=y -CONFIG_SLIM_QCOM_NGD_CTRL=m -CONFIG_INTERCONNECT_IMX=y -CONFIG_INTERCONNECT_IMX8MM=m -CONFIG_INTERCONNECT_IMX8MN=m -CONFIG_INTERCONNECT_IMX8MQ=m -CONFIG_INTERCONNECT_IMX8MP=y -CONFIG_INTERCONNECT_QCOM=y -CONFIG_INTERCONNECT_QCOM_ELIZA=y -CONFIG_INTERCONNECT_QCOM_GLYMUR=y -CONFIG_INTERCONNECT_QCOM_KAANAPALI=y -CONFIG_INTERCONNECT_QCOM_MSM8916=m -CONFIG_INTERCONNECT_QCOM_MSM8953=y -CONFIG_INTERCONNECT_QCOM_MSM8996=y -CONFIG_INTERCONNECT_QCOM_OSM_L3=m -CONFIG_INTERCONNECT_QCOM_QCM2290=y -CONFIG_INTERCONNECT_QCOM_QCS404=m -CONFIG_INTERCONNECT_QCOM_QCS615=y -CONFIG_INTERCONNECT_QCOM_QCS8300=y -CONFIG_INTERCONNECT_QCOM_QDU1000=y -CONFIG_INTERCONNECT_QCOM_SA8775P=y -CONFIG_INTERCONNECT_QCOM_SC7180=y -CONFIG_INTERCONNECT_QCOM_SC7280=y -CONFIG_INTERCONNECT_QCOM_SC8180X=y -CONFIG_INTERCONNECT_QCOM_SC8280XP=y -CONFIG_INTERCONNECT_QCOM_SDM845=y -CONFIG_INTERCONNECT_QCOM_SDX75=y -CONFIG_INTERCONNECT_QCOM_SM6115=y -CONFIG_INTERCONNECT_QCOM_SM6350=y -CONFIG_INTERCONNECT_QCOM_MILOS=y -CONFIG_INTERCONNECT_QCOM_SM8150=y -CONFIG_INTERCONNECT_QCOM_SM8250=y -CONFIG_INTERCONNECT_QCOM_SM8350=y -CONFIG_INTERCONNECT_QCOM_SM8450=y -CONFIG_INTERCONNECT_QCOM_SM8550=y -CONFIG_INTERCONNECT_QCOM_SM8650=y -CONFIG_INTERCONNECT_QCOM_SM8750=y -CONFIG_INTERCONNECT_QCOM_X1E80100=y -CONFIG_COUNTER=m -CONFIG_RZ_MTU3_CNT=m -CONFIG_STM32_TIMER_CNT=m -CONFIG_TI_EQEP=m -CONFIG_HTE=y -CONFIG_HTE_TEGRA194=y -CONFIG_HTE_TEGRA194_TEST=m -CONFIG_EXT4_FS=y -CONFIG_EXT4_FS_POSIX_ACL=y -CONFIG_EXT4_FS_SECURITY=y -CONFIG_XFS_FS=m -CONFIG_XFS_SUPPORT_V4=y -CONFIG_XFS_SUPPORT_ASCII_CI=y -CONFIG_XFS_QUOTA=y -CONFIG_XFS_POSIX_ACL=y -CONFIG_XFS_RT=y -CONFIG_BTRFS_FS=y -CONFIG_BTRFS_FS_POSIX_ACL=y -CONFIG_F2FS_FS=m -CONFIG_F2FS_FS_SECURITY=y -CONFIG_F2FS_FS_COMPRESSION=y -CONFIG_F2FS_UNFAIR_RWSEM=y -CONFIG_FANOTIFY=y -CONFIG_FANOTIFY_ACCESS_PERMISSIONS=y -CONFIG_QUOTA=y -CONFIG_AUTOFS_FS=y -CONFIG_FUSE_FS=m -CONFIG_CUSE=m -CONFIG_OVERLAY_FS=m -CONFIG_ISO9660_FS=m -CONFIG_JOLIET=y -CONFIG_ZISOFS=y -CONFIG_UDF_FS=m -CONFIG_MSDOS_FS=m -CONFIG_VFAT_FS=y -CONFIG_EXFAT_FS=m -CONFIG_NTFS3_FS=m -CONFIG_NTFS3_LZX_XPRESS=y -CONFIG_NTFS3_FS_POSIX_ACL=y -CONFIG_TMPFS_POSIX_ACL=y -CONFIG_HUGETLBFS=y -CONFIG_EFIVAR_FS=y -CONFIG_HFS_FS=m -CONFIG_HFSPLUS_FS=m -CONFIG_UBIFS_FS=m -CONFIG_SQUASHFS=y -CONFIG_SQUASHFS_XATTR=y -CONFIG_SQUASHFS_LZ4=y -CONFIG_SQUASHFS_LZO=y -CONFIG_SQUASHFS_XZ=y -CONFIG_SQUASHFS_ZSTD=y -CONFIG_PSTORE_RAM=m -CONFIG_EROFS_FS=m -CONFIG_EROFS_FS_ZIP_DEFLATE=y -CONFIG_EROFS_FS_ZIP_ZSTD=y -CONFIG_EROFS_FS_ZIP_ACCEL=y -CONFIG_NFS_FS=y -CONFIG_NFS_V4=y -CONFIG_NFS_V4_2=y -CONFIG_ROOT_NFS=y -CONFIG_CIFS=m -CONFIG_CIFS_UPCALL=y -CONFIG_CIFS_XATTR=y -CONFIG_CIFS_POSIX=y -CONFIG_CIFS_DFS_UPCALL=y -CONFIG_CIFS_SWN_UPCALL=y -CONFIG_9P_FS=y -CONFIG_NLS_CODEPAGE_437=y -CONFIG_NLS_CODEPAGE_737=m -CONFIG_NLS_CODEPAGE_775=m -CONFIG_NLS_CODEPAGE_850=m -CONFIG_NLS_CODEPAGE_852=m -CONFIG_NLS_CODEPAGE_855=m -CONFIG_NLS_CODEPAGE_857=m -CONFIG_NLS_CODEPAGE_860=m -CONFIG_NLS_CODEPAGE_861=m -CONFIG_NLS_CODEPAGE_862=m -CONFIG_NLS_CODEPAGE_863=m -CONFIG_NLS_CODEPAGE_864=m -CONFIG_NLS_CODEPAGE_865=m -CONFIG_NLS_CODEPAGE_866=m -CONFIG_NLS_CODEPAGE_869=m -CONFIG_NLS_CODEPAGE_936=m -CONFIG_NLS_CODEPAGE_950=m -CONFIG_NLS_CODEPAGE_932=m -CONFIG_NLS_CODEPAGE_949=m -CONFIG_NLS_CODEPAGE_874=m -CONFIG_NLS_ISO8859_8=m -CONFIG_NLS_CODEPAGE_1250=m -CONFIG_NLS_CODEPAGE_1251=m -CONFIG_NLS_ASCII=m -CONFIG_NLS_ISO8859_1=y -CONFIG_NLS_ISO8859_2=m -CONFIG_NLS_ISO8859_3=m -CONFIG_NLS_ISO8859_4=m -CONFIG_NLS_ISO8859_5=m -CONFIG_NLS_ISO8859_6=m -CONFIG_NLS_ISO8859_7=m -CONFIG_NLS_ISO8859_9=m -CONFIG_NLS_ISO8859_13=m -CONFIG_NLS_ISO8859_14=m -CONFIG_NLS_ISO8859_15=m -CONFIG_NLS_KOI8_R=m -CONFIG_NLS_KOI8_U=m -CONFIG_NLS_MAC_ROMAN=m -CONFIG_NLS_MAC_CELTIC=m -CONFIG_NLS_MAC_CENTEURO=m -CONFIG_NLS_MAC_CROATIAN=m -CONFIG_NLS_MAC_CYRILLIC=m -CONFIG_NLS_MAC_GAELIC=m -CONFIG_NLS_MAC_GREEK=m -CONFIG_NLS_MAC_ICELAND=m -CONFIG_NLS_MAC_INUIT=m -CONFIG_NLS_MAC_ROMANIAN=m -CONFIG_NLS_MAC_TURKISH=m -CONFIG_PERSISTENT_KEYRINGS=y -CONFIG_TRUSTED_KEYS=y -# CONFIG_TRUSTED_KEYS_TPM is not set -# CONFIG_TRUSTED_KEYS_TEE is not set -CONFIG_SECURITY=y -CONFIG_SECURITY_YAMA=y -CONFIG_SECURITY_LANDLOCK=y -CONFIG_CRYPTO_USER=y -CONFIG_CRYPTO_NULL=m -CONFIG_CRYPTO_CRYPTD=m -CONFIG_CRYPTO_BENCHMARK=m -CONFIG_CRYPTO_ECDH=y -CONFIG_CRYPTO_AES=y -CONFIG_CRYPTO_BLOWFISH=m -CONFIG_CRYPTO_CAMELLIA=m -CONFIG_CRYPTO_CAST5=m -CONFIG_CRYPTO_CAST6=m -CONFIG_CRYPTO_SERPENT=m -CONFIG_CRYPTO_TWOFISH=m -CONFIG_CRYPTO_ADIANTUM=m -CONFIG_CRYPTO_CTS=m -CONFIG_CRYPTO_HCTR2=m -CONFIG_CRYPTO_LRW=m -CONFIG_CRYPTO_PCBC=m -CONFIG_CRYPTO_AEGIS128=m -CONFIG_CRYPTO_CCM=y -CONFIG_CRYPTO_GCM=y -CONFIG_CRYPTO_ECHAINIV=y -CONFIG_CRYPTO_BLAKE2B=m -CONFIG_CRYPTO_CMAC=y -CONFIG_CRYPTO_HMAC=y -CONFIG_CRYPTO_MD4=m -CONFIG_CRYPTO_RMD160=m -CONFIG_CRYPTO_SHA256=y -CONFIG_CRYPTO_STREEBOG=m -CONFIG_CRYPTO_WP512=m -CONFIG_CRYPTO_XCBC=m -CONFIG_CRYPTO_XXHASH=m -CONFIG_CRYPTO_CRC32C=m -CONFIG_CRYPTO_CRC32=m -CONFIG_CRYPTO_LZ4HC=m -CONFIG_CRYPTO_USER_API_HASH=m -CONFIG_CRYPTO_USER_API_SKCIPHER=m -CONFIG_CRYPTO_USER_API_RNG=m -CONFIG_CRYPTO_USER_API_AEAD=m -CONFIG_CRYPTO_GHASH_ARM64_CE=y -CONFIG_CRYPTO_AES_ARM64_BS=m -CONFIG_CRYPTO_AES_ARM64_CE_CCM=y -CONFIG_CRYPTO_DEV_SUN8I_CE=m -CONFIG_CRYPTO_DEV_FSL_CAAM=m -CONFIG_CRYPTO_DEV_FSL_DPAA2_CAAM=m -CONFIG_CRYPTO_DEV_QCE=m -CONFIG_CRYPTO_DEV_QCOM_RNG=m -CONFIG_CRYPTO_DEV_TEGRA=m -CONFIG_CRYPTO_DEV_XILINX_TRNG=m -CONFIG_CRYPTO_DEV_ZYNQMP_AES=m -CONFIG_CRYPTO_DEV_ZYNQMP_SHA3=m -CONFIG_CRYPTO_DEV_CCREE=m -CONFIG_CRYPTO_DEV_HISI_SEC2=m -CONFIG_CRYPTO_DEV_HISI_ZIP=m -CONFIG_CRYPTO_DEV_HISI_HPRE=m -CONFIG_CRYPTO_DEV_HISI_TRNG=m -CONFIG_CRYPTO_DEV_SA2UL=m -CONFIG_DMA_RESTRICTED_POOL=y -CONFIG_CMA_SIZE_MBYTES=32 -CONFIG_PRINTK_TIME=y -CONFIG_DYNAMIC_DEBUG=y -CONFIG_DEBUG_KERNEL=y -CONFIG_DEBUG_INFO_DWARF_TOOLCHAIN_DEFAULT=y -CONFIG_DEBUG_INFO_REDUCED=y -CONFIG_MAGIC_SYSRQ=y -CONFIG_DEBUG_FS=y -CONFIG_ENABLE_DEFAULT_TRACERS=y -CONFIG_CORESIGHT=m -CONFIG_CORESIGHT_LINK_AND_SINK_TMC=m -CONFIG_CORESIGHT_CATU=m -CONFIG_CORESIGHT_SINK_TPIU=m -CONFIG_CORESIGHT_SINK_ETBV10=m -CONFIG_CORESIGHT_STM=m -CONFIG_CORESIGHT_CPU_DEBUG=m -CONFIG_CORESIGHT_CTI=m -CONFIG_ARM64_USER4K_EXPERIMENTAL=y -CONFIG_MEMTEST=y diff --git a/arch/arm64/configs/j700-device-parity.config b/arch/arm64/configs/j700-device-parity.config deleted file mode 100644 index 55bce7a9ac0af..0000000000000 --- a/arch/arm64/configs/j700-device-parity.config +++ /dev/null @@ -1,1053 +0,0 @@ -# J700 (MacBook Neo, T8140) device-parity fragment. -# -# Brings the J700 configuration to device and feature parity with a general-purpose -# distribution kernel (reference: Fedora Asahi Remix 44, 7.1.13-402.asahi.fc44.aarch64+16k): -# drivers for devices that can attach over USB or Bluetooth, and common protocols, -# filesystems and features. Most entries are modules. Nothing here changes an option -# that defconfig, asahi.config, j700-radio.config or j700.config set. -# -# Merge after j700.config; see that file for the full sequence. - -# Prerequisites of the entries below: nftables core, hidraw, uinput and the ALSA sequencer -CONFIG_NF_TABLES=m -CONFIG_NF_TABLES_INET=y -CONFIG_NF_TABLES_IPV4=y -CONFIG_NF_TABLES_IPV6=y -CONFIG_NF_TABLES_NETDEV=y -CONFIG_NFT_CT=m -CONFIG_NFT_LOG=m -CONFIG_NFT_LIMIT=m -CONFIG_NFT_MASQ=m -CONFIG_NFT_NAT=m -CONFIG_NFT_REJECT=m -CONFIG_NFT_REJECT_INET=m -CONFIG_NFT_FIB_INET=m -CONFIG_NFT_FIB_IPV4=m -CONFIG_NFT_FIB_IPV6=m -CONFIG_HIDRAW=y -CONFIG_INPUT_UINPUT=m -CONFIG_SND_SEQUENCER=m -CONFIG_SND_HRTIMER=m -CONFIG_SND_SEQ_DUMMY=m -CONFIG_SND_SEQ_HRTIMER_DEFAULT=y - -# HID / input / game controllers (USB + Bluetooth HID devices, xpad, joydev, USB tablets/touchscreens) -CONFIG_DRAGONRISE_FF=y -CONFIG_GREENASIA_FF=y -CONFIG_HID_A4TECH=m -CONFIG_HID_ACCUTOUCH=m -CONFIG_HID_ACRUX=m -CONFIG_HID_ACRUX_FF=y -CONFIG_HID_ALPS=m -CONFIG_HID_APPLEIR=m -CONFIG_HID_AUREAL=m -CONFIG_HID_BATTERY_STRENGTH=y -CONFIG_HID_BELKIN=m -CONFIG_HID_BETOP_FF=m -CONFIG_HID_BIGBEN_FF=m -CONFIG_HID_CHERRY=m -CONFIG_HID_CHICONY=m -CONFIG_HID_CMEDIA=m -CONFIG_HID_CORSAIR=m -CONFIG_HID_COUGAR=m -CONFIG_HID_CP2112=m -CONFIG_HID_CREATIVE_SB0540=m -CONFIG_HID_CYPRESS=m -CONFIG_HID_DRAGONRISE=m -CONFIG_HID_ELAN=m -CONFIG_HID_ELECOM=m -CONFIG_HID_ELO=m -CONFIG_HID_EMS_FF=m -CONFIG_HID_EVISION=m -CONFIG_HID_EZKEY=m -CONFIG_HID_FT260=m -CONFIG_HID_GEMBIRD=m -CONFIG_HID_GFRM=m -CONFIG_HID_GLORIOUS=m -CONFIG_HID_GOOGLE_STADIA_FF=m -CONFIG_HID_GREENASIA=m -CONFIG_HID_GT683R=m -CONFIG_HID_GYRATION=m -CONFIG_HID_HOLTEK=m -CONFIG_HID_HUAWEI=m -CONFIG_HID_ICADE=m -CONFIG_HID_ITE=m -CONFIG_HID_JABRA=m -CONFIG_HID_KENSINGTON=m -CONFIG_HID_KEYTOUCH=m -CONFIG_HID_KYE=m -CONFIG_HID_KYSONA=m -CONFIG_HID_LCPOWER=m -CONFIG_HID_LED=m -CONFIG_HID_LENOVO=m -CONFIG_HID_LENOVO_GO=m -CONFIG_HID_LENOVO_GO_S=m -CONFIG_HID_LETSKETCH=m -CONFIG_HID_LOGITECH=m -CONFIG_HID_LOGITECH_DJ=m -CONFIG_HID_LOGITECH_HIDPP=m -CONFIG_HID_MACALLY=m -CONFIG_HID_MALTRON=m -CONFIG_HID_MAYFLASH=m -CONFIG_HID_MCP2221=m -CONFIG_HID_MEGAWORLD_FF=m -CONFIG_HID_MICROSOFT=m -CONFIG_HID_MONTEREY=m -CONFIG_HID_NINTENDO=m -CONFIG_HID_NTI=m -CONFIG_HID_NTRIG=m -CONFIG_HID_NVIDIA_SHIELD=m -CONFIG_HID_ORTEK=m -CONFIG_HID_PANTHERLORD=m -CONFIG_HID_PENMOUNT=m -CONFIG_HID_PETALYNX=m -CONFIG_HID_PICOLCD=m -CONFIG_HID_PICOLCD_BACKLIGHT=y -CONFIG_HID_PICOLCD_FB=y -CONFIG_HID_PICOLCD_LEDS=y -CONFIG_HID_PID=y -CONFIG_HID_PLANTRONICS=m -CONFIG_HID_PLAYSTATION=m -CONFIG_HID_PRIMAX=m -CONFIG_HID_PRODIKEYS=m -CONFIG_HID_PXRC=m -CONFIG_HID_RAPOO=m -CONFIG_HID_RAZER=m -CONFIG_HID_RETRODE=m -CONFIG_HID_RMI=m -CONFIG_HID_ROCCAT=m -CONFIG_HID_SAITEK=m -CONFIG_HID_SAMSUNG=m -CONFIG_HID_SEMITEK=m -CONFIG_HID_SIGMAMICRO=m -CONFIG_HID_SMARTJOYPLUS=m -CONFIG_HID_SONY=m -CONFIG_HID_SPEEDLINK=m -CONFIG_HID_STEAM=m -CONFIG_HID_STEELSERIES=m -CONFIG_HID_SUNPLUS=m -CONFIG_HID_THINGM=m -CONFIG_HID_THRUSTMASTER=m -CONFIG_HID_TIVO=m -CONFIG_HID_TOPRE=m -CONFIG_HID_TOPSEED=m -CONFIG_HID_TWINHAN=m -CONFIG_HID_U2FZERO=m -CONFIG_HID_UCLOGIC=m -CONFIG_HID_UDRAW_PS3=m -CONFIG_HID_UNIVERSAL_PIDFF=m -CONFIG_HID_VIEWSONIC=m -CONFIG_HID_VIVALDI=m -CONFIG_HID_WACOM=m -CONFIG_HID_WALTOP=m -CONFIG_HID_WIIMOTE=m -CONFIG_HID_WINWING=m -CONFIG_HID_XIAOMI=m -CONFIG_HID_XINMO=m -CONFIG_HID_ZEROPLUS=m -CONFIG_HID_ZYDACRON=m -CONFIG_HOLTEK_FF=y -CONFIG_INPUT_JOYDEV=m -CONFIG_INPUT_JOYSTICK=y -CONFIG_INPUT_TABLET=y -CONFIG_JOYSTICK_IFORCE=m -CONFIG_JOYSTICK_IFORCE_USB=m -CONFIG_JOYSTICK_PXRC=m -CONFIG_JOYSTICK_XPAD=m -CONFIG_JOYSTICK_XPAD_FF=y -CONFIG_JOYSTICK_XPAD_LEDS=y -CONFIG_LOGIG940_FF=y -CONFIG_LOGIRUMBLEPAD2_FF=y -CONFIG_LOGITECH_FF=y -CONFIG_LOGIWHEELS_FF=y -CONFIG_MOUSE_SYNAPTICS_USB=m -CONFIG_NINTENDO_FF=y -CONFIG_NVIDIA_SHIELD_FF=y -CONFIG_PANTHERLORD_FF=y -CONFIG_PLAYSTATION_FF=y -CONFIG_RMI4_CORE=m -CONFIG_RMI4_F03=y -CONFIG_RMI4_F11=y -CONFIG_RMI4_F12=y -CONFIG_RMI4_F1A=y -CONFIG_RMI4_F21=y -CONFIG_RMI4_F30=y -CONFIG_RMI4_F34=y -CONFIG_RMI4_F3A=y -CONFIG_RMI4_F55=y -CONFIG_SMARTJOYPLUS_FF=y -CONFIG_SONY_FF=y -CONFIG_STEAM_FF=y -CONFIG_TABLET_USB_ACECAD=m -CONFIG_TABLET_USB_AIPTEK=m -CONFIG_TABLET_USB_HANWANG=m -CONFIG_TABLET_USB_KBTAB=m -CONFIG_TABLET_USB_PEGASUS=m -CONFIG_THRUSTMASTER_FF=y -CONFIG_TOUCHSCREEN_USB_3M=y -CONFIG_TOUCHSCREEN_USB_COMPOSITE=m -CONFIG_TOUCHSCREEN_USB_DMC_TSC10=y -CONFIG_TOUCHSCREEN_USB_E2I=y -CONFIG_TOUCHSCREEN_USB_EASYTOUCH=y -CONFIG_TOUCHSCREEN_USB_EGALAX=y -CONFIG_TOUCHSCREEN_USB_ELO=y -CONFIG_TOUCHSCREEN_USB_ETT_TC45USB=y -CONFIG_TOUCHSCREEN_USB_ETURBO=y -CONFIG_TOUCHSCREEN_USB_GENERAL_TOUCH=y -CONFIG_TOUCHSCREEN_USB_GOTOP=y -CONFIG_TOUCHSCREEN_USB_GUNZE=y -CONFIG_TOUCHSCREEN_USB_IDEALTEK=y -CONFIG_TOUCHSCREEN_USB_IRTOUCH=y -CONFIG_TOUCHSCREEN_USB_ITM=y -CONFIG_TOUCHSCREEN_USB_JASTEC=y -CONFIG_TOUCHSCREEN_USB_NEXIO=y -CONFIG_TOUCHSCREEN_USB_PANJIT=y -CONFIG_TOUCHSCREEN_USB_ZYTRONIC=y -CONFIG_USB_HIDDEV=y -CONFIG_ZEROPLUS_FF=y - -# USB device classes: serial adapters, storage sub-drivers, printers/TMC/WDM, misc USB gadgets, usbmon, USB/IP, card readers, maker adapters -CONFIG_APPLE_MFI_FASTCHARGE=m -CONFIG_DLN2_ADC=m -CONFIG_GNSS_USB=m -CONFIG_GPIB=m -CONFIG_GPIB_AGILENT_82357A=m -CONFIG_GPIB_COMMON=m -CONFIG_GPIB_LPVO=m -CONFIG_GPIB_NI_USB=m -CONFIG_GPIO_DLN2=m -CONFIG_GPIO_MPSSE=m -CONFIG_I2C_CP2615=m -CONFIG_I2C_DIOLAN_U2C=m -CONFIG_I2C_DLN2=m -CONFIG_I2C_TINY_USB=m -CONFIG_MEMSTICK=m -CONFIG_MEMSTICK_REALTEK_USB=m -CONFIG_MFD_DLN2=m -CONFIG_MISC_RTSX_USB=m -CONFIG_MMC_REALTEK_USB=m -CONFIG_MMC_USHC=m -CONFIG_MMC_VUB300=m -CONFIG_MSPRO_BLOCK=m -CONFIG_REALTEK_AUTOPM=y -CONFIG_SENSORS_AQUACOMPUTER_D5NEXT=m -CONFIG_SENSORS_ASUS_ROG_RYUJIN=m -CONFIG_SENSORS_CORSAIR_CPRO=m -CONFIG_SENSORS_CORSAIR_PSU=m -CONFIG_SENSORS_GIGABYTE_WATERFORCE=m -CONFIG_SENSORS_NZXT_KRAKEN2=m -CONFIG_SENSORS_NZXT_KRAKEN3=m -CONFIG_SENSORS_NZXT_SMART2=m -CONFIG_SENSORS_POWERZ=m -CONFIG_SPI_CH341=m -CONFIG_SPI_DLN2=m -CONFIG_USBIP_CORE=m -CONFIG_USBIP_HOST=m -CONFIG_USBIP_VHCI_HCD=m -CONFIG_USBPCWATCHDOG=m -CONFIG_USB_ADUTUX=m -CONFIG_USB_APPLEDISPLAY=m -CONFIG_USB_CHAOSKEY=m -CONFIG_USB_EMI26=m -CONFIG_USB_EMI62=m -CONFIG_USB_EZUSB_FX2=m -CONFIG_USB_IDMOUSE=m -CONFIG_USB_IOWARRIOR=m -CONFIG_USB_ISIGHTFW=m -CONFIG_USB_LCD=m -CONFIG_USB_LD=m -CONFIG_USB_LEGOTOWER=m -CONFIG_USB_MDC800=m -CONFIG_USB_MICROTEK=m -CONFIG_USB_MON=m -CONFIG_USB_PRINTER=m -CONFIG_USB_SERIAL_AIRCABLE=m -CONFIG_USB_SERIAL_ARK3116=m -CONFIG_USB_SERIAL_BELKIN=m -CONFIG_USB_SERIAL_CH341=m -CONFIG_USB_SERIAL_CYBERJACK=m -CONFIG_USB_SERIAL_CYPRESS_M8=m -CONFIG_USB_SERIAL_DIGI_ACCELEPORT=m -CONFIG_USB_SERIAL_EDGEPORT=m -CONFIG_USB_SERIAL_EDGEPORT_TI=m -CONFIG_USB_SERIAL_EMPEG=m -CONFIG_USB_SERIAL_F81232=m -CONFIG_USB_SERIAL_F8153X=m -CONFIG_USB_SERIAL_GARMIN=m -CONFIG_USB_SERIAL_GENERIC=y -CONFIG_USB_SERIAL_IPAQ=m -CONFIG_USB_SERIAL_IPW=m -CONFIG_USB_SERIAL_IR=m -CONFIG_USB_SERIAL_IUU=m -CONFIG_USB_SERIAL_KEYSPAN=m -CONFIG_USB_SERIAL_KEYSPAN_PDA=m -CONFIG_USB_SERIAL_KLSI=m -CONFIG_USB_SERIAL_KOBIL_SCT=m -CONFIG_USB_SERIAL_MCT_U232=m -CONFIG_USB_SERIAL_MOS7720=m -CONFIG_USB_SERIAL_MOS7840=m -CONFIG_USB_SERIAL_NAVMAN=m -CONFIG_USB_SERIAL_OMNINET=m -CONFIG_USB_SERIAL_OPTICON=m -CONFIG_USB_SERIAL_OTI6858=m -CONFIG_USB_SERIAL_PL2303=m -CONFIG_USB_SERIAL_QCAUX=m -CONFIG_USB_SERIAL_QT2=m -CONFIG_USB_SERIAL_QUALCOMM=m -CONFIG_USB_SERIAL_SAFE=m -CONFIG_USB_SERIAL_SAFE_PADDED=y -CONFIG_USB_SERIAL_SIERRAWIRELESS=m -CONFIG_USB_SERIAL_SIMPLE=m -CONFIG_USB_SERIAL_SPCP8X5=m -CONFIG_USB_SERIAL_SSU100=m -CONFIG_USB_SERIAL_SYMBOL=m -CONFIG_USB_SERIAL_TI=m -CONFIG_USB_SERIAL_UPD78F0730=m -CONFIG_USB_SERIAL_VISOR=m -CONFIG_USB_SERIAL_WHITEHEAT=m -CONFIG_USB_SERIAL_XR=m -CONFIG_USB_SERIAL_XSENS_MT=m -CONFIG_USB_SEVSEG=m -CONFIG_USB_SISUSBVGA=m -CONFIG_USB_STORAGE_ALAUDA=m -CONFIG_USB_STORAGE_CYPRESS_ATACB=m -CONFIG_USB_STORAGE_DATAFAB=m -CONFIG_USB_STORAGE_ENE_UB6250=m -CONFIG_USB_STORAGE_FREECOM=m -CONFIG_USB_STORAGE_ISD200=m -CONFIG_USB_STORAGE_JUMPSHOT=m -CONFIG_USB_STORAGE_KARMA=m -CONFIG_USB_STORAGE_ONETOUCH=m -CONFIG_USB_STORAGE_REALTEK=m -CONFIG_USB_STORAGE_SDDR09=m -CONFIG_USB_STORAGE_SDDR55=m -CONFIG_USB_STORAGE_USBAT=m -CONFIG_USB_TMC=m -CONFIG_USB_TRANCEVIBRATOR=m -CONFIG_USB_WDM=m -CONFIG_USB_YUREX=m -CONFIG_W1=m -CONFIG_W1_MASTER_DS2490=m -CONFIG_W1_SLAVE_DS2405=m -CONFIG_W1_SLAVE_DS2406=m -CONFIG_W1_SLAVE_DS2408=m -CONFIG_W1_SLAVE_DS2413=m -CONFIG_W1_SLAVE_DS2423=m -CONFIG_W1_SLAVE_DS2430=m -CONFIG_W1_SLAVE_DS2431=m -CONFIG_W1_SLAVE_DS2433=m -CONFIG_W1_SLAVE_DS2433_CRC=y -CONFIG_W1_SLAVE_DS2438=m -CONFIG_W1_SLAVE_DS2780=m -CONFIG_W1_SLAVE_DS2781=m -CONFIG_W1_SLAVE_DS2805=m -CONFIG_W1_SLAVE_DS28E04=m -CONFIG_W1_SLAVE_SMEM=m -CONFIG_W1_SLAVE_THERM=m -CONFIG_XILLYUSB=m - -# USB network adapters: USB Ethernet/tethering/mobile broadband, USB Wi-Fi dongles, USB CAN adapters -CONFIG_AR5523=m -CONFIG_ATH10K_USB=m -CONFIG_ATH6KL=m -CONFIG_ATH6KL_USB=m -CONFIG_ATH9K_HTC=m -CONFIG_BRCMFMAC_USB=y -CONFIG_CAN_8DEV_USB=m -CONFIG_CAN_EMS_USB=m -CONFIG_CAN_ESD_USB=m -CONFIG_CAN_F81604=m -CONFIG_CAN_GS_USB=m -CONFIG_CAN_ISOTP=m -CONFIG_CAN_J1939=m -CONFIG_CAN_KVASER_USB=m -CONFIG_CAN_MCBA_USB=m -CONFIG_CAN_PEAK_USB=m -CONFIG_CARL9170=m -CONFIG_CARL9170_LEDS=y -CONFIG_LIBERTAS_THINFIRM=m -CONFIG_LIBERTAS_THINFIRM_USB=m -CONFIG_MT7601U=m -CONFIG_MT7663U=m -CONFIG_MT76x0U=m -CONFIG_MT76x2U=m -CONFIG_MT7921U=m -CONFIG_MT7925U=m -CONFIG_RT2500USB=m -CONFIG_RT2800USB=m -CONFIG_RT2800USB_RT33XX=y -CONFIG_RT2800USB_RT3573=y -CONFIG_RT2800USB_RT35XX=y -CONFIG_RT2800USB_RT53XX=y -CONFIG_RT2800USB_RT55XX=y -CONFIG_RT2800USB_UNKNOWN=y -CONFIG_RT2X00=m -CONFIG_RT73USB=m -CONFIG_RTL8187=m -CONFIG_RTL8192DU=m -CONFIG_RTL8XXXU=m -CONFIG_RTW88=m -CONFIG_RTW88_8723DU=m -CONFIG_RTW88_8812AU=m -CONFIG_RTW88_8814AU=m -CONFIG_RTW88_8821AU=m -CONFIG_RTW88_8821CU=m -CONFIG_RTW88_8822BU=m -CONFIG_RTW88_8822CU=m -CONFIG_RTW89=m -CONFIG_RTW89_8851BU=m -CONFIG_RTW89_8852AU=m -CONFIG_RTW89_8852BU=m -CONFIG_RTW89_8852CU=m -CONFIG_USB_ALI_M5632=y -CONFIG_USB_AN2720=y -CONFIG_USB_CATC=m -CONFIG_USB_EPSON2888=y -CONFIG_USB_HSO=m -CONFIG_USB_IPHETH=m -CONFIG_USB_KAWETH=m -CONFIG_USB_KC2190=y -CONFIG_USB_NET_AQC111=m -CONFIG_USB_NET_CDC_EEM=m -CONFIG_USB_NET_CDC_MBIM=m -CONFIG_USB_NET_CH9200=m -CONFIG_USB_NET_CX82310_ETH=m -CONFIG_USB_NET_GL620A=m -CONFIG_USB_NET_HUAWEI_CDC_NCM=m -CONFIG_USB_NET_INT51X1=m -CONFIG_USB_NET_KALMIA=m -CONFIG_USB_NET_QMI_WWAN=m -CONFIG_USB_NET_RNDIS_HOST=m -CONFIG_USB_NET_SR9700=m -CONFIG_USB_SIERRA_NET=m -CONFIG_USB_VL600=m -CONFIG_ZD1211RW=m - -# Storage: optical/SCSI generic, device-mapper (LUKS, LVM, verity, integrity), MD RAID, partition tables -CONFIG_AIX_PARTITION=y -CONFIG_BLK_DEV_SR=m -CONFIG_BSD_DISKLABEL=y -CONFIG_CHR_DEV_SG=m -CONFIG_DM_CACHE=m -CONFIG_DM_CACHE_SMQ=m -CONFIG_DM_CRYPT=m -CONFIG_DM_INTEGRITY=m -CONFIG_DM_RAID=m -CONFIG_DM_SNAPSHOT=m -CONFIG_DM_THIN_PROVISIONING=m -CONFIG_DM_UEVENT=y -CONFIG_DM_VERITY=m -CONFIG_DM_VERITY_FEC=y -CONFIG_DM_WRITECACHE=m -CONFIG_LDM_PARTITION=y -CONFIG_MAC_PARTITION=y -CONFIG_MD_LINEAR=m -CONFIG_MD_RAID0=m -CONFIG_MD_RAID1=m -CONFIG_MD_RAID10=m -CONFIG_MD_RAID456=m -CONFIG_MINIX_SUBPARTITION=y -CONFIG_OSF_PARTITION=y -CONFIG_PARTITION_ADVANCED=y -CONFIG_SGI_PARTITION=y -CONFIG_SOLARIS_X86_PARTITION=y -CONFIG_SUN_PARTITION=y -CONFIG_UNIXWARE_DISKLABEL=y - -# Filesystems for removable media and images, binfmt_misc, SMB client -CONFIG_BINFMT_MISC=m -CONFIG_CIFS=m -CONFIG_CIFS_ALLOW_INSECURE_LEGACY=y -CONFIG_CIFS_DFS_UPCALL=y -CONFIG_CIFS_POSIX=y -CONFIG_CIFS_SWN_UPCALL=y -CONFIG_CIFS_UPCALL=y -CONFIG_CIFS_XATTR=y -CONFIG_EROFS_FS=m -CONFIG_EROFS_FS_BACKED_BY_FILE=y -CONFIG_EROFS_FS_POSIX_ACL=y -CONFIG_EROFS_FS_SECURITY=y -CONFIG_EROFS_FS_XATTR=y -CONFIG_EROFS_FS_ZIP=y -CONFIG_EROFS_FS_ZIP_ACCEL=y -CONFIG_EROFS_FS_ZIP_DEFLATE=y -CONFIG_EROFS_FS_ZIP_LZMA=y -CONFIG_EROFS_FS_ZIP_ZSTD=y -CONFIG_EXFAT_FS=m -CONFIG_F2FS_FS=m -CONFIG_F2FS_FS_COMPRESSION=y -CONFIG_F2FS_FS_LZ4=y -CONFIG_F2FS_FS_LZ4HC=y -CONFIG_F2FS_FS_LZO=y -CONFIG_F2FS_FS_LZORLE=y -CONFIG_F2FS_FS_POSIX_ACL=y -CONFIG_F2FS_FS_SECURITY=y -CONFIG_F2FS_FS_XATTR=y -CONFIG_F2FS_FS_ZSTD=y -CONFIG_F2FS_IOSTAT=y -CONFIG_F2FS_STAT_FS=y -CONFIG_F2FS_UNFAIR_RWSEM=y -CONFIG_HFSPLUS_FS=m -CONFIG_HFS_FS=m -CONFIG_ISO9660_FS=m -CONFIG_JOLIET=y -CONFIG_MSDOS_FS=m -CONFIG_NLS_ASCII=m -CONFIG_NLS_CODEPAGE_1250=m -CONFIG_NLS_CODEPAGE_1251=m -CONFIG_NLS_CODEPAGE_737=m -CONFIG_NLS_CODEPAGE_775=m -CONFIG_NLS_CODEPAGE_850=m -CONFIG_NLS_CODEPAGE_852=m -CONFIG_NLS_CODEPAGE_855=m -CONFIG_NLS_CODEPAGE_857=m -CONFIG_NLS_CODEPAGE_860=m -CONFIG_NLS_CODEPAGE_861=m -CONFIG_NLS_CODEPAGE_862=m -CONFIG_NLS_CODEPAGE_863=m -CONFIG_NLS_CODEPAGE_864=m -CONFIG_NLS_CODEPAGE_865=m -CONFIG_NLS_CODEPAGE_866=m -CONFIG_NLS_CODEPAGE_869=m -CONFIG_NLS_CODEPAGE_874=m -CONFIG_NLS_CODEPAGE_932=m -CONFIG_NLS_CODEPAGE_936=m -CONFIG_NLS_CODEPAGE_949=m -CONFIG_NLS_CODEPAGE_950=m -CONFIG_NLS_ISO8859_13=m -CONFIG_NLS_ISO8859_14=m -CONFIG_NLS_ISO8859_15=m -CONFIG_NLS_ISO8859_2=m -CONFIG_NLS_ISO8859_3=m -CONFIG_NLS_ISO8859_4=m -CONFIG_NLS_ISO8859_5=m -CONFIG_NLS_ISO8859_6=m -CONFIG_NLS_ISO8859_7=m -CONFIG_NLS_ISO8859_8=m -CONFIG_NLS_ISO8859_9=m -CONFIG_NLS_KOI8_R=m -CONFIG_NLS_KOI8_U=m -CONFIG_NLS_MAC_CELTIC=m -CONFIG_NLS_MAC_CENTEURO=m -CONFIG_NLS_MAC_CROATIAN=m -CONFIG_NLS_MAC_CYRILLIC=m -CONFIG_NLS_MAC_GAELIC=m -CONFIG_NLS_MAC_GREEK=m -CONFIG_NLS_MAC_ICELAND=m -CONFIG_NLS_MAC_INUIT=m -CONFIG_NLS_MAC_ROMAN=m -CONFIG_NLS_MAC_ROMANIAN=m -CONFIG_NLS_MAC_TURKISH=m -CONFIG_NLS_UTF8=m -CONFIG_NTFS3_FS=m -CONFIG_NTFS3_FS_POSIX_ACL=y -CONFIG_NTFS3_LZX_XPRESS=y -CONFIG_SQUASHFS_LZ4=y -CONFIG_SQUASHFS_LZO=y -CONFIG_SQUASHFS_XATTR=y -CONFIG_SQUASHFS_XZ=y -CONFIG_SQUASHFS_ZSTD=y -CONFIG_UDF_FS=m -CONFIG_XFS_FS=m -CONFIG_XFS_ONLINE_REPAIR=y -CONFIG_XFS_ONLINE_SCRUB=y -CONFIG_XFS_ONLINE_SCRUB_STATS=y -CONFIG_XFS_POSIX_ACL=y -CONFIG_XFS_QUOTA=y -CONFIG_XFS_RT=y -CONFIG_XFS_SUPPORT_ASCII_CI=y -CONFIG_XFS_SUPPORT_V4=y -CONFIG_ZISOFS=y - -# Network protocols: WireGuard/OpenVPN-DCO/L2TP/PPP/IPsec/kTLS, tunnels, policy routing, nftables+xtables+ipset+ebtables, tc, TCP CC, ss diag, virtual links -CONFIG_BONDING=m -CONFIG_BRIDGE_EBT_802_3=m -CONFIG_BRIDGE_EBT_AMONG=m -CONFIG_BRIDGE_EBT_ARP=m -CONFIG_BRIDGE_EBT_ARPREPLY=m -CONFIG_BRIDGE_EBT_BROUTE=m -CONFIG_BRIDGE_EBT_DNAT=m -CONFIG_BRIDGE_EBT_IP=m -CONFIG_BRIDGE_EBT_IP6=m -CONFIG_BRIDGE_EBT_LIMIT=m -CONFIG_BRIDGE_EBT_LOG=m -CONFIG_BRIDGE_EBT_MARK=m -CONFIG_BRIDGE_EBT_MARK_T=m -CONFIG_BRIDGE_EBT_NFLOG=m -CONFIG_BRIDGE_EBT_PKTTYPE=m -CONFIG_BRIDGE_EBT_REDIRECT=m -CONFIG_BRIDGE_EBT_SNAT=m -CONFIG_BRIDGE_EBT_STP=m -CONFIG_BRIDGE_EBT_T_FILTER=m -CONFIG_BRIDGE_EBT_T_NAT=m -CONFIG_BRIDGE_EBT_VLAN=m -CONFIG_BRIDGE_NF_EBTABLES=m -CONFIG_BRIDGE_NF_EBTABLES_LEGACY=m -CONFIG_CLS_U32_MARK=y -CONFIG_CLS_U32_PERF=y -CONFIG_DEFAULT_CUBIC=y -CONFIG_DUMMY=m -CONFIG_GACT_PROB=y -CONFIG_GENEVE=m -CONFIG_IFB=m -CONFIG_INET6_AH=m -CONFIG_INET6_ESP=m -CONFIG_INET6_ESPINTCP=y -CONFIG_INET6_ESP_OFFLOAD=m -CONFIG_INET6_IPCOMP=m -CONFIG_INET_AH=m -CONFIG_INET_DIAG_DESTROY=y -CONFIG_INET_ESP=m -CONFIG_INET_ESPINTCP=y -CONFIG_INET_ESP_OFFLOAD=m -CONFIG_INET_IPCOMP=m -CONFIG_INET_RAW_DIAG=m -CONFIG_INET_UDP_DIAG=m -CONFIG_IP6_NF_FILTER=m -CONFIG_IP6_NF_IPTABLES_LEGACY=m -CONFIG_IP6_NF_MANGLE=m -CONFIG_IP6_NF_MATCH_AH=m -CONFIG_IP6_NF_MATCH_EUI64=m -CONFIG_IP6_NF_MATCH_FRAG=m -CONFIG_IP6_NF_MATCH_HL=m -CONFIG_IP6_NF_MATCH_IPV6HEADER=m -CONFIG_IP6_NF_MATCH_MH=m -CONFIG_IP6_NF_MATCH_OPTS=m -CONFIG_IP6_NF_MATCH_RPFILTER=m -CONFIG_IP6_NF_MATCH_RT=m -CONFIG_IP6_NF_MATCH_SRH=m -CONFIG_IP6_NF_NAT=m -CONFIG_IP6_NF_RAW=m -CONFIG_IP6_NF_TARGET_HL=m -CONFIG_IP6_NF_TARGET_MASQUERADE=m -CONFIG_IP6_NF_TARGET_NPT=m -CONFIG_IP6_NF_TARGET_REJECT=m -CONFIG_IPV6_GRE=m -CONFIG_IPV6_MULTIPLE_TABLES=y -CONFIG_IPV6_TUNNEL=m -CONFIG_IPV6_VTI=m -CONFIG_IP_ADVANCED_ROUTER=y -CONFIG_IP_MULTIPLE_TABLES=y -CONFIG_IP_NF_ARPFILTER=m -CONFIG_IP_NF_ARPTABLES=m -CONFIG_IP_NF_ARP_MANGLE=m -CONFIG_IP_NF_FILTER=m -CONFIG_IP_NF_IPTABLES_LEGACY=m -CONFIG_IP_NF_MANGLE=m -CONFIG_IP_NF_MATCH_AH=m -CONFIG_IP_NF_MATCH_ECN=m -CONFIG_IP_NF_MATCH_RPFILTER=m -CONFIG_IP_NF_MATCH_TTL=m -CONFIG_IP_NF_NAT=m -CONFIG_IP_NF_RAW=m -CONFIG_IP_NF_TARGET_ECN=m -CONFIG_IP_NF_TARGET_MASQUERADE=m -CONFIG_IP_NF_TARGET_NETMAP=m -CONFIG_IP_NF_TARGET_REDIRECT=m -CONFIG_IP_NF_TARGET_REJECT=m -CONFIG_IP_NF_TARGET_TTL=m -CONFIG_IP_ROUTE_MULTIPATH=y -CONFIG_IP_SET=m -CONFIG_IP_SET_BITMAP_IP=m -CONFIG_IP_SET_BITMAP_IPMAC=m -CONFIG_IP_SET_BITMAP_PORT=m -CONFIG_IP_SET_HASH_IP=m -CONFIG_IP_SET_HASH_IPMAC=m -CONFIG_IP_SET_HASH_IPMARK=m -CONFIG_IP_SET_HASH_IPPORT=m -CONFIG_IP_SET_HASH_IPPORTIP=m -CONFIG_IP_SET_HASH_IPPORTNET=m -CONFIG_IP_SET_HASH_MAC=m -CONFIG_IP_SET_HASH_NET=m -CONFIG_IP_SET_HASH_NETIFACE=m -CONFIG_IP_SET_HASH_NETNET=m -CONFIG_IP_SET_HASH_NETPORT=m -CONFIG_IP_SET_HASH_NETPORTNET=m -CONFIG_IP_SET_LIST_SET=m -CONFIG_L2TP=m -CONFIG_L2TP_ETH=m -CONFIG_L2TP_IP=m -CONFIG_L2TP_V3=y -CONFIG_NETFILTER_NETLINK_ACCT=m -CONFIG_NETFILTER_NETLINK_GLUE_CT=y -CONFIG_NETFILTER_NETLINK_HOOK=m -CONFIG_NETFILTER_NETLINK_LOG=m -CONFIG_NETFILTER_NETLINK_OSF=m -CONFIG_NETFILTER_NETLINK_QUEUE=m -CONFIG_NETFILTER_XTABLES_LEGACY=y -CONFIG_NETFILTER_XT_CONNMARK=m -CONFIG_NETFILTER_XT_MATCH_BPF=m -CONFIG_NETFILTER_XT_MATCH_CGROUP=m -CONFIG_NETFILTER_XT_MATCH_CLUSTER=m -CONFIG_NETFILTER_XT_MATCH_COMMENT=m -CONFIG_NETFILTER_XT_MATCH_CONNBYTES=m -CONFIG_NETFILTER_XT_MATCH_CONNLABEL=m -CONFIG_NETFILTER_XT_MATCH_CONNLIMIT=m -CONFIG_NETFILTER_XT_MATCH_CONNMARK=m -CONFIG_NETFILTER_XT_MATCH_CPU=m -CONFIG_NETFILTER_XT_MATCH_DCCP=m -CONFIG_NETFILTER_XT_MATCH_DEVGROUP=m -CONFIG_NETFILTER_XT_MATCH_DSCP=m -CONFIG_NETFILTER_XT_MATCH_ECN=m -CONFIG_NETFILTER_XT_MATCH_ESP=m -CONFIG_NETFILTER_XT_MATCH_HASHLIMIT=m -CONFIG_NETFILTER_XT_MATCH_HELPER=m -CONFIG_NETFILTER_XT_MATCH_HL=m -CONFIG_NETFILTER_XT_MATCH_IPCOMP=m -CONFIG_NETFILTER_XT_MATCH_IPRANGE=m -CONFIG_NETFILTER_XT_MATCH_L2TP=m -CONFIG_NETFILTER_XT_MATCH_LENGTH=m -CONFIG_NETFILTER_XT_MATCH_LIMIT=m -CONFIG_NETFILTER_XT_MATCH_MAC=m -CONFIG_NETFILTER_XT_MATCH_MARK=m -CONFIG_NETFILTER_XT_MATCH_MULTIPORT=m -CONFIG_NETFILTER_XT_MATCH_NFACCT=m -CONFIG_NETFILTER_XT_MATCH_OSF=m -CONFIG_NETFILTER_XT_MATCH_OWNER=m -CONFIG_NETFILTER_XT_MATCH_PHYSDEV=m -CONFIG_NETFILTER_XT_MATCH_PKTTYPE=m -CONFIG_NETFILTER_XT_MATCH_POLICY=m -CONFIG_NETFILTER_XT_MATCH_QUOTA=m -CONFIG_NETFILTER_XT_MATCH_RATEEST=m -CONFIG_NETFILTER_XT_MATCH_REALM=m -CONFIG_NETFILTER_XT_MATCH_RECENT=m -CONFIG_NETFILTER_XT_MATCH_SCTP=m -CONFIG_NETFILTER_XT_MATCH_SOCKET=m -CONFIG_NETFILTER_XT_MATCH_STATE=m -CONFIG_NETFILTER_XT_MATCH_STATISTIC=m -CONFIG_NETFILTER_XT_MATCH_STRING=m -CONFIG_NETFILTER_XT_MATCH_TCPMSS=m -CONFIG_NETFILTER_XT_MATCH_TIME=m -CONFIG_NETFILTER_XT_MATCH_U32=m -CONFIG_NETFILTER_XT_NAT=m -CONFIG_NETFILTER_XT_SET=m -CONFIG_NETFILTER_XT_TARGET_AUDIT=m -CONFIG_NETFILTER_XT_TARGET_CHECKSUM=m -CONFIG_NETFILTER_XT_TARGET_CLASSIFY=m -CONFIG_NETFILTER_XT_TARGET_CONNMARK=m -CONFIG_NETFILTER_XT_TARGET_CT=m -CONFIG_NETFILTER_XT_TARGET_DSCP=m -CONFIG_NETFILTER_XT_TARGET_HL=m -CONFIG_NETFILTER_XT_TARGET_HMARK=m -CONFIG_NETFILTER_XT_TARGET_IDLETIMER=m -CONFIG_NETFILTER_XT_TARGET_LED=m -CONFIG_NETFILTER_XT_TARGET_MARK=m -CONFIG_NETFILTER_XT_TARGET_MASQUERADE=m -CONFIG_NETFILTER_XT_TARGET_NETMAP=m -CONFIG_NETFILTER_XT_TARGET_NFLOG=m -CONFIG_NETFILTER_XT_TARGET_NFQUEUE=m -CONFIG_NETFILTER_XT_TARGET_NOTRACK=m -CONFIG_NETFILTER_XT_TARGET_RATEEST=m -CONFIG_NETFILTER_XT_TARGET_REDIRECT=m -CONFIG_NETFILTER_XT_TARGET_TCPMSS=m -CONFIG_NETFILTER_XT_TARGET_TCPOPTSTRIP=m -CONFIG_NETFILTER_XT_TARGET_TEE=m -CONFIG_NETFILTER_XT_TARGET_TPROXY=m -CONFIG_NETFILTER_XT_TARGET_TRACE=m -CONFIG_NETLINK_DIAG=m -CONFIG_NET_ACT_BPF=m -CONFIG_NET_ACT_CONNMARK=m -CONFIG_NET_ACT_CSUM=m -CONFIG_NET_ACT_CT=m -CONFIG_NET_ACT_CTINFO=m -CONFIG_NET_ACT_MPLS=m -CONFIG_NET_ACT_NAT=m -CONFIG_NET_ACT_PEDIT=m -CONFIG_NET_ACT_POLICE=m -CONFIG_NET_ACT_SAMPLE=m -CONFIG_NET_ACT_SIMP=m -CONFIG_NET_ACT_SKBEDIT=m -CONFIG_NET_ACT_SKBMOD=m -CONFIG_NET_ACT_TUNNEL_KEY=m -CONFIG_NET_ACT_VLAN=m -CONFIG_NET_CLS_BPF=m -CONFIG_NET_CLS_FLOW=m -CONFIG_NET_CLS_FW=m -CONFIG_NET_CLS_MATCHALL=m -CONFIG_NET_CLS_ROUTE4=m -CONFIG_NET_CLS_U32=m -CONFIG_NET_EMATCH=y -CONFIG_NET_EMATCH_CMP=m -CONFIG_NET_EMATCH_IPSET=m -CONFIG_NET_EMATCH_IPT=m -CONFIG_NET_EMATCH_META=m -CONFIG_NET_EMATCH_NBYTE=m -CONFIG_NET_EMATCH_TEXT=m -CONFIG_NET_EMATCH_U32=m -CONFIG_NET_FOU=m -CONFIG_NET_FOU_IP_TUNNELS=y -CONFIG_NET_IPGRE=m -CONFIG_NET_IPGRE_BROADCAST=y -CONFIG_NET_IPGRE_DEMUX=m -CONFIG_NET_IPIP=m -CONFIG_NET_IPVTI=m -CONFIG_NET_KEY=m -CONFIG_NET_KEY_MIGRATE=y -CONFIG_NET_SCH_CAKE=m -CONFIG_NET_SCH_CHOKE=m -CONFIG_NET_SCH_CODEL=m -CONFIG_NET_SCH_DRR=m -CONFIG_NET_SCH_DUALPI2=m -CONFIG_NET_SCH_ETS=m -CONFIG_NET_SCH_FQ=m -CONFIG_NET_SCH_FQ_CODEL=m -CONFIG_NET_SCH_FQ_PIE=m -CONFIG_NET_SCH_GRED=m -CONFIG_NET_SCH_HFSC=m -CONFIG_NET_SCH_HHF=m -CONFIG_NET_SCH_HTB=m -CONFIG_NET_SCH_MULTIQ=m -CONFIG_NET_SCH_NETEM=m -CONFIG_NET_SCH_PIE=m -CONFIG_NET_SCH_PLUG=m -CONFIG_NET_SCH_PRIO=m -CONFIG_NET_SCH_QFQ=m -CONFIG_NET_SCH_RED=m -CONFIG_NET_SCH_SFB=m -CONFIG_NET_SCH_SFQ=m -CONFIG_NET_SCH_TBF=m -CONFIG_NET_SCH_TEQL=m -CONFIG_NET_TEAM=m -CONFIG_NET_TEAM_MODE_ACTIVEBACKUP=m -CONFIG_NET_TEAM_MODE_BROADCAST=m -CONFIG_NET_TEAM_MODE_LOADBALANCE=m -CONFIG_NET_TEAM_MODE_RANDOM=m -CONFIG_NET_TEAM_MODE_ROUNDROBIN=m -CONFIG_NFT_BRIDGE_META=m -CONFIG_NFT_BRIDGE_REJECT=m -CONFIG_NFT_COMPAT=m -CONFIG_NFT_CONNLIMIT=m -CONFIG_NFT_DUP_IPV4=m -CONFIG_NFT_DUP_IPV6=m -CONFIG_NFT_DUP_NETDEV=m -CONFIG_NFT_FIB_NETDEV=m -CONFIG_NFT_FLOW_OFFLOAD=m -CONFIG_NFT_FWD_NETDEV=m -CONFIG_NFT_HASH=m -CONFIG_NFT_NUMGEN=m -CONFIG_NFT_OSF=m -CONFIG_NFT_QUEUE=m -CONFIG_NFT_QUOTA=m -CONFIG_NFT_REDIR=m -CONFIG_NFT_REJECT_NETDEV=m -CONFIG_NFT_SOCKET=m -CONFIG_NFT_TPROXY=m -CONFIG_NFT_TUNNEL=m -CONFIG_NFT_XFRM=m -CONFIG_NF_CONNTRACK_AMANDA=m -CONFIG_NF_CONNTRACK_BRIDGE=m -CONFIG_NF_CONNTRACK_FTP=m -CONFIG_NF_CONNTRACK_H323=m -CONFIG_NF_CONNTRACK_IRC=m -CONFIG_NF_CONNTRACK_LABELS=y -CONFIG_NF_CONNTRACK_MARK=y -CONFIG_NF_CONNTRACK_NETBIOS_NS=m -CONFIG_NF_CONNTRACK_PPTP=m -CONFIG_NF_CONNTRACK_PROCFS=y -CONFIG_NF_CONNTRACK_SANE=m -CONFIG_NF_CONNTRACK_SIP=m -CONFIG_NF_CONNTRACK_SNMP=m -CONFIG_NF_CONNTRACK_TFTP=m -CONFIG_NF_CONNTRACK_TIMEOUT=y -CONFIG_NF_CONNTRACK_TIMESTAMP=y -CONFIG_NF_CONNTRACK_ZONES=y -CONFIG_NF_CT_NETLINK=m -CONFIG_NF_CT_NETLINK_HELPER=m -CONFIG_NF_CT_NETLINK_TIMEOUT=m -CONFIG_NF_DUP_IPV4=m -CONFIG_NF_DUP_IPV6=m -CONFIG_NF_DUP_NETDEV=m -CONFIG_NF_FLOW_TABLE=m -CONFIG_NF_FLOW_TABLE_INET=m -CONFIG_NF_FLOW_TABLE_PROCFS=y -CONFIG_NF_LOG_ARP=m -CONFIG_NF_LOG_IPV4=m -CONFIG_NF_NAT_SNMP_BASIC=m -CONFIG_NF_SOCKET_IPV4=m -CONFIG_NF_SOCKET_IPV6=m -CONFIG_NF_TABLES_ARP=y -CONFIG_NF_TABLES_BRIDGE=m -CONFIG_NF_TPROXY_IPV4=m -CONFIG_NF_TPROXY_IPV6=m -CONFIG_OVPN=m -CONFIG_PACKET_DIAG=m -CONFIG_PPP=m -CONFIG_PPPOE=m -CONFIG_PPPOE_HASH_BITS_4=y -CONFIG_PPPOL2TP=m -CONFIG_PPP_ASYNC=m -CONFIG_PPP_BSDCOMP=m -CONFIG_PPP_DEFLATE=m -CONFIG_PPP_FILTER=y -CONFIG_PPP_MPPE=m -CONFIG_PPP_MULTILINK=y -CONFIG_PPP_SYNC_TTY=m -CONFIG_PPTP=m -CONFIG_PSAMPLE=m -CONFIG_TCP_CONG_ADVANCED=y -CONFIG_TCP_CONG_BBR=m -CONFIG_TCP_CONG_BIC=m -CONFIG_TCP_CONG_CDG=m -CONFIG_TCP_CONG_DCTCP=m -CONFIG_TCP_CONG_HSTCP=m -CONFIG_TCP_CONG_HTCP=m -CONFIG_TCP_CONG_HYBLA=m -CONFIG_TCP_CONG_ILLINOIS=m -CONFIG_TCP_CONG_LP=m -CONFIG_TCP_CONG_NV=m -CONFIG_TCP_CONG_SCALABLE=m -CONFIG_TCP_CONG_VEGAS=m -CONFIG_TCP_CONG_VENO=m -CONFIG_TCP_CONG_WESTWOOD=m -CONFIG_TCP_CONG_YEAH=m -CONFIG_TLS=m -CONFIG_UNIX_DIAG=m -CONFIG_VXLAN=m -CONFIG_WIREGUARD=m -CONFIG_XFRM_INTERFACE=m -CONFIG_XFRM_MIGRATE=y -CONFIG_XFRM_STATISTICS=y -CONFIG_XFRM_USER=m - -# Bluetooth: USB dongle drivers, vhci, MSFT/AOSP vendor extensions (USB controllers only) -CONFIG_BT_AOSPEXT=y -CONFIG_BT_ATH3K=m -CONFIG_BT_HCIBCM203X=m -CONFIG_BT_HCIBFUSB=m -CONFIG_BT_HCIBPA10X=m -CONFIG_BT_HCIBTUSB_AUTOSUSPEND=y -CONFIG_BT_HCIVHCI=m -CONFIG_BT_MSFTEXT=y - -# Containers / virtualisation: CRIU/checkpoint, userfaultfd, misc cgroup, KVM host vhost-net/vhost-vsock/vsock -CONFIG_CGROUP_MISC=y -CONFIG_CHECKPOINT_RESTORE=y -CONFIG_USERFAULTFD=y -CONFIG_VHOST_NET=m -CONFIG_VHOST_VSOCK=m -CONFIG_VSOCKETS=m -CONFIG_VSOCKETS_DIAG=m -CONFIG_VSOCKETS_LOOPBACK=m - -# Crypto: AF_ALG user API (cryptsetup/iwd), dm-crypt/fscrypt modes, VeraCrypt/TrueCrypt ciphers+hashes, IPsec AEADs -CONFIG_CRYPTO_ADIANTUM=m -CONFIG_CRYPTO_AEGIS128=m -CONFIG_CRYPTO_AEGIS128_SIMD=y -CONFIG_CRYPTO_BLAKE2B=m -CONFIG_CRYPTO_BLOWFISH=m -CONFIG_CRYPTO_CAMELLIA=m -CONFIG_CRYPTO_CAST5=m -CONFIG_CRYPTO_CAST6=m -CONFIG_CRYPTO_CHACHA20POLY1305=m -CONFIG_CRYPTO_CRC32=m -CONFIG_CRYPTO_CRC32C=m -CONFIG_CRYPTO_CRYPTD=m -CONFIG_CRYPTO_CTS=m -CONFIG_CRYPTO_ESSIV=m -CONFIG_CRYPTO_HCTR2=m -CONFIG_CRYPTO_LRW=m -CONFIG_CRYPTO_LZ4HC=m -CONFIG_CRYPTO_MD4=m -CONFIG_CRYPTO_NULL=m -CONFIG_CRYPTO_PCBC=m -CONFIG_CRYPTO_RMD160=m -CONFIG_CRYPTO_SEQIV=m -CONFIG_CRYPTO_SERPENT=m -CONFIG_CRYPTO_STREEBOG=m -CONFIG_CRYPTO_TWOFISH=m -CONFIG_CRYPTO_USER_API_AEAD=m -CONFIG_CRYPTO_USER_API_HASH=m -CONFIG_CRYPTO_USER_API_SKCIPHER=m -CONFIG_CRYPTO_WP512=m -CONFIG_CRYPTO_XCBC=m -CONFIG_CRYPTO_XXHASH=m - -# Sound: USB audio/MIDI device drivers, MIDI 2.0, virtual MIDI, dummy card, userspace timers -CONFIG_SND_BCD2000=m -CONFIG_SND_DUMMY=m -CONFIG_SND_SEQ_UMP=y -CONFIG_SND_UMP_LEGACY_RAWMIDI=y -CONFIG_SND_USB_6FIRE=m -CONFIG_SND_USB_AUDIO_MIDI_V2=y -CONFIG_SND_USB_CAIAQ=m -CONFIG_SND_USB_CAIAQ_INPUT=y -CONFIG_SND_USB_HIFACE=m -CONFIG_SND_USB_POD=m -CONFIG_SND_USB_PODHD=m -CONFIG_SND_USB_TONEPORT=m -CONFIG_SND_USB_UA101=m -CONFIG_SND_USB_VARIAX=m -CONFIG_SND_UTIMER=y -CONFIG_SND_VIRMIDI=m - -# Video: non-UVC USB webcams (gspca family, Philips pwc), USB displays (DisplayLink USB2 udl, GUD, GM12U320) -CONFIG_DRM_GM12U320=m -CONFIG_DRM_GUD=m -CONFIG_DRM_UDL=m -CONFIG_USB_GL860=m -CONFIG_USB_GSPCA=m -CONFIG_USB_GSPCA_BENQ=m -CONFIG_USB_GSPCA_CONEX=m -CONFIG_USB_GSPCA_CPIA1=m -CONFIG_USB_GSPCA_DTCS033=m -CONFIG_USB_GSPCA_ETOMS=m -CONFIG_USB_GSPCA_FINEPIX=m -CONFIG_USB_GSPCA_JEILINJ=m -CONFIG_USB_GSPCA_JL2005BCD=m -CONFIG_USB_GSPCA_KINECT=m -CONFIG_USB_GSPCA_KONICA=m -CONFIG_USB_GSPCA_MARS=m -CONFIG_USB_GSPCA_MR97310A=m -CONFIG_USB_GSPCA_NW80X=m -CONFIG_USB_GSPCA_OV519=m -CONFIG_USB_GSPCA_OV534=m -CONFIG_USB_GSPCA_OV534_9=m -CONFIG_USB_GSPCA_PAC207=m -CONFIG_USB_GSPCA_PAC7302=m -CONFIG_USB_GSPCA_PAC7311=m -CONFIG_USB_GSPCA_SE401=m -CONFIG_USB_GSPCA_SN9C2028=m -CONFIG_USB_GSPCA_SN9C20X=m -CONFIG_USB_GSPCA_SONIXB=m -CONFIG_USB_GSPCA_SONIXJ=m -CONFIG_USB_GSPCA_SPCA1528=m -CONFIG_USB_GSPCA_SPCA500=m -CONFIG_USB_GSPCA_SPCA501=m -CONFIG_USB_GSPCA_SPCA505=m -CONFIG_USB_GSPCA_SPCA506=m -CONFIG_USB_GSPCA_SPCA508=m -CONFIG_USB_GSPCA_SPCA561=m -CONFIG_USB_GSPCA_SQ905=m -CONFIG_USB_GSPCA_SQ905C=m -CONFIG_USB_GSPCA_SQ930X=m -CONFIG_USB_GSPCA_STK014=m -CONFIG_USB_GSPCA_STK1135=m -CONFIG_USB_GSPCA_STV0680=m -CONFIG_USB_GSPCA_SUNPLUS=m -CONFIG_USB_GSPCA_T613=m -CONFIG_USB_GSPCA_TOPRO=m -CONFIG_USB_GSPCA_TOUPTEK=m -CONFIG_USB_GSPCA_TV8532=m -CONFIG_USB_GSPCA_VC032X=m -CONFIG_USB_GSPCA_VICAM=m -CONFIG_USB_GSPCA_XIRLINK_CIT=m -CONFIG_USB_GSPCA_ZC3XX=m -CONFIG_USB_M5602=m -CONFIG_USB_PWC=m -CONFIG_USB_PWC_INPUT_EVDEV=y -CONFIG_USB_STV06XX=m - -# Other user-facing: Kerberos persistent keyrings, Speakup screen reader, Wine/Proton ntsync -CONFIG_ACCESSIBILITY=y -CONFIG_NTSYNC=m -CONFIG_PERSISTENT_KEYRINGS=y -CONFIG_SPEAKUP=m -CONFIG_SPEAKUP_SYNTH_ACNTSA=m -CONFIG_SPEAKUP_SYNTH_APOLLO=m -CONFIG_SPEAKUP_SYNTH_AUDPTR=m -CONFIG_SPEAKUP_SYNTH_BNS=m -CONFIG_SPEAKUP_SYNTH_DECTLK=m -CONFIG_SPEAKUP_SYNTH_LTLK=m -CONFIG_SPEAKUP_SYNTH_SOFT=m -CONFIG_SPEAKUP_SYNTH_SPKOUT=m -CONFIG_SPEAKUP_SYNTH_TXPRT=m diff --git a/arch/arm64/configs/j700-radio.config b/arch/arm64/configs/j700-radio.config deleted file mode 100644 index f5c5d3c3ec0bf..0000000000000 --- a/arch/arm64/configs/j700-radio.config +++ /dev/null @@ -1,20 +0,0 @@ -# J700 retained radio bootstrap; merge after defconfig and asahi.config. -# System sleep is retained; active radio firmware cannot cross kexec. -# Firmware/calibration and runtime admission follow the MT7932 documentation. -# CONFIG_KEXEC is not set -# CONFIG_KEXEC_FILE is not set -# CONFIG_KEXEC_HANDOVER is not set -CONFIG_PCI=y -CONFIG_PCIE_APPLE=y -CONFIG_APPLE_DART=y -CONFIG_PCIE_APPLE_PIODMA_DIAG=y -CONFIG_CFG80211=m -CONFIG_WLAN=y -CONFIG_WLAN_VENDOR_MEDIATEK=y -CONFIG_MT7932_FULLMAC=m -CONFIG_BT=m -CONFIG_BT_BREDR=y -CONFIG_BT_LE=y -CONFIG_BT_MTK7932_PCIE=m -CONFIG_CRYPTO_AES=y -CONFIG_CRYPTO_CMAC=y diff --git a/arch/arm64/configs/j700.config b/arch/arm64/configs/j700.config deleted file mode 100644 index 976fd65b7161c..0000000000000 --- a/arch/arm64/configs/j700.config +++ /dev/null @@ -1,113 +0,0 @@ -# J700 (MacBook Neo, T8140) base fragment. -# -# Merge after defconfig, asahi.config and j700-radio.config, then add the device-parity fragment: -# make defconfig -# scripts/kconfig/merge_config.sh -m .config arch/arm64/configs/asahi.config arch/arm64/configs/j700-radio.config -# make olddefconfig -# scripts/kconfig/merge_config.sh -m .config arch/arm64/configs/j700.config arch/arm64/configs/j700-device-parity.config -# make olddefconfig -# The result is the configuration the J700 kernel is tested with. - -# Boot NOR (nvram) access for boot-volume switching, and the PMU RTC wake alarm -CONFIG_SPI_APPLE_QSPI=m -CONFIG_RTC_DRV_APPLE_PMU=m - -# Classic Bluetooth profiles (headsets, tethering) -CONFIG_BT_RFCOMM=m -CONFIG_BT_RFCOMM_TTY=y -CONFIG_BT_BNEP=m -CONFIG_BT_BNEP_MC_FILTER=y -CONFIG_BT_BNEP_PROTO_FILTER=y - -# Distribution firmware files are compressed -CONFIG_FW_LOADER_COMPRESS=y -CONFIG_FW_LOADER_COMPRESS_ZSTD=y - -# 48-bit virtual addresses; KASLR is untested on this machine -CONFIG_ARM64_VA_BITS_48=y -# CONFIG_RANDOMIZE_BASE is not set - -# Boot path and platform built in, so storage, keyboard, SMC and USB do not depend on the initramfs -CONFIG_BLK_DEV_NVME=y -CONFIG_NVME_APPLE=y -CONFIG_APPLE_SART=y -CONFIG_APPLE_RTKIT_HELPER=y -CONFIG_BTRFS_FS=y -CONFIG_APPLE_DOCKCHANNEL=y -CONFIG_HID_DOCKCHANNEL=y -CONFIG_HID_APPLE=y -CONFIG_HID_MAGICMOUSE=y -CONFIG_SPMI_APPLE=y -CONFIG_NVMEM_APPLE_SPMI=y -CONFIG_MFD_MACSMC=y -CONFIG_GPIO_MACSMC=y -CONFIG_POWER_RESET_MACSMC=y -CONFIG_SENSORS_MACSMC_HWMON=y -CONFIG_I2C_APPLE=y -CONFIG_PINCTRL_APPLE_GPIO=y -CONFIG_APPLE_WATCHDOG=y -CONFIG_ARM_APPLE_SOC_CPUFREQ=y -CONFIG_PHY_APPLE_ATC=y -CONFIG_USB_DWC3_APPLE=y -CONFIG_TYPEC=y -CONFIG_TYPEC_SN201202X=y -CONFIG_MUX_APPLE_DPXBAR=y -CONFIG_BACKLIGHT_CLASS_DEVICE=y -CONFIG_SYSFB_SIMPLEFB=y -CONFIG_ZRAM=y -CONFIG_UHID=y -CONFIG_RFKILL=y -CONFIG_THERMAL_NETLINK=y -CONFIG_NVME_HWMON=y - -# J700 driver choices -CONFIG_APPLE_CLPC=m -CONFIG_APPLE_PMP=y -CONFIG_DRM_ACCEL=y -CONFIG_DRM_ACCEL_ANE=m -# CONFIG_HID_APPLE_MTP_HAPTIC is not set -CONFIG_IIO=m -# CONFIG_IIO_AOP_SENSOR_LAS is not set -# CONFIG_USB4 is not set - -# Per-process 4K user pages on the 16K kernel (x86 emulation) -CONFIG_ARM64_USER4K_EXPERIMENTAL=y - -# USB Ethernet built in, for a wired link before modules load -CONFIG_USB_USBNET=y -CONFIG_USB_NET_AX8817X=y -CONFIG_USB_NET_AX88179_178A=y -CONFIG_USB_NET_CDC_NCM=y -CONFIG_USB_RTL8152=y -CONFIG_USB_UAS=m - -# Crypto that Bluetooth and Wi-Fi need early -CONFIG_CRYPTO_CCM=y -CONFIG_CRYPTO_ECDH=y -CONFIG_CRYPTO_GCM=y -CONFIG_CRYPTO_HMAC=y -CONFIG_CRYPTO_SHA256=y - -# Utilisation clamping -CONFIG_UCLAMP_TASK=y -CONFIG_UCLAMP_TASK_GROUP=y - -# LSMs named in CONFIG_LSM -CONFIG_SECURITY_LANDLOCK=y -CONFIG_SECURITY_YAMA=y - -# dma-buf heaps and udmabuf (camera and GPU buffer sharing) -CONFIG_DMABUF_HEAPS=y -CONFIG_DMABUF_HEAPS_SYSTEM=y -CONFIG_UDMABUF=y - -# Debugging aids kept in the tested configuration -CONFIG_DYNAMIC_DEBUG=y -CONFIG_FTRACE=y -CONFIG_ENABLE_DEFAULT_TRACERS=y -CONFIG_PM_DEBUG=y -CONFIG_PM_ADVANCED_DEBUG=y - -# Match the tested kernel image: no relative relocation packing, no SCM version suffix -# CONFIG_RELR is not set -# CONFIG_LOCALVERSION_AUTO is not set From f57db613d65dd826437a64f190e57135063f2080 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 14:59:51 +1100 Subject: [PATCH 162/388] media: apple: isp: copy complete IPC messages with FORTIFY enabled Acquire the validity word, copy the complete message object after the DMA read barrier and restore that captured word. Copying seven payload words through the arg1 field violates FORTIFY field bounds. Cover all eight copied words and keep the ownership ordering unchanged. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/media/platform/apple/isp/isp-ipc-test.c | 16 ++++++++++++++++ drivers/media/platform/apple/isp/isp-ipc.c | 5 +++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/drivers/media/platform/apple/isp/isp-ipc-test.c b/drivers/media/platform/apple/isp/isp-ipc-test.c index 8b6decd63f274..caa9e92f735f1 100644 --- a/drivers/media/platform/apple/isp/isp-ipc-test.c +++ b/drivers/media/platform/apple/isp/isp-ipc-test.c @@ -188,7 +188,23 @@ static void isp_test_late_ack_keeps_cursor(struct kunit *test) KUNIT_EXPECT_EQ(test, t->chan->cursor, 1); } +static void isp_test_message_payload_copy(struct kunit *test) +{ + struct isp_sm_test *t = isp_sm_test_alloc(test); + struct isp_message response = { + .arg0 = 0x5000001, .arg1 = 1, .arg2 = 2, .arg3 = 3, + .arg4 = 4, .arg5 = 5, .arg6 = 6, .arg7 = 7, + }; + struct isp_message copied = {}; + + isp_get_current_format(t->isp)->pixelformat = V4L2_PIX_FMT_P010; + t->chan->virt = &response; + chan_read_msg_index(t->isp, t->chan, &copied, 0); + KUNIT_EXPECT_MEMEQ(test, &copied, &response, sizeof(response)); +} + static struct kunit_case isp_ipc_test_cases[] = { + KUNIT_CASE(isp_test_message_payload_copy), KUNIT_CASE(isp_test_late_ack_keeps_cursor), KUNIT_CASE(isp_test_command_ring_wrap), KUNIT_CASE(isp_test_sm_free_releases), diff --git a/drivers/media/platform/apple/isp/isp-ipc.c b/drivers/media/platform/apple/isp/isp-ipc.c index 92cfc6ab35f5b..74cc717eadc07 100644 --- a/drivers/media/platform/apple/isp/isp-ipc.c +++ b/drivers/media/platform/apple/isp/isp-ipc.c @@ -72,11 +72,12 @@ static inline void chan_read_msg_index(struct apple_isp *isp, { if (isp_is_p010(isp)) { u64 *slot = chan_msg_virt(chan, index); + u64 arg0 = READ_ONCE(*slot); /* Acquire validity before consuming the firmware-owned payload. */ - msg->arg0 = READ_ONCE(*slot); dma_rmb(); - memcpy(&msg->arg1, slot + 1, sizeof(*msg) - sizeof(msg->arg0)); + memcpy(msg, slot, sizeof(*msg)); + msg->arg0 = arg0; } else { memcpy(msg, chan_msg_virt(chan, index), sizeof(*msg)); } From 1bd911c107446e95e7d4423e0f37a9afe674ebc0 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 15:03:19 +1100 Subject: [PATCH 163/388] media: apple: isp: use the contiguous single-planar capture API Remove the parameter-gated multi-planar hooks, which changed the queue type without reinitialising its cached plane semantics. Both contiguous NV12 and P010 use the existing single-planar queue. Keep format negotiation explicit through S_FMT. Assisted-by: LLM Signed-off-by: Ryan Murray --- .../media/drivers/apple-isp-j616s.rst | 13 ++- .../media/platform/apple/isp/isp-v4l2-test.c | 11 +-- drivers/media/platform/apple/isp/isp-v4l2.c | 95 +------------------ 3 files changed, 11 insertions(+), 108 deletions(-) diff --git a/Documentation/driver-api/media/drivers/apple-isp-j616s.rst b/Documentation/driver-api/media/drivers/apple-isp-j616s.rst index d5342c1d796af..bb0b9433a6119 100644 --- a/Documentation/driver-api/media/drivers/apple-isp-j616s.rst +++ b/Documentation/driver-api/media/drivers/apple-isp-j616s.rst @@ -68,9 +68,9 @@ the runtime input geometry matches the source profile. An unspecified P010 size selects 1280x720 first. Larger P010 modes require a consistent firmware output reply. P010 uses two little-endian 16-bit component planes with ten useful MSB-aligned bits, and equal 64-byte-aligned Y/UV strides. UV immediately -follows Y, as required for contiguous P010. The standard multi-planar V4L2 -API on T6040/IMX958 reports both NV12 and P010 as **one memory plane** -containing both component planes. The firmware still receives two component addresses. +follows Y, as required for contiguous P010. Both NV12 and P010 use the +standard single-planar V4L2 API with one memory plane containing both +component planes. The firmware still receives two component addresses. At 1920x1080, stride is 3840, Y is 4147200 bytes, UV is 2073600 bytes and ``sizeimage`` is 6220800. At 1280x720, stride is 2560 and ``sizeimage`` is @@ -89,10 +89,9 @@ only after the firmware report ACK and doorbell have been published. NV12 pool sizes and the T8140/Neo default formats, presets and command path retain their existing behavior. -The inherited ``multiplanar`` module parameter is removed. T6040/IMX958 -supports standard single-planar and multi-planar V4L2 negotiation for these -contiguous formats without a parameter; the non-contiguous NV12M alias is not -advertised. Other sensors retain their shipped single-planar API. No driver +The inherited ``multiplanar`` module parameter and its inconsistent queue-type +hooks are removed. All sensors use the shipped single-planar V4L2 API; the +non-contiguous NV12M alias is not advertised. No driver module/kernel parameter, platform flag or per-platform Kconfig option selects support. NV12 stays first and is the initial format because browsers/WebRTC and many applications do not support P010 or simply choose the first format. diff --git a/drivers/media/platform/apple/isp/isp-v4l2-test.c b/drivers/media/platform/apple/isp/isp-v4l2-test.c index b3c8d26dfd4aa..398e8e92356ab 100644 --- a/drivers/media/platform/apple/isp/isp-v4l2-test.c +++ b/drivers/media/platform/apple/isp/isp-v4l2-test.c @@ -37,6 +37,8 @@ static void isp_test_queue_init(struct kunit *test) KUNIT_ASSERT_EQ(test, isp_vb2_queue_init(isp), 0); KUNIT_EXPECT_GE(test, isp->vbq.max_num_buffers, VB2_MAX_FRAME); + KUNIT_EXPECT_EQ(test, isp->vbq.type, V4L2_BUF_TYPE_VIDEO_CAPTURE); + KUNIT_EXPECT_FALSE(test, isp->vbq.is_multiplanar); vb2_queue_release(&isp->vbq); } @@ -140,10 +142,6 @@ static void isp_test_p010_format(struct kunit *test) KUNIT_EXPECT_EQ(test, fmt->plane_size[0], (size_t)4147200); KUNIT_EXPECT_EQ(test, fmt->plane_size[1], (size_t)2073600); KUNIT_EXPECT_EQ(test, f.fmt.pix.quantization, V4L2_QUANTIZATION_LIM_RANGE); - isp_get_mp_pix_format(isp, &f, fmt); - KUNIT_EXPECT_EQ(test, f.fmt.pix_mp.pixelformat, V4L2_PIX_FMT_P010); - KUNIT_EXPECT_EQ(test, f.fmt.pix_mp.num_planes, 1); - KUNIT_EXPECT_EQ(test, f.fmt.pix_mp.plane_fmt[0].sizeimage, 6220800); isp->vbq.drv_priv = isp; isp_test_set_num_buffers(test, &isp->vbq, 0); @@ -164,10 +162,7 @@ static void isp_test_p010_format(struct kunit *test) KUNIT_EXPECT_EQ(test, fmt->total_size, (size_t)1382400); KUNIT_EXPECT_EQ(test, isp_num_rendered(isp), ISP_MAX_BUFFERS); KUNIT_EXPECT_EQ(test, isp_num_meta(isp), ISP_MAX_BUFFERS); - isp_get_mp_pix_format(isp, &f, fmt); - KUNIT_EXPECT_EQ(test, f.fmt.pix_mp.pixelformat, V4L2_PIX_FMT_NV12); - KUNIT_EXPECT_EQ(test, f.fmt.pix_mp.num_planes, 1); - KUNIT_EXPECT_EQ(test, f.fmt.pix_mp.plane_fmt[0].sizeimage, 1382400); + } static void isp_test_p010_admission(struct kunit *test) diff --git a/drivers/media/platform/apple/isp/isp-v4l2.c b/drivers/media/platform/apple/isp/isp-v4l2.c index bc270e06be21f..ff0133e34dee5 100644 --- a/drivers/media/platform/apple/isp/isp-v4l2.c +++ b/drivers/media/platform/apple/isp/isp-v4l2.c @@ -803,25 +803,6 @@ static inline void isp_get_sp_pix_format(struct apple_isp *isp, f->fmt.pix.quantization = V4L2_QUANTIZATION_LIM_RANGE; } -static inline void isp_get_mp_pix_format(struct apple_isp *isp, - struct v4l2_format *f, - struct isp_format *fmt) -{ - f->fmt.pix_mp.width = fmt->preset->output_dim.x; - f->fmt.pix_mp.height = fmt->preset->output_dim.y; - /* Both contiguous formats have one memory plane and two ISP components. */ - f->fmt.pix_mp.num_planes = 1; - memset(f->fmt.pix_mp.plane_fmt, 0, sizeof(f->fmt.pix_mp.plane_fmt)); - f->fmt.pix_mp.plane_fmt[0].sizeimage = fmt->total_size; - f->fmt.pix_mp.plane_fmt[0].bytesperline = fmt->strides[0]; - f->fmt.pix_mp.field = V4L2_FIELD_NONE; - f->fmt.pix_mp.pixelformat = fmt->pixelformat; - f->fmt.pix_mp.colorspace = V4L2_COLORSPACE_REC709; - f->fmt.pix_mp.ycbcr_enc = V4L2_YCBCR_ENC_709; - f->fmt.pix_mp.xfer_func = V4L2_XFER_FUNC_709; - f->fmt.pix_mp.quantization = V4L2_QUANTIZATION_LIM_RANGE; -} - static int isp_vidioc_get_format(struct file *file, void *fh, struct v4l2_format *f) { @@ -877,69 +858,6 @@ static int isp_vidioc_try_format(struct file *file, void *fh, return 0; } -static int isp_vidioc_get_format_mplane(struct file *file, void *fh, - struct v4l2_format *f) -{ - struct apple_isp *isp = video_drvdata(file); - struct isp_format *fmt = isp_get_current_format(isp); - - if (!isp_supports_p010(isp)) - return -ENOTTY; - - isp_get_mp_pix_format(isp, f, fmt); - - return 0; -} - -static int isp_vidioc_set_format_mplane(struct file *file, void *fh, - struct v4l2_format *f) -{ - struct apple_isp *isp = video_drvdata(file); - struct isp_format *fmt = isp_get_current_format(isp); - struct isp_preset *preset; - int err; - - if (!isp_supports_p010(isp)) - return -ENOTTY; - - if (vb2_is_busy(&isp->vbq)) - return -EBUSY; - - preset = isp_select_preset(isp, f->fmt.pix_mp.width, f->fmt.pix_mp.height, - f->fmt.pix_mp.pixelformat); - err = isp_set_preset(isp, fmt, preset, f->fmt.pix_mp.pixelformat); - if (err) - return err; - - isp_get_mp_pix_format(isp, f, fmt); - - isp->vbq.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; - - return 0; -} - -static int isp_vidioc_try_format_mplane(struct file *file, void *fh, - struct v4l2_format *f) -{ - struct apple_isp *isp = video_drvdata(file); - struct isp_format fmt = *isp_get_current_format(isp); - struct isp_preset *preset; - int err; - - if (!isp_supports_p010(isp)) - return -ENOTTY; - - preset = isp_select_preset(isp, f->fmt.pix_mp.width, f->fmt.pix_mp.height, - f->fmt.pix_mp.pixelformat); - err = isp_set_preset(isp, &fmt, preset, f->fmt.pix_mp.pixelformat); - if (err) - return err; - - isp_get_mp_pix_format(isp, f, &fmt); - - return 0; -} - static int isp_vidioc_enum_input(struct file *file, void *fh, struct v4l2_input *inp) { @@ -972,9 +890,7 @@ static int isp_vidioc_get_param(struct file *file, void *fh, { struct apple_isp *isp = video_drvdata(file); - if (a->type != V4L2_BUF_TYPE_VIDEO_CAPTURE && - (!isp_supports_p010(isp) || - a->type != V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE)) + if (a->type != V4L2_BUF_TYPE_VIDEO_CAPTURE) return -EINVAL; a->parm.capture.capability = V4L2_CAP_TIMEPERFRAME; @@ -990,9 +906,7 @@ static int isp_vidioc_set_param(struct file *file, void *fh, struct apple_isp *isp = video_drvdata(file); struct v4l2_fract *tpf = &a->parm.capture.timeperframe; - if (a->type != V4L2_BUF_TYPE_VIDEO_CAPTURE && - (!isp_supports_p010(isp) || - a->type != V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE)) + if (a->type != V4L2_BUF_TYPE_VIDEO_CAPTURE) return -EINVAL; /* The rate is configured when the stream starts. */ @@ -1041,9 +955,6 @@ static const struct v4l2_ioctl_ops isp_v4l2_ioctl_ops = { .vidioc_g_fmt_vid_cap = isp_vidioc_get_format, .vidioc_s_fmt_vid_cap = isp_vidioc_set_format, .vidioc_try_fmt_vid_cap = isp_vidioc_try_format, - .vidioc_g_fmt_vid_cap_mplane = isp_vidioc_get_format_mplane, - .vidioc_s_fmt_vid_cap_mplane = isp_vidioc_set_format_mplane, - .vidioc_try_fmt_vid_cap_mplane = isp_vidioc_try_format_mplane, .vidioc_enum_framesizes = isp_vidioc_enum_framesizes, .vidioc_enum_frameintervals = isp_vidioc_enum_frameintervals, @@ -1213,8 +1124,6 @@ int apple_isp_setup_video(struct apple_isp *isp) vdev->fops = &isp_v4l2_fops; vdev->ioctl_ops = &isp_v4l2_ioctl_ops; vdev->device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING; - if (isp_supports_p010(isp)) - vdev->device_caps |= V4L2_CAP_VIDEO_CAPTURE_MPLANE; vdev->entity.flags = MEDIA_ENT_FL_DEFAULT; vdev->v4l2_dev = &isp->v4l2_dev; vdev->vfl_type = VFL_TYPE_VIDEO; From 0ad03dc7fb76a31cd5aa11d7fc695ba4c17d98ff Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 15:08:26 +1100 Subject: [PATCH 164/388] drm/asahi: keep G15 compatible with the shared G16 interfaces Building G15 and G16 together exposes stale initializers and calls in the G15 adapter. Initialise the inactive G16 platform owner and preserve the RTKit default protocol negotiation and legacy shmem callback. Resolve timestamp handles through Buffer::firmware_mapping(), retaining the firmware mapping and rejecting CPU-only destinations. Pass no reserved region map when walking G15 host-allocated RAM page tables, matching the same shared API contract already used by G16. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/asahi/driver.rs | 2 ++ drivers/gpu/drm/asahi/g15/boot.rs | 2 ++ drivers/gpu/drm/asahi/g15/command.rs | 1 + drivers/gpu/drm/asahi/g15/uat.rs | 2 +- 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/asahi/driver.rs b/drivers/gpu/drm/asahi/driver.rs index addabd04fe701..ab68573881659 100644 --- a/drivers/gpu/drm/asahi/driver.rs +++ b/drivers/gpu/drm/asahi/driver.rs @@ -346,6 +346,8 @@ impl AsahiPlatformDriver { _device: reg.device().into(), _reg: reg, g17: None, + #[cfg(CONFIG_DRM_ASAHI_G16)] + g16: None, raw_dev: core::ptr::null_mut(), raw_stats: core::ptr::null_mut(), stats: None, diff --git a/drivers/gpu/drm/asahi/g15/boot.rs b/drivers/gpu/drm/asahi/g15/boot.rs index ddcf5efef4c3d..7a06aa64ffbc3 100644 --- a/drivers/gpu/drm/asahi/g15/boot.rs +++ b/drivers/gpu/drm/asahi/g15/boot.rs @@ -306,10 +306,12 @@ unsafe extern "C" fn rtkit_shmem_destroy( /// The GPU's RTKit callbacks. static RTKIT_OPS: bindings::apple_rtkit_ops = bindings::apple_rtkit_ops { flags: 0, + protocol_version: 0, crashed: Some(rtkit_crashed), recv_message: Some(rtkit_recv), recv_message_early: Some(rtkit_recv_early), shmem_setup: Some(rtkit_shmem_setup), + shmem_setup_ep: None, shmem_destroy: Some(rtkit_shmem_destroy), syslog_is_debug: None, }; diff --git a/drivers/gpu/drm/asahi/g15/command.rs b/drivers/gpu/drm/asahi/g15/command.rs index 45b65d1ddc026..6fc1e03fb637c 100644 --- a/drivers/gpu/drm/asahi/g15/command.rs +++ b/drivers/gpu/drm/asahi/g15/command.rs @@ -361,6 +361,7 @@ impl Timestamps<'_> { .ok_or(ENOENT)? .clone(); let file::Object::TimestampBuffer(mapping) = object; + let mapping = mapping.firmware_mapping()?; let offset = timestamp.offset as usize; if offset % 8 != 0 || offset >= mapping.size() || mapping.size() - offset < 8 { return Err(EINVAL); diff --git a/drivers/gpu/drm/asahi/g15/uat.rs b/drivers/gpu/drm/asahi/g15/uat.rs index 854ff34e729a3..d6971489ee80f 100644 --- a/drivers/gpu/drm/asahi/g15/uat.rs +++ b/drivers/gpu/drm/asahi/g15/uat.rs @@ -96,7 +96,7 @@ fn with_table( // SAFETY: Every table walked here is a page the shared page-table code allocated in RAM // (`Page::alloc_page`) and never frees while the firmware runs (tables are not freed on // unmap, and these VMs are retained until reboot). - unsafe { pgtable::with_root_entries(pa, count, f) } + unsafe { pgtable::with_root_entries(None, pa, count, f) } } /// Cleans `len` bytes of the RAM table page at `pa`, from entry `first`, to the point of From a2b3221b38cd211d4dc9e67487575e2897ad9e31 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 15:08:26 +1100 Subject: [PATCH 165/388] drm/asahi: keep G15 compatible with the shared G16 interfaces Building G15 and G16 together exposes stale initializers and calls in the G15 adapter. Initialise the inactive G16 platform owner and preserve the RTKit default protocol negotiation and legacy shmem callback. Resolve timestamp handles through Buffer::firmware_mapping(), retaining the firmware mapping and rejecting CPU-only destinations. Pass no reserved region map when walking G15 host-allocated RAM page tables, matching the same shared API contract already used by G16. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/asahi/driver.rs | 2 ++ drivers/gpu/drm/asahi/g15/boot.rs | 2 ++ drivers/gpu/drm/asahi/g15/command.rs | 1 + drivers/gpu/drm/asahi/g15/uat.rs | 2 +- 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/asahi/driver.rs b/drivers/gpu/drm/asahi/driver.rs index addabd04fe701..ab68573881659 100644 --- a/drivers/gpu/drm/asahi/driver.rs +++ b/drivers/gpu/drm/asahi/driver.rs @@ -346,6 +346,8 @@ impl AsahiPlatformDriver { _device: reg.device().into(), _reg: reg, g17: None, + #[cfg(CONFIG_DRM_ASAHI_G16)] + g16: None, raw_dev: core::ptr::null_mut(), raw_stats: core::ptr::null_mut(), stats: None, diff --git a/drivers/gpu/drm/asahi/g15/boot.rs b/drivers/gpu/drm/asahi/g15/boot.rs index ddcf5efef4c3d..7a06aa64ffbc3 100644 --- a/drivers/gpu/drm/asahi/g15/boot.rs +++ b/drivers/gpu/drm/asahi/g15/boot.rs @@ -306,10 +306,12 @@ unsafe extern "C" fn rtkit_shmem_destroy( /// The GPU's RTKit callbacks. static RTKIT_OPS: bindings::apple_rtkit_ops = bindings::apple_rtkit_ops { flags: 0, + protocol_version: 0, crashed: Some(rtkit_crashed), recv_message: Some(rtkit_recv), recv_message_early: Some(rtkit_recv_early), shmem_setup: Some(rtkit_shmem_setup), + shmem_setup_ep: None, shmem_destroy: Some(rtkit_shmem_destroy), syslog_is_debug: None, }; diff --git a/drivers/gpu/drm/asahi/g15/command.rs b/drivers/gpu/drm/asahi/g15/command.rs index 45b65d1ddc026..6fc1e03fb637c 100644 --- a/drivers/gpu/drm/asahi/g15/command.rs +++ b/drivers/gpu/drm/asahi/g15/command.rs @@ -361,6 +361,7 @@ impl Timestamps<'_> { .ok_or(ENOENT)? .clone(); let file::Object::TimestampBuffer(mapping) = object; + let mapping = mapping.firmware_mapping()?; let offset = timestamp.offset as usize; if offset % 8 != 0 || offset >= mapping.size() || mapping.size() - offset < 8 { return Err(EINVAL); diff --git a/drivers/gpu/drm/asahi/g15/uat.rs b/drivers/gpu/drm/asahi/g15/uat.rs index 854ff34e729a3..d6971489ee80f 100644 --- a/drivers/gpu/drm/asahi/g15/uat.rs +++ b/drivers/gpu/drm/asahi/g15/uat.rs @@ -96,7 +96,7 @@ fn with_table( // SAFETY: Every table walked here is a page the shared page-table code allocated in RAM // (`Page::alloc_page`) and never frees while the firmware runs (tables are not freed on // unmap, and these VMs are retained until reboot). - unsafe { pgtable::with_root_entries(pa, count, f) } + unsafe { pgtable::with_root_entries(None, pa, count, f) } } /// Cleans `len` bytes of the RAM table page at `pa`, from entry `first`, to the point of From 5de99fc6f682f64fb9e22617063bb38158c455b1 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 25 Oct 2026 21:51:41 -0400 Subject: [PATCH 166/388] drm/asahi: keep withholding G17 compute doorbells while a stamp is awaited A queue whose retirement stops at a command with a completion record but no covering stamp yet sets retire_pending, which counted as leaving ordinary retirement: the next publication rang its doorbell at once, and a withheld doorbell was released whatever its predecessors. That happens after most completions. Each doorbell makes the firmware read the work ring, and a hop between queues whose producer ends 30-80 us after one is slowed by 12-24 us. The withheld doorbell already counts only predecessors without a completion record, which covers a front waiting for its stamp. Count retire_pending as settling only when the front has no record, the case of a command the firmware finished without one. In a two-queue semaphore ping-pong (gpubench sync.signal, 0.6 s traced), doorbells rung at publication fall from 2006 to 96 and releases forced by settling from 1280 to 23; all doorbells fall from 8035 to 6707. --- drivers/gpu/drm/asahi/g17/queue/compute.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/queue/compute.rs b/drivers/gpu/drm/asahi/g17/queue/compute.rs index 9c4c5c92d049d..9ad6f0507898d 100644 --- a/drivers/gpu/drm/asahi/g17/queue/compute.rs +++ b/drivers/gpu/drm/asahi/g17/queue/compute.rs @@ -628,7 +628,10 @@ impl Queue { } /// The queue has left ordinary retirement: a timeout, kill, quarantine, replay, recovery - /// witness or exit is in progress. + /// witness or exit is in progress, or retirement stopped at a command the firmware + /// finished without a completion record. A front whose record has arrived and that only + /// waits for its stamp is ordinary retirement: [`Self::due_doorbell`] counts it as + /// finished, and ringing at once there rang most withheld doorbells early. fn settling(&self) -> bool { self.quarantined || self.released @@ -636,7 +639,8 @@ impl Queue { || self.replays.len != 0 || self.replay_retirement || self.awaiting_witness - || self.retire_pending + || (self.retire_pending + && self.active.front().is_none_or(|active| active.observed.is_none())) || self.retired_by_teardown || self.spared_quarantine || self.failure_status_pending From 83ff3a9040e6c06ade92f690b39419d783289128 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 25 Oct 2026 21:51:58 -0400 Subject: [PATCH 167/388] drm/asahi: restart a withheld G17 compute doorbell another doorbell covered A doorbell announces every work slot written on its ring, so a withheld doorbell of one queue is often covered by a doorbell another queue rings. Publications of the first queue after that kept the withheld doorbell's first publication, whose predecessors had long retired: the doorbell was due at once and rang for one or two new slots. In a two-queue semaphore ping-pong almost half of all releases announced a single slot. When a publication finds that another doorbell announced every slot the withheld one covered, it starts the withheld doorbell over from itself, so the doorbell waits for this command's own predecessors. Measured on Neo with this and the previous change against the base in the same boot, alternating, three rounds (us, medians): sync.signal ping-pong doorbells 7409 -> 4625 hops slowed by more than 7 us 19.6% -> 13.9% sync.signal timeline, per hop 23.40 -> 22.83 sync.signal binary, per hop 23.35 -> 22.67 compute.light queue d64 13.89 -> 13.11 The median hop is unchanged; compute.light queue d1/d2/d3/d4, queues d1/d2/d4 and render.light d1/d32 stay within 1%. --- drivers/gpu/drm/asahi/g17/queue/compute.rs | 16 ++++++++++++++-- drivers/gpu/drm/asahi/g17/runtime.rs | 6 ++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/queue/compute.rs b/drivers/gpu/drm/asahi/g17/queue/compute.rs index 9ad6f0507898d..b4edec4a7b6c0 100644 --- a/drivers/gpu/drm/asahi/g17/queue/compute.rs +++ b/drivers/gpu/drm/asahi/g17/queue/compute.rs @@ -594,8 +594,9 @@ pub(crate) struct Queue { source_readers: Arc, source_hint: crate::g17::sync_input::ReaderHint, /// The doorbell withheld from publications that had [`WITHHOLD_DEPTH`] - 1 or more - /// unretired commands of this queue ahead of them, the publication of the first, and the - /// compute work slots written at its priority once its slot was. + /// unretired commands of this queue ahead of them, the publication of the first of them + /// that no doorbell has announced, and the compute work slots written at its priority once + /// the last one's slot was. withheld_doorbell: Option<(u64, u64, u64)>, } @@ -1956,6 +1957,8 @@ pub(crate) trait Host { -> Result; fn notify_compute(&mut self, priority: u8, message: u64) -> Result; fn compute_written(&self, priority: u8) -> u64; + /// Compute work slots at `priority` that a doorbell has announced. + fn compute_announced(&self, priority: u8) -> u64; fn note_submission(&mut self) -> Result; } @@ -2307,6 +2310,15 @@ impl Queue { && self.withheld_doorbell.is_none_or(|(withheld, _, _)| withheld == doorbell) { let written = host.compute_written(policy.priority()); + // Once another doorbell announced every slot the withheld one covered, it + // starts over from this publication: it then waits for this command's own + // predecessors rather than for those of slots already announced. + if self + .withheld_doorbell + .is_some_and(|(_, _, last)| host.compute_announced(policy.priority()) >= last) + { + self.withheld_doorbell = None; + } let entry = self.withheld_doorbell.get_or_insert((doorbell, published, written)); entry.2 = written; } else { diff --git a/drivers/gpu/drm/asahi/g17/runtime.rs b/drivers/gpu/drm/asahi/g17/runtime.rs index e04295d576606..bf9399cd87e73 100644 --- a/drivers/gpu/drm/asahi/g17/runtime.rs +++ b/drivers/gpu/drm/asahi/g17/runtime.rs @@ -1396,6 +1396,9 @@ impl compute::Host for ComputeHost<'_> { fn compute_written(&self, priority: u8) -> u64 { self.port.compute_written(priority) } + fn compute_announced(&self, priority: u8) -> u64 { + self.port.compute_announced(priority) + } fn note_submission(&mut self) -> Result { self.effort.submit(); super::Firmware::apply_shared_effort(self.init, self.effort, self.port, super::power::FULL_EFFORT) @@ -1443,6 +1446,9 @@ impl compute::Host for DirectHost<'_> { fn compute_written(&self, priority: u8) -> u64 { self.port.compute_written(priority) } + fn compute_announced(&self, priority: u8) -> u64 { + self.port.compute_announced(priority) + } fn note_submission(&mut self) -> Result { Ok(()) } From 7fc12d0823e95d8e8ecd44e504f1f6a59a4689a6 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 15:51:14 +1100 Subject: [PATCH 168/388] docs: media: audit the public J616s capture evidence and AOP map Credit Ryan\x27s m1n1 PR #20, https://github.com/aurora-silicon/m1n1/pull/20, at 9be380c8a5a94609fdbe62d693c7b2121478ad8f for the processed preview protocol. Record what each requested file establishes, the diagnostic RAW snapshot limitation and the missing per-config timing records. Distinguish the separate AOP audio sequence from ISP camera configuration. Assisted-by: LLM Signed-off-by: Ryan Murray --- .../media/drivers/apple-isp-j616s.rst | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/Documentation/driver-api/media/drivers/apple-isp-j616s.rst b/Documentation/driver-api/media/drivers/apple-isp-j616s.rst index bb0b9433a6119..650a109cc7ad1 100644 --- a/Documentation/driver-api/media/drivers/apple-isp-j616s.rst +++ b/Documentation/driver-api/media/drivers/apple-isp-j616s.rst @@ -41,6 +41,59 @@ firmware-derived output modes still need capture validation on 26.6.2. Only our source and synthetic protocol tests are included; no Apple binary, setfile contents, calibration table or firmware image is added. +Public source audit +------------------- + +The pinned PR and its discussion were checked again for the RAW layout and +per-configuration timing. The reusable code establishes the processed path; +it does not contain a RAW Bayer packing/CFA contract or the 13 reply records: + +* ``isp_capture.py`` accepts only 1280x720 and 1920x1080 processed geometry. + Its generic pool-3 builder describes a single address, not the pixel packing, + CFA order or RAW receiver command sequence. The qualified preview uses pool 9. +* ``isp_pixels.py`` contains only an MSB-aligned P010 decoder, with no Bayer decoder. +* ``isp_sensor.py`` reads the sensor ID/count and queries an index at runtime. + It has neither saved per-index replies nor a frame-rate decoder. +* ``isp_profile.py`` and ``fixtures/isp_t6040_25g76.json`` describe boot segments, + allocation geometry and channels. The fixture has no camera-info or config replies. +* ``isp_vid.py`` and ``experiments/isp.py`` use the older fixed 720p NV12 receiver. + ``isp_cmd.py`` includes legacy sensor-0x248 examples and a temporary 3.75 FPS + AE request (0x3c0), alongside a comment describing the older 30 FPS request. + These are not IMX958 per-mode timing records. The qualified T6040 helper instead + requests 30/15 FPS AE limits; neither set supplies a measured nominal-rate table. +* ``isp_command.py`` implements ownership, command/reply and deadline handling. + ``src/isp.c`` prepares boot-time power and heap handoff, not a RAW capture client. +* ``docs/j616s-bringup.md`` lines 752-789 explicitly distinguish a stable RAW + snapshot after stop from a completed buffer report and say orchestration remains + private. Commit ``743e4e75`` adds that diagnostic RAW evidence as documentation; + it does not add a RAW receiver implementation. Lines 832-841 qualify one + completed 1080p P010 frame, with measured FPS still unqualified. + +These findings do not deny the recorded RAW snapshot. They mean a V4L2 Bayer +FOURCC, frame-completion contract and per-config rates cannot be derived from +these files without additional runtime replies or capture evidence. In particular, +the old 0x103 SIF example must not be treated as an IMX958 Bayer-format declaration. + +AOP endpoints and separate audio stream +-------------------------------------- + +The modern ``fw/aop/client.py`` map in PR 20 is 0x20 SPUApp, 0x21 wakehint, +0x22 audio, 0x23 voice trigger, 0x24 accelerometer, 0x25 gyroscope, 0x26 lid +angle, 0x27 ALS and four endpoints at 0x28-0x2b with unqualified roles. The +separate ENDPOINTS13 map assigns different roles and is not the T6040 map. + +The documented HQ audio sequence uses the advertised aop-audio service at +endpoint 0x22, EPIC version 2, checked attachment/property replies and owned +DART mappings. It first establishes LP readiness using adpx buffer property 300, +attachments lpai/pdm0/adpx, lpai pw1, lai attachment and lai/lpai runn. HQ then +attaches hpai, requests pw1, prepares stream-10 NS RX0 DMA and starts DMA before +requesting hpai pwrd. Matching reports and two full copies precede RX stop and +return through pw1 to idle. These are audio commands, not camera mode selectors. + +The qualified ISP preview helper contains no AOP command. This camera change +therefore does not replay the AOP audio stream or alter its Linux driver. The +ISP channel and command maps below describe the camera path independently. + Mode and format selection ------------------------- From 2114ed0c6245126cd90a2e5e96e2cddd4e0697e0 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 15:51:35 +1100 Subject: [PATCH 169/388] docs: media: fix AOP section heading width Correct the reStructuredText underline and verify the complete document with warnings treated as errors. Source protocol credit: Ryan Murray, m1n1 PR #20, https://github.com/aurora-silicon/m1n1/pull/20, at 9be380c8a5a94609fdbe62d693c7b2121478ad8f. Assisted-by: LLM Signed-off-by: Ryan Murray --- Documentation/driver-api/media/drivers/apple-isp-j616s.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/driver-api/media/drivers/apple-isp-j616s.rst b/Documentation/driver-api/media/drivers/apple-isp-j616s.rst index 650a109cc7ad1..9a2bf93d7b059 100644 --- a/Documentation/driver-api/media/drivers/apple-isp-j616s.rst +++ b/Documentation/driver-api/media/drivers/apple-isp-j616s.rst @@ -75,7 +75,7 @@ these files without additional runtime replies or capture evidence. In particula the old 0x103 SIF example must not be treated as an IMX958 Bayer-format declaration. AOP endpoints and separate audio stream --------------------------------------- +----------------------------------------- The modern ``fw/aop/client.py`` map in PR 20 is 0x20 SPUApp, 0x21 wakehint, 0x22 audio, 0x23 voice trigger, 0x24 accelerometer, 0x25 gyroscope, 0x26 lid From c9648e3322d53a3b546b8b2e037fd0cb50184b27 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 15:50:54 +1100 Subject: [PATCH 170/388] arm64: configs: drop helpers selected by the shared Apple drivers The shared drivers already select keys, AOP, the MAX98357A codec, Rust firmware loading, the Apple mailbox and C RTKit support. Removing these six requests cumulatively changes no resolved symbol over either ARM64 defconfig or the Fedora Asahi base, with WERROR enabled. Keep the drivers and their selections as the source of those dependencies. Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/configs/asahi.config | 6 ------ 1 file changed, 6 deletions(-) diff --git a/arch/arm64/configs/asahi.config b/arch/arm64/configs/asahi.config index 590df15222c8b..59d8572309847 100644 --- a/arch/arm64/configs/asahi.config +++ b/arch/arm64/configs/asahi.config @@ -48,7 +48,6 @@ CONFIG_SND_SOC_APPLE_T8140_AOP_AUDIO=m CONFIG_SND_SOC_APPLE_MACAUDIO=m CONFIG_SND_SOC_CS42L83=m CONFIG_SND_SOC_CS42L84=m -CONFIG_SND_SOC_MAX98357A=m CONFIG_SND_SOC_TAS2764=m CONFIG_SND_SOC_TAS2770=m CONFIG_HID_APPLE=m @@ -68,12 +67,8 @@ CONFIG_APPLE_DART_J713_DCP=y CONFIG_APPLE_DOCKCHANNEL=m CONFIG_SERIAL_APPLE_DOCKCHANNEL=y CONFIG_SERIAL_APPLE_DOCKCHANNEL_EARLYCON=y -CONFIG_APPLE_MAILBOX=y CONFIG_APPLE_PMGR_MISC=y -CONFIG_APPLE_RTKIT=y CONFIG_APPLE_RTKIT_HELPER=m -CONFIG_APPLE_AOP=m -CONFIG_KEYS=y CONFIG_TRUSTED_KEYS=y # CONFIG_TRUSTED_KEYS_TPM is not set # CONFIG_TRUSTED_KEYS_TEE is not set @@ -84,7 +79,6 @@ CONFIG_APPLE_PMP_THERMAL=m CONFIG_APPLE_T8132_PLATFORM_HANDOFF=y CONFIG_IIO_AOP_SENSOR_LAS=m CONFIG_IIO_AOP_SENSOR_ALS=m -CONFIG_RUST_FW_LOADER_ABSTRACTIONS=y CONFIG_PHY_APPLE_ATC=m CONFIG_PHY_APPLE_T6040_USB2=m CONFIG_PHY_APPLE_DPTX=m From fca758c8146d25047628146f49da8f900fc1f2bd Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Mon, 26 Oct 2026 02:13:26 -0400 Subject: [PATCH 171/388] soc: apple: clpc: give the GPU floor its own states The GPU floor used the top entry of each rail's CPU table, so raising the fabric for GPU work also raised it for a CPU cluster in its top frequency quartile. On J700 that costs 0.21 W during a single-thread load (6.738 -> 6.948 W), and no CPU benefit has been measured. Give each SoC a GPU target separate from its CPU table. T8140 keeps DCS 4 and fabric 3 for streaming GPU work, and its CPU table no longer touches the fabric, as before the GPU floor. T8132's GPU target is its CPU table's top entry, so it is unchanged. --- drivers/soc/apple/clpc.c | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/drivers/soc/apple/clpc.c b/drivers/soc/apple/clpc.c index c2f45979dc485..5f59c72ee2b64 100644 --- a/drivers/soc/apple/clpc.c +++ b/drivers/soc/apple/clpc.c @@ -60,7 +60,8 @@ enum apple_clpc_rail { * 0 or 1 for the first * @map: state of each rail for a busy cluster below 25, 50, 75 and 100 * percent of its maximum frequency - * @gpu_demand: streaming GPU work requests both rails' top states + * @gpu_demand: streaming GPU work requests @gpu + * @gpu: state of each rail for streaming GPU work * @pmp_backoff: release that GPU floor while the PMP, voter agent * @pmp_agent, limits the DCS, see apple_clpc_gpu_limited() */ @@ -69,6 +70,7 @@ struct apple_clpc_hw { unsigned int sustain; u8 map[APPLE_CLPC_RAILS][APPLE_CLPC_LEVELS]; bool gpu_demand; + u8 gpu[APPLE_CLPC_RAILS]; bool pmp_backoff; unsigned int pmp_agent; }; @@ -290,7 +292,7 @@ static bool apple_clpc_gpu_limited(struct apple_clpc *clpc, unsigned long now) dcs = (apple_pmc_floor_read(clpc->pmc, clpc->hw->pmp_agent) >> APPLE_PMC_RAIL_SHIFT(rail)) & 0xf; - if (dcs < clpc->hw->map[APPLE_CLPC_DCS][APPLE_CLPC_LEVELS - 1]) { + if (dcs < clpc->hw->gpu[APPLE_CLPC_DCS]) { clpc->gpu_limited = true; clpc->gpu_limited_at = now; } @@ -323,7 +325,7 @@ static void apple_clpc_sample(struct apple_clpc *clpc) if (clpc->gpu_pmgr && apple_clpc_gpu_streaming(clpc, now) && !apple_clpc_gpu_limited(clpc, now)) for (rail = 0; rail < APPLE_CLPC_RAILS; rail++) - demand[rail] = clpc->hw->map[rail][APPLE_CLPC_LEVELS - 1]; + demand[rail] = max(demand[rail], clpc->hw->gpu[rail]); for (rail = 0; rail < APPLE_CLPC_RAILS; rail++) state[rail] = apple_clpc_vote(&clpc->vote[rail], demand[rail], @@ -544,12 +546,13 @@ static const struct apple_clpc_hw apple_clpc_hw_t8132 = { [APPLE_CLPC_SOC] = { 3, 3, 3, 3 }, }, .gpu_demand = true, + .gpu = { [APPLE_CLPC_DCS] = 5, [APPLE_CLPC_SOC] = 3 }, }; /* * T8140: DCS states 0-4 on rail 0, fabric states 0-3 on rail 1. A busy - * cluster raises the DCS one state per frequency quartile; only a cluster - * at its top quartile also raises the fabric. + * cluster raises the DCS one state per frequency quartile and leaves the + * fabric alone. * * GPU work needs both rails at their top states: the GPU firmware takes up * and completes every command over the fabric. With the fabric left at @@ -570,9 +573,10 @@ static const struct apple_clpc_hw apple_clpc_hw_t8140 = { .rail = { [APPLE_CLPC_DCS] = 0, [APPLE_CLPC_SOC] = 1 }, .map = { [APPLE_CLPC_DCS] = { 1, 2, 3, 4 }, - [APPLE_CLPC_SOC] = { 0, 0, 0, 3 }, + [APPLE_CLPC_SOC] = { 0, 0, 0, 0 }, }, .gpu_demand = true, + .gpu = { [APPLE_CLPC_DCS] = 4, [APPLE_CLPC_SOC] = 3 }, .pmp_backoff = true, .pmp_agent = 0, }; From 6f9ffbe2ff67fff6ac7c79477130a2f2e3dd546a Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Mon, 26 Oct 2026 02:13:54 -0400 Subject: [PATCH 172/388] soc: apple: clpc: limit the GPU streaming gate to T8140 The 64 ms streaming gate and the PMP back-off were measured on T8140 but sat on the shared graphics path, so they also changed T8132. That SoC had no DT consumer yet, but it would have lost its floor for bursts under 64 ms, against what was qualified there. Make the time graphics must stay powered a per-SoC parameter: 64 ms on T8140, and 0 on T8132, which keeps that SoC's floor whenever graphics is powered, as before. The PMP back-off was already T8140 only. The debugfs status now says "never" when the PMP has not limited the DCS, rather than 0 ms ago. The comment no longer says a failed read cannot starve a graphics consumer: a failed read counts as powered, so read errors alone do not end a stream that is already under way. --- drivers/soc/apple/clpc.c | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/drivers/soc/apple/clpc.c b/drivers/soc/apple/clpc.c index 5f59c72ee2b64..4ef32f023756a 100644 --- a/drivers/soc/apple/clpc.c +++ b/drivers/soc/apple/clpc.c @@ -48,8 +48,6 @@ enum apple_clpc_rail { #define APPLE_CLPC_BUSY_PCT 50 /* Time a rail keeps its last request after demand stops */ #define APPLE_CLPC_HOLD_MS 32 -/* Time graphics stays powered without a break before it counts as GPU demand */ -#define APPLE_CLPC_GPU_SUSTAIN_MS 64 /* Time the GPU floor stays released after the PMP last limited the DCS */ #define APPLE_CLPC_GPU_BACKOFF_MS 1000 @@ -62,6 +60,8 @@ enum apple_clpc_rail { * percent of its maximum frequency * @gpu_demand: streaming GPU work requests @gpu * @gpu: state of each rail for streaming GPU work + * @gpu_sustain_ms: time graphics stays powered without a break before it + * counts as streaming, see apple_clpc_gpu_streaming(); 0 counts it at once * @pmp_backoff: release that GPU floor while the PMP, voter agent * @pmp_agent, limits the DCS, see apple_clpc_gpu_limited() */ @@ -71,6 +71,7 @@ struct apple_clpc_hw { u8 map[APPLE_CLPC_RAILS][APPLE_CLPC_LEVELS]; bool gpu_demand; u8 gpu[APPLE_CLPC_RAILS]; + unsigned int gpu_sustain_ms; bool pmp_backoff; unsigned int pmp_agent; }; @@ -246,13 +247,13 @@ static void apple_clpc_queue(struct apple_clpc *clpc, bool idle) } /* - * Whether the GPU streams work: graphics has stayed powered for - * APPLE_CLPC_GPU_SUSTAIN_MS, with no sample in between that found it off and - * no gap in sampling long enough to have missed that. The firmware powers - * graphics off a few milliseconds after the GPU idles, so a frame-paced load - * such as video playback powers it up and down with each frame and does not - * count. A failed read counts as powered, so that it cannot starve a running - * graphics consumer. + * Whether the GPU streams work: graphics has stayed powered for the SoC's + * gpu_sustain_ms, with no sample in between that found it off and no gap in + * sampling long enough to have missed that. The firmware powers graphics off + * a few milliseconds after the GPU idles, so a frame-paced load such as video + * playback powers it up and down with each frame and does not count. A failed + * read counts as powered, so read errors alone do not end a stream that is + * already under way. */ static bool apple_clpc_gpu_streaming(struct apple_clpc *clpc, unsigned long now) { @@ -272,7 +273,7 @@ static bool apple_clpc_gpu_streaming(struct apple_clpc *clpc, unsigned long now) clpc->gpu_on_since = now; clpc->gpu_on = true; return time_after_eq(now, clpc->gpu_on_since + - msecs_to_jiffies(APPLE_CLPC_GPU_SUSTAIN_MS)); + msecs_to_jiffies(clpc->hw->gpu_sustain_ms)); } /* @@ -369,12 +370,15 @@ static int apple_clpc_status_show(struct seq_file *s, void *unused) clpc->hw->map[rail][2], clpc->hw->map[rail][3]); seq_printf(s, "samples: %lu update errors: %lu\n", clpc->samples, clpc->update_errors); - if (clpc->gpu_pmgr) - seq_printf(s, "gpu: powered %u for %u ms, pmp limited %u ms ago\n", - clpc->gpu_on, - clpc->gpu_on ? jiffies_to_msecs(jiffies - clpc->gpu_on_since) : 0, - clpc->gpu_limited ? - jiffies_to_msecs(jiffies - clpc->gpu_limited_at) : 0); + if (clpc->gpu_pmgr) { + seq_printf(s, "gpu: powered %u for %u ms, pmp limited ", clpc->gpu_on, + clpc->gpu_on ? jiffies_to_msecs(jiffies - clpc->gpu_on_since) : 0); + if (clpc->gpu_limited) + seq_printf(s, "%u ms ago\n", + jiffies_to_msecs(jiffies - clpc->gpu_limited_at)); + else + seq_puts(s, "never\n"); + } for (i = 0; i < clpc->nr_clusters; i++) seq_printf(s, "cluster cpu%u: busy %u%% level %u%% -> dcs %u soc %u\n", clpc->clusters[i].policy_cpu, clpc->clusters[i].busy, @@ -559,7 +563,7 @@ static const struct apple_clpc_hw apple_clpc_hw_t8132 = { * state 0, light GPU submissions take 30-50% longer than at state 3, and * macOS holds the fabric at its top states while such work streams. The * firmware powers graphics off a few milliseconds after the GPU idles, so - * graphics that stays powered for GPU_SUSTAIN_MS marks such a stream. Loads + * graphics that stays powered for 64 ms marks such a stream. Loads * paced by frames power it up and down with each frame instead: on J700, * 1080p video playback and light browsing keep it powered for at most 63 ms * at a time, and leave the floor down. The floor is released HOLD_MS after @@ -577,6 +581,7 @@ static const struct apple_clpc_hw apple_clpc_hw_t8140 = { }, .gpu_demand = true, .gpu = { [APPLE_CLPC_DCS] = 4, [APPLE_CLPC_SOC] = 3 }, + .gpu_sustain_ms = 64, .pmp_backoff = true, .pmp_agent = 0, }; From 800efe4fd30ec7d4f5f9e2ca92e74cd46ed2e8ed Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Mon, 26 Oct 2026 02:12:15 -0400 Subject: [PATCH 173/388] cpufreq: apple: keep T8140 policy->cur in step with the cluster The fast die-temperature limit moves a lazy_verify cluster without a scheduler request. It lowers a request above a new limit, and it restores the scheduler's request when the limit rises again before the QoS request was updated. policy->cur kept the value the last fast-switch request returned. If that was a capped frequency equal to the suspend frequency, cpufreq_generic_suspend() skipped the driver after the limit rose, and the cluster suspended at the restored, higher state. The suspend step now records the state it settles the cluster in as policy->cur before the generic suspend, so the suspend frequency is written whenever the cluster is elsewhere. When the limit moves the cluster, policy->cur is updated too, for scaling_cur_freq and the statistics, until the next fast-switch request sets it. Checked on T8140 with a diagnostic build. The limit was set to the suspend frequency under load, then restored before the QoS request changed, and policy->cur was left at the suspend frequency. A pm_test=devices suspend then left the cluster at 3336 MHz, because the generic suspend skipped the driver. With this change, policy->cur followed the restored state, and the suspend frequency was written and verified. --- drivers/cpufreq/apple-soc-cpufreq.c | 58 +++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 11 deletions(-) diff --git a/drivers/cpufreq/apple-soc-cpufreq.c b/drivers/cpufreq/apple-soc-cpufreq.c index bc3e2402ced2a..85e592faa0538 100644 --- a/drivers/cpufreq/apple-soc-cpufreq.c +++ b/drivers/cpufreq/apple-soc-cpufreq.c @@ -728,12 +728,15 @@ static unsigned int apple_soc_cpufreq_lazy_request(struct cpufreq_policy *policy * * Waited-for requests own the cluster from apple_soc_cpufreq_drain() to the * next fast-switch request, and the core applies the limit to them. + * + * Returns the P-state the cluster was moved to, or -1. */ -static void apple_soc_cpufreq_lazy_cap(struct apple_cpu_priv *priv, - unsigned int cap, unsigned int freq) +static int apple_soc_cpufreq_lazy_cap(struct apple_cpu_priv *priv, + unsigned int cap, unsigned int freq) { unsigned int requested, pstate; unsigned long flags; + int moved = -1; raw_spin_lock_irqsave(&priv->lock, flags); priv->cap_pstate = cap; @@ -741,9 +744,11 @@ static void apple_soc_cpufreq_lazy_cap(struct apple_cpu_priv *priv, requested = priv->pending_pstate >= 0 ? priv->pending_pstate : priv->expected_pstate; pstate = min(priv->asked_pstate, cap); - if (!priv->waited && pstate != requested) - apple_soc_cpufreq_lazy_set(priv, pstate); + if (!priv->waited && pstate != requested && + !apple_soc_cpufreq_lazy_set(priv, pstate)) + moved = pstate; raw_spin_unlock_irqrestore(&priv->lock, flags); + return moved; } /* @@ -793,11 +798,26 @@ static int apple_soc_cpufreq_set_target(struct cpufreq_policy *policy, return apple_soc_cpufreq_request(policy, index); } +/* Returns the frequency-table frequency of @pstate, or 0. */ +static unsigned int apple_soc_cpufreq_pstate_freq(struct cpufreq_policy *policy, + unsigned int pstate) +{ + struct cpufreq_frequency_table *pos; + + cpufreq_for_each_valid_entry(pos, policy->freq_table) + if (pos->driver_data == pstate) + return pos->frequency; + return 0; +} + /* - * policy->cur holds the last fast-switch request, which on a lazy_verify SoC - * may still be pending, or lost to a failure that stopped requests. The core - * skips a suspend frequency equal to policy->cur, so settle the cluster - * first: write and verify the pending request, or report the failure. + * On a lazy_verify SoC policy->cur is the last fast-switch request, which the + * cluster may not hold: it may still be pending, be lost to a failure that + * stopped requests, or have been moved since by a fast die-temperature limit, + * see apple_soc_cpufreq_lazy_cap(). The core skips a suspend frequency equal + * to policy->cur, so settle the cluster first, writing and verifying the + * pending request or reporting the failure, and record the state it settled + * in as policy->cur before the generic suspend. */ static int apple_soc_cpufreq_suspend(struct cpufreq_policy *policy) { @@ -805,12 +825,17 @@ static int apple_soc_cpufreq_suspend(struct cpufreq_policy *policy) if (priv->info->lazy_verify) { int pstate = apple_soc_cpufreq_drain(priv); + unsigned int freq; int ret; - ret = apple_soc_cpufreq_set_pstate(priv, pstate >= 0 ? pstate : - priv->expected_pstate); + if (pstate < 0) + pstate = priv->expected_pstate; + ret = apple_soc_cpufreq_set_pstate(priv, pstate); if (ret) return ret; + freq = apple_soc_cpufreq_pstate_freq(policy, pstate); + if (freq) + policy->cur = freq; } return cpufreq_generic_suspend(policy); @@ -934,6 +959,7 @@ static void apple_fast_die_cap(struct apple_fast_die *fd, unsigned int limit) struct apple_cpu_priv *priv = policy->driver_data; struct cpufreq_frequency_table *pos; unsigned int cap = 0, freq = 0; + int moved; if (!priv->info->lazy_verify) return; @@ -945,8 +971,18 @@ static void apple_fast_die_cap(struct apple_fast_die *fd, unsigned int limit) } } /* Limits are frequency-table entries, see apple_fast_die_fit(). */ + if (!freq) + return; + /* + * For scaling_cur_freq and the statistics until the next fast-switch + * request, which sets policy->cur itself. + */ + moved = apple_soc_cpufreq_lazy_cap(priv, cap, freq); + if (moved < 0) + return; + freq = apple_soc_cpufreq_pstate_freq(policy, moved); if (freq) - apple_soc_cpufreq_lazy_cap(priv, cap, freq); + WRITE_ONCE(policy->cur, freq); } /* Take a sample and queue an update if the limit changes. Call with @lock held. */ From 22e984f1e1b166b7ef2a45a3442f53d70f414bfc Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 17:49:55 +1100 Subject: [PATCH 174/388] selftests: apple-dcp: keep WIP host fixtures portable Carry the existing integration errno and GNU make selection fixes onto the WIP consistency port. Extracted PM helpers use the host errno header, the synthetic EREMOTEIO status has a portable definition, and Kconfig producer checks honour MAKE or find gmake. The initial WIP Darwin run exposed these fixture-only failures; no driver or test timeout changes. Based on the public integration/aurora fixes at 9cbdd0734d9e. Assisted-by: LLM --- tools/testing/selftests/drivers/gpu/apple-dcp/common.py | 2 ++ tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py | 4 ++++ tools/testing/selftests/drivers/gpu/apple-dcp/pm.py | 5 +++-- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/common.py b/tools/testing/selftests/drivers/gpu/apple-dcp/common.py index cdf20256dfa4f..918a9d91fa30f 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/common.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/common.py @@ -47,6 +47,8 @@ def source_defines(source): def tipd_pm_header(root): """CD321x PM state machine for host units, with a host clamp() for minmax.h.""" header = (root / 'drivers/usb/typec/tipd/cd321x-pm.h').read_text() + # Use the host errno namespace, not the AFK Linux-numbered shim. + header = header.replace('#include ', '#include ') # Host integer types; a repeated identical typedef is valid C11. header = header.replace('#include ', '#include \ntypedef uint32_t u32;') diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py index d7485a6681747..21a962fb10cb3 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py @@ -1770,6 +1770,10 @@ def power_reply(root): #include #include #include +/* Darwin has no EREMOTEIO; this synthetic status is never passed to libc. */ +#ifndef EREMOTEIO +#define EREMOTEIO 121 +#endif typedef uint32_t u32; typedef uint64_t u64; #define WRITE_ONCE(p,v) ((p)=(v)) #define container_of(p,t,m) ((t *)((char *)(p)-offsetof(t,m))) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py b/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py index af3a590b375a4..664ba046dad35 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py @@ -2,7 +2,7 @@ # Copyright 2026 Ryan Murray """Actual PM/HCD/mux/probe boundaries using the shared PHY extraction fixture.""" from pathlib import Path -import os, re, subprocess +import os, re, shlex, shutil, subprocess import phy from common import block, tipd_pm_header, atc_function @@ -681,7 +681,8 @@ def kconfig(root, output): 'config PHY_APPLE_ATC\n\ttristate "PHY"\n\t' + dependency + '\n') compiler = Path(os.environ.get('KCONFIG_CONF', root / 'scripts/kconfig/conf')) if not compiler.is_file(): - subprocess.run(['make', '-C', str(root), 'O=' + str(directory / 'producer'), + make = shlex.split(os.environ.get('MAKE', shutil.which('gmake') or 'make')) + subprocess.run(make + ['-C', str(root), 'O=' + str(directory / 'producer'), 'ARCH=arm64', 'LLVM=1', 'allnoconfig'], check=True) compiler = directory / 'producer/scripts/kconfig/conf' for usb, phy_setting in [('n', 'y'), ('y', 'y'), ('m', 'y'), ('m', 'm')]: From c01ad1ba4f1a558fe281dd123423032d7aa54172 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 17:23:00 +1100 Subject: [PATCH 175/388] drm/asahi: g17: build test-only helpers only with built-in KUnit Four G17 helpers exist only for the KUnit suites: the compute QID floor, the growth gate and admission pool constructors and the runtime registry constructor. They are gated on cfg(CONFIG_KUNIT), which is also set when KUnit is a module. #[kunit_tests] compiles its suites only for CONFIG_KUNIT=y, so with CONFIG_KUNIT=m (the Fedora-based release configuration) nothing uses the helpers and rustc warns about dead code: warning: constant `COMPUTE_FLOOR` is never used warning: associated function `new` is never used (x3) With CONFIG_WERROR=y, KBUILD_RUSTFLAGS gains -Dwarnings and the asahi module no longer builds. Gate the helpers on CONFIG_KUNIT="y", the condition the suites use. Every caller is inside a #[kunit_tests] module, so built-in KUnit runs are unchanged. Fixes: a36e352d8564 ("drm/asahi: size the G17 compute QID budget by open logical queues") Fixes: 014605a36795 ("drm/asahi: add G17 resource wait domains and use them for queue creation") Fixes: 64f717dcfd1e ("drm/asahi: wait for G17 compute capacity on the release that frees it") Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/asahi/g17/kick.rs | 2 +- drivers/gpu/drm/asahi/g17/preparation.rs | 2 +- drivers/gpu/drm/asahi/g17/queue/admission.rs | 2 +- drivers/gpu/drm/asahi/g17/runtime.rs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/kick.rs b/drivers/gpu/drm/asahi/g17/kick.rs index a1f8f70f597d4..4adf2c01ac4d5 100644 --- a/drivers/gpu/drm/asahi/g17/kick.rs +++ b/drivers/gpu/drm/asahi/g17/kick.rs @@ -43,7 +43,7 @@ const RENDER_HEADROOM: usize = 24; /// The render reserve with every execution context open. const FULL_RENDER_RESERVE: usize = render_reserve(crate::mmu::MAX_EXECUTION_CONTEXTS as usize); /// Compute channels that stay reachable with every execution context open. -#[cfg(CONFIG_KUNIT)] +#[cfg(CONFIG_KUNIT = "y")] pub(super) const COMPUTE_FLOOR: usize = QID_COUNT - FULL_RENDER_RESERVE; /// Upper bound on distinct physical compute channels named by one kick. pub(super) const COMPUTE_CHANNELS: usize = QID_COUNT; diff --git a/drivers/gpu/drm/asahi/g17/preparation.rs b/drivers/gpu/drm/asahi/g17/preparation.rs index eb2366fa3451b..2aa3213115076 100644 --- a/drivers/gpu/drm/asahi/g17/preparation.rs +++ b/drivers/gpu/drm/asahi/g17/preparation.rs @@ -60,7 +60,7 @@ pub(super) struct Gate { } impl Gate { - #[cfg(CONFIG_KUNIT)] + #[cfg(CONFIG_KUNIT = "y")] pub(super) fn new() -> Result> { Self::with_waiters(None) } diff --git a/drivers/gpu/drm/asahi/g17/queue/admission.rs b/drivers/gpu/drm/asahi/g17/queue/admission.rs index e1e7552c243f2..5d770ca1f64b0 100644 --- a/drivers/gpu/drm/asahi/g17/queue/admission.rs +++ b/drivers/gpu/drm/asahi/g17/queue/admission.rs @@ -406,7 +406,7 @@ pub(crate) struct Pool { renders: Option>, } impl Pool { - #[cfg(CONFIG_KUNIT)] + #[cfg(CONFIG_KUNIT = "y")] pub(crate) fn new(max_queues: u32) -> Result> { Self::with_waiters(max_queues, None, None) } diff --git a/drivers/gpu/drm/asahi/g17/runtime.rs b/drivers/gpu/drm/asahi/g17/runtime.rs index e04295d576606..9fe17db8a2809 100644 --- a/drivers/gpu/drm/asahi/g17/runtime.rs +++ b/drivers/gpu/drm/asahi/g17/runtime.rs @@ -186,7 +186,7 @@ pub(super) struct Registry { impl Registry { /// Initialize inside the firmware allocation instead of staging the whole /// registry on the probe stack. No additional allocation is required. - #[cfg(CONFIG_KUNIT)] + #[cfg(CONFIG_KUNIT = "y")] pub(super) fn new( clusters: u32, deflake: crate::g17::fw::render::Deflake, From 91c4792269c9c2bb3f5815e625ae2e843ec1ae96 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 17:58:30 +1100 Subject: [PATCH 176/388] drm/asahi: gate G17 KUnit helpers on built-in KUnit The kunit_tests macro compiles its test modules only when CONFIG_KUNIT is y. With CONFIG_KUNIT=m the four test-only G17 helpers remain compiled and produce dead-code warnings, breaking a CONFIG_WERROR build. Use the same built-in KUnit condition for the helpers as for their callers. Fixes: a36e352d8564 ("drm/asahi: size the G17 compute QID budget by open logical queues") Fixes: 014605a36795 ("drm/asahi: add G17 resource wait domains and use them for queue creation") Fixes: 64f717dcfd1e ("drm/asahi: wait for G17 compute capacity on the release that frees it") Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/asahi/g17/kick.rs | 2 +- drivers/gpu/drm/asahi/g17/preparation.rs | 2 +- drivers/gpu/drm/asahi/g17/queue/admission.rs | 2 +- drivers/gpu/drm/asahi/g17/runtime.rs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/kick.rs b/drivers/gpu/drm/asahi/g17/kick.rs index a1f8f70f597d4..4adf2c01ac4d5 100644 --- a/drivers/gpu/drm/asahi/g17/kick.rs +++ b/drivers/gpu/drm/asahi/g17/kick.rs @@ -43,7 +43,7 @@ const RENDER_HEADROOM: usize = 24; /// The render reserve with every execution context open. const FULL_RENDER_RESERVE: usize = render_reserve(crate::mmu::MAX_EXECUTION_CONTEXTS as usize); /// Compute channels that stay reachable with every execution context open. -#[cfg(CONFIG_KUNIT)] +#[cfg(CONFIG_KUNIT = "y")] pub(super) const COMPUTE_FLOOR: usize = QID_COUNT - FULL_RENDER_RESERVE; /// Upper bound on distinct physical compute channels named by one kick. pub(super) const COMPUTE_CHANNELS: usize = QID_COUNT; diff --git a/drivers/gpu/drm/asahi/g17/preparation.rs b/drivers/gpu/drm/asahi/g17/preparation.rs index eb2366fa3451b..2aa3213115076 100644 --- a/drivers/gpu/drm/asahi/g17/preparation.rs +++ b/drivers/gpu/drm/asahi/g17/preparation.rs @@ -60,7 +60,7 @@ pub(super) struct Gate { } impl Gate { - #[cfg(CONFIG_KUNIT)] + #[cfg(CONFIG_KUNIT = "y")] pub(super) fn new() -> Result> { Self::with_waiters(None) } diff --git a/drivers/gpu/drm/asahi/g17/queue/admission.rs b/drivers/gpu/drm/asahi/g17/queue/admission.rs index e1e7552c243f2..5d770ca1f64b0 100644 --- a/drivers/gpu/drm/asahi/g17/queue/admission.rs +++ b/drivers/gpu/drm/asahi/g17/queue/admission.rs @@ -406,7 +406,7 @@ pub(crate) struct Pool { renders: Option>, } impl Pool { - #[cfg(CONFIG_KUNIT)] + #[cfg(CONFIG_KUNIT = "y")] pub(crate) fn new(max_queues: u32) -> Result> { Self::with_waiters(max_queues, None, None) } diff --git a/drivers/gpu/drm/asahi/g17/runtime.rs b/drivers/gpu/drm/asahi/g17/runtime.rs index e04295d576606..9fe17db8a2809 100644 --- a/drivers/gpu/drm/asahi/g17/runtime.rs +++ b/drivers/gpu/drm/asahi/g17/runtime.rs @@ -186,7 +186,7 @@ pub(super) struct Registry { impl Registry { /// Initialize inside the firmware allocation instead of staging the whole /// registry on the probe stack. No additional allocation is required. - #[cfg(CONFIG_KUNIT)] + #[cfg(CONFIG_KUNIT = "y")] pub(super) fn new( clusters: u32, deflake: crate::g17::fw::render::Deflake, From d9e7c8dbf9a28e34c61f9f7faf509d75d2adf9fe Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 18:09:02 +1100 Subject: [PATCH 177/388] arm64: configs: inherit the energy model from both bases The universal and Fedora bases already enable ENERGY_MODEL. Drop the redundant fragment request, retaining the power allocator governor and default. Both complete integration resolutions have zero delta from this removal. Assisted-by: LLM --- arch/arm64/configs/asahi.config | 1 - 1 file changed, 1 deletion(-) diff --git a/arch/arm64/configs/asahi.config b/arch/arm64/configs/asahi.config index 59d8572309847..1ca03ba6bdbdc 100644 --- a/arch/arm64/configs/asahi.config +++ b/arch/arm64/configs/asahi.config @@ -8,7 +8,6 @@ CONFIG_ARM64_VA_BITS_48=y CONFIG_ARM64_USER4K_EXPERIMENTAL=y CONFIG_ARM64_MEMORY_MODEL_CONTROL=y CONFIG_ARM_APPLE_CPUIDLE=y -CONFIG_ENERGY_MODEL=y CONFIG_THERMAL_GOV_POWER_ALLOCATOR=y CONFIG_THERMAL_DEFAULT_GOV_POWER_ALLOCATOR=y CONFIG_BT_BRCMEXT=y From f769da00ca04eaa9128f787b8c37734dd755d9bd Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Mon, 26 Oct 2026 03:13:25 -0400 Subject: [PATCH 178/388] drm/asahi: skip G17 event-pass housekeeping for completion records alone Every completion notification queues an event pass, and every pass ran the full housekeeping after retiring the records: render linger expiry, drained-pair reclaim, compute exit settlement, compute pool recycling and vacating, evicted render pool vacating, heap shrinking and teardown. Each of these scans every compute queue, render entry or pool, so with around 40000 passes a second in light rows the scans took a large part of the event worker's time on the IRQ CPU, while retiring a completion and signalling its fence needs none of them. Run that housekeeping only when a pass is requested through queue_events(), on recovery, while a teardown is pending, while anything waits for channels, progress, queue slots or render capacity, when no work remains in flight, and at the latest on the first pass 1 ms after the last full one. The firmware's event notification, records that arrive during a pass and render events missed while a pair was on loan queue a pass for the records alone. Every other caller keeps getting a full pass. Measured on Neo in the same boot against every pass running the housekeeping, alternating, four rounds (medians): event pass, sync.signal + render.light procs 8.2 -> 6.2 us event worker share of CPU0, all rows 24.9% -> 20.8% compute.light procs d2 36.80 -> 34.81 us compute.light queues d2 36.76 -> 34.96 us submit.empty queue d2 1.78 -> 1.69 us compute.light queue d1/d2/d64, queues d4, render.light d1/d32 and queues d4/d8, render.light procs d2, sync.signal binary/timeline, concurrent.cr and the cross-queue pair stay within 2%. --- drivers/gpu/drm/asahi/g17/mod.rs | 21 ++++++- drivers/gpu/drm/asahi/g17/runtime/events.rs | 61 +++++++++++++------ .../drm/asahi/g17/runtime/render/direct.rs | 3 +- .../drm/asahi/g17/runtime/render/observe.rs | 3 +- 4 files changed, 66 insertions(+), 22 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/mod.rs b/drivers/gpu/drm/asahi/g17/mod.rs index c63d5a3258314..5d43f4734b53f 100644 --- a/drivers/gpu/drm/asahi/g17/mod.rs +++ b/drivers/gpu/drm/asahi/g17/mod.rs @@ -65,6 +65,7 @@ use core::ops::Range; use core::sync::atomic::{ AtomicBool, AtomicU32, + AtomicU64, Ordering, // }; @@ -369,7 +370,7 @@ impl rtkit::Operations for RoleOps { if !is_events_message(ep, msg) { return false; } - data.shared.queue_events(); + data.shared.queue_event_records(); true } @@ -857,6 +858,11 @@ struct Shared { compute_demand: AtomicU32, #[pin] work: Work, + /// The next event pass runs every housekeeping step: set by every [`Self::queue_events`] + /// request, not by the firmware's event notification ([`Self::queue_event_records`]). + housekeeping: AtomicBool, + /// When an event pass last ran every housekeeping step, in ns. + housekeeping_ns: AtomicU64, #[pin] poll: DelayedWork, #[pin] @@ -989,8 +995,17 @@ impl Shared { self.port.store(core::ptr::null_mut(), Ordering::Release); } - /// Queues the event worker. Callable from any context. + /// Queues the event worker for a pass that runs every housekeeping step. Callable from any + /// context. fn queue_events(self: &Arc) { + self.housekeeping.store(true, Ordering::Release); + self.queue_event_records(); + } + + /// Queues the event worker for firmware records alone: completions, or records that arrived + /// during a pass. The pass skips the housekeeping that nothing needs (see + /// `runtime_event_turn`). Callable from any context. + fn queue_event_records(self: &Arc) { // Already queued if this fails, which is just as good. let _ = workqueue::system_highpri().enqueue::, EVENT_WORK>(self.clone()); } @@ -1170,6 +1185,8 @@ impl Gpu { renders, compute_demand: AtomicU32::new(0), work <- new_work!("g17::Shared::work"), + housekeeping: AtomicBool::new(true), + housekeeping_ns: AtomicU64::new(0), poll <- new_delayed_work!("g17::Shared::poll"), idle <- new_delayed_work!("g17::Shared::idle"), grow <- new_work!("g17::Shared::grow"), diff --git a/drivers/gpu/drm/asahi/g17/runtime/events.rs b/drivers/gpu/drm/asahi/g17/runtime/events.rs index 4cf2dd0fafb97..09a62c3549d52 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/events.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/events.rs @@ -15,6 +15,9 @@ use kernel::{prelude::*, sync::Arc}; /// Wake interval while a tiler heap may still shrink: the native check period. const SHRINK_WAKE_NS: u64 = 1_000_000_000; +/// Longest interval between two event passes that run every housekeeping step while +/// completion records alone keep the worker busy. +const HOUSEKEEPING_NS: u64 = 1_000_000; struct Service<'a> { firmware: &'a mut Firmware, @@ -378,6 +381,10 @@ impl crate::g17::Shared { } fn runtime_event_turn(self: &Arc, deferred: &mut DeferredBatch) -> Result { + // Taken before consuming records: a request made during this pass gets the next one. + let requested = self + .housekeeping + .swap(false, core::sync::atomic::Ordering::AcqRel); let (recover, masks) = { let mut state = self.state.lock(); let Some(firmware) = (*state).as_deref_mut() else { @@ -438,24 +445,42 @@ impl crate::g17::Shared { .any(|queue| queue.in_flight()); let in_flight = compute || firmware.queues.render_in_flight(); let idle = firmware.idle_check(now, in_flight)?; - let lingers = if firmware.recovery.pending() { - None - } else { - firmware.expire_render_lingers(now)? - }; - firmware.reclaim_drained_renders(deferred)?; - firmware.service_compute_exit(deferred)?; - let pools = firmware.service_compute_pools(deferred, now)?; + // Expiries, drained pairs, exits, pool and heap trimming and teardown give back what + // retirement freed; a completion needs none of them to retire or to signal. While work + // is in flight and nothing waits for capacity, a pass for firmware records alone skips + // them. Any requested pass, recovery, teardown, a waiter, the last retirement and, at + // the latest, the first pass after HOUSEKEEPING_NS run them all. + let housekeeping = requested + || recover + || !in_flight + || firmware.recovery.pending() + || !firmware.queues.teardown.is_empty() + || self.channels.has_waiters() + || self.progress.has_waiters() + || self.queues.has_waiters() + || self.renders.has_waiters() + || now.saturating_sub(self.housekeeping_ns.load(core::sync::atomic::Ordering::Relaxed)) + >= HOUSEKEEPING_NS; + let mut lingers = None; + let mut pools = None; let mut shrinking = false; - if !firmware.recovery.pending() { - firmware - .queues - .vacate_evicted_render_pools(&firmware.init, deferred)?; - shrinking = firmware - .queues - .shrink_render_heaps(&firmware.init, now, deferred); - } - { + if housekeeping { + self.housekeeping_ns + .store(now, core::sync::atomic::Ordering::Relaxed); + if !firmware.recovery.pending() { + lingers = firmware.expire_render_lingers(now)?; + } + firmware.reclaim_drained_renders(deferred)?; + firmware.service_compute_exit(deferred)?; + pools = firmware.service_compute_pools(deferred, now)?; + if !firmware.recovery.pending() { + firmware + .queues + .vacate_evicted_render_pools(&firmware.init, deferred)?; + shrinking = firmware + .queues + .shrink_render_heaps(&firmware.init, now, deferred); + } let mut reclaim = self.reclaim.lock(); firmware.service_teardown(&mut reclaim)?; if !reclaim.is_empty() { @@ -507,7 +532,7 @@ impl crate::g17::Shared { { // A full bounded pass must not depend on a fresh notification for // records that arrived while it was consuming the same ring. - self.queue_events(); + self.queue_event_records(); } let grow_ready = firmware.render_grow_ready(); drop(state); diff --git a/drivers/gpu/drm/asahi/g17/runtime/render/direct.rs b/drivers/gpu/drm/asahi/g17/runtime/render/direct.rs index 1f42d1e6f404f..36eae4816f967 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render/direct.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render/direct.rs @@ -220,7 +220,8 @@ impl crate::g17::Firmware { ) -> Result { self.queues.render.return_pair(slot, &mut loan.pair)?; if self.queues.render.take_missed_event(slot) { - self.primary.state.shared.queue_events(); + // The record is retired by the next pass's observation, like any completion. + self.primary.state.shared.queue_event_records(); } let entry = self.queues.render.entry_mut(slot)?; // A failure or timeout recorded against the lent pair is settled by the diff --git a/drivers/gpu/drm/asahi/g17/runtime/render/observe.rs b/drivers/gpu/drm/asahi/g17/runtime/render/observe.rs index 52bdd964ca4ae..5ae76639f6521 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render/observe.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render/observe.rs @@ -166,7 +166,8 @@ impl crate::g17::Shared { let firmware = (*state).as_deref_mut().ok_or(EIO)?; self.note_changed(); if firmware.render_grow_pending() || firmware.queues.render.take_missed_event(slot as u8) { - self.queue_events(); + // Observation and the growth check run on every pass. + self.queue_event_records(); } if !epoch.is_current() || firmware.recovery.pending() From e7843a83510dfb1c3c7fd0a3462379c98decc7c5 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 18:19:49 +1100 Subject: [PATCH 179/388] rust: device: drop the stale private-parent lint expectation Device::parent is a public API in the carried Asahi abstractions, and AOP service lookup uses it with AUXILIARY_BUS disabled. The inherited expectation described the upstream crate-private helper and becomes unfulfilled in the built-in Asahi/firmware KUnit recipe. WERROR correctly rejects it. Remove the obsolete dead_code expectation from the public method. Keep warning enforcement and the minimal test config; do not enable an unrelated bus to hide the warning. Production behavior is unchanged. Assisted-by: LLM --- rust/kernel/device.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/rust/kernel/device.rs b/rust/kernel/device.rs index e54bf419c7a66..f047b90e7b04a 100644 --- a/rust/kernel/device.rs +++ b/rust/kernel/device.rs @@ -268,7 +268,6 @@ impl Device { } /// Returns a reference to the parent device, if any. - #[cfg_attr(not(CONFIG_AUXILIARY_BUS), expect(dead_code))] pub fn parent(&self) -> Option<&Device> { // SAFETY: // - By the type invariant `self.as_raw()` is always valid. From 27060370b2013a1f673cbf9aabd61d5934809c1f Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 18:19:49 +1100 Subject: [PATCH 180/388] rust: device: drop the stale private-parent lint expectation Device::parent is a public API in the carried Asahi abstractions, and AOP service lookup uses it with AUXILIARY_BUS disabled. The inherited expectation described the upstream crate-private helper and becomes unfulfilled in the built-in Asahi/firmware KUnit recipe. WERROR correctly rejects it. Remove the obsolete dead_code expectation from the public method. Keep warning enforcement and the minimal test config; do not enable an unrelated bus to hide the warning. Production behavior is unchanged. Assisted-by: LLM (cherry picked from commit e7843a83510dfb1c3c7fd0a3462379c98decc7c5) --- rust/kernel/device.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/rust/kernel/device.rs b/rust/kernel/device.rs index e54bf419c7a66..f047b90e7b04a 100644 --- a/rust/kernel/device.rs +++ b/rust/kernel/device.rs @@ -268,7 +268,6 @@ impl Device { } /// Returns a reference to the parent device, if any. - #[cfg_attr(not(CONFIG_AUXILIARY_BUS), expect(dead_code))] pub fn parent(&self) -> Option<&Device> { // SAFETY: // - By the type invariant `self.as_raw()` is always valid. From 7b563df1ba87e6f22d54be1186dce7024eab5934 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 20:06:25 +1100 Subject: [PATCH 181/388] rust: require checked binding layouts for every kernel build With SCHEDSTATS disabled, stock bindgen loses the alignment of the empty sched_statistics member and shifts task_struct.mm. A current-MM read can then pass a scheduler-list pointer to the RSS helpers. Keep unconditional size, alignment and offset checks in production builds and document the corrected generator and command-line BINDGEN selection. --- Documentation/rust/bindgen-layout-checks.rst | 57 ++++++++++---------- rust/Makefile | 15 ++---- tools/aurora-bindgen/README.rst | 20 +++---- tools/aurora-bindgen/build.py | 2 +- 4 files changed, 43 insertions(+), 51 deletions(-) diff --git a/Documentation/rust/bindgen-layout-checks.rst b/Documentation/rust/bindgen-layout-checks.rst index aaac5b24fb742..5af9951a64338 100644 --- a/Documentation/rust/bindgen-layout-checks.rst +++ b/Documentation/rust/bindgen-layout-checks.rst @@ -3,22 +3,21 @@ Checking generated binding layouts ================================== -bindgen can emit compile-time assertions that compare the size, alignment and -field offsets of every generated Rust type with the values Clang computes for -the C type. As upstream, the kernel build passes ``--no-layout-tests`` to -bindgen, so the generated bindings do not contain these assertions and any -supported bindgen version can build the kernel. - -To check the layouts of a configuration, build with ``BINDGEN_LAYOUT_TESTS=1``:: - - make LLVM=1 BINDGEN_LAYOUT_TESTS=1 prepare - -The bindings are then generated with the assertions, which are checked while -the ``bindings`` and ``uapi`` crates are compiled; nothing is executed. A -mismatch stops the build with an ``E0080`` error naming the type and the -property (``Size of``, ``Alignment of`` or ``Offset of``). Changing the -variable regenerates the bindings. The check covers only the configuration -being built. +Every Rust build retains bindgen's unconditional compile-time assertions for +size, alignment and field offsets. A mismatch stops the bindings crate build +with an ``E0080`` error naming the C type and property. These assertions are +required even when KUnit is disabled; ``BINDGEN_LAYOUT_TESTS=0`` cannot disable +them. Build the complete Image and all modules with the same generator. + +A successful C build does not validate Rust field offsets. In particular, +``CONFIG_SCHEDSTATS=n`` leaves a zero-sized, cacheline-aligned +``sched_statistics`` in ``task_struct``. Stock bindgen 0.73.2 loses its alignment +and can shift subsequent fields, including ``mm``. The ARM64 defconfig + +Asahi configuration exposed a 40-byte difference: Rust read a scheduler list +pointer as the current mm and Asahi's RSS charge faulted in +``percpu_counter_add_batch``. The C RSS helpers and allocator agreed on the +counter layout; the pointer supplied to them was wrong. Enabling SCHEDSTATS +only masks the generator defect and is not a requirement of the driver. Known generator limitations --------------------------- @@ -37,16 +36,11 @@ Unmodified bindgen 0.73 does not reproduce every C layout used by the kernel: * It can drop the explicit alignment of a defined zero-sized C struct, such as ``struct empty {} __attribute__((aligned(64)));``. -With the assertions enabled and stock bindgen 0.73.2, an arm64 Apple -silicon configuration of this tree fails on the size of ``ns_common`` and of -the namespace structures that embed it; bindgen 0.73.1 also fails on -``task_struct``, ``zone`` and ``pglist_data``. In those builds every offset -and alignment assertion passes: bindgen pads each field to the offset Clang -reports, so only the end of the Rust type is short. Rust code that reaches -such a type through a pointer and reads its fields gets the right data, but -must not rely on ``size_of`` for it, copy it by value or index an array of -it. The affected types depend on the bindgen version and the configuration, -which is what the check is for. +The earlier Fedora configuration exposed only trailing size differences for +some namespace types. That observation does not extend to defconfig: an +aligned empty member can also shift field offsets. Neither size nor offset +mismatches are safe to ignore. The affected types depend on the generator +version and the resolved kernel configuration, so validate each build. Corrected generator ------------------- @@ -56,7 +50,7 @@ repair both cases, a private build recipe and compile-only tests; see ``tools/aurora-bindgen/README.rst``. It installs nothing system-wide. To check the layouts with it:: - make LLVM=1 BINDGEN=/path/to/bindgen BINDGEN_LAYOUT_TESTS=1 ... + make LLVM=1 BINDGEN=/path/to/bindgen ... The generated bindings are consumed by the Rust kernel crate and by several drivers. After changing the bindings generator, rebuild the kernel Image and @@ -64,5 +58,10 @@ all modules together. The assertions need bindgen 0.71.1 or later (the kernel minimum) together with the configured ``--rust-target 1.85``: that selects unconditional constant -assertions rather than ``#[test]`` functions, so a build with -``BINDGEN_LAYOUT_TESTS=1`` checks them without a Rust test configuration. +assertions rather than ``#[test]`` functions. They are checked without a +Rust test configuration. + +Pass ``BINDGEN=/absolute/path/to/bindgen`` as a make command-line argument. +Exporting it in the environment is insufficient: the top-level Makefile's +``BINDGEN = bindgen`` assignment takes precedence over an environment value. +Check the recorded bindings command, not just the toolchain version banner. diff --git a/rust/Makefile b/rust/Makefile index f9b22e936fa40..b5ba0c3176bce 100644 --- a/rust/Makefile +++ b/rust/Makefile @@ -513,23 +513,16 @@ endif # architecture instead of generating `usize`. bindgen_c_flags_final = $(bindgen_c_flags_lto) -fno-builtin -D__BINDGEN__ -# Layout assertions are off by default, as upstream: stock bindgen 0.73 -# computes a different total size for a few kernel types, so the bindings -# would not build with it. `make BINDGEN_LAYOUT_TESTS=1` keeps them, for -# checking a bindings generator; see -# Documentation/rust/bindgen-layout-checks.rst. -ifeq ($(BINDGEN_LAYOUT_TESTS),1) -bindgen_layout_flags := -else -bindgen_layout_flags := --no-layout-tests -endif +# Keep bindgen's unconditional layout assertions in every build. An empty, +# cacheline-aligned sched_statistics (CONFIG_SCHEDSTATS=n) exposes a generator +# bug that shifts task_struct.mm, so even pointer field access can be unsafe. +# See Documentation/rust/bindgen-layout-checks.rst for the corrected generator. # `--rust-target` points to our minimum supported Rust version. quiet_cmd_bindgen = BINDGEN $@ cmd_bindgen = \ $(BINDGEN) $< $(bindgen_target_flags) --rust-target 1.85 \ --use-core --with-derive-default --ctypes-prefix ffi \ - $(bindgen_layout_flags) \ --no-debug '.*' --enable-function-attribute-detection \ -o $@ -- $(bindgen_c_flags_final) -DMODULE \ $(bindgen_target_cflags) $(bindgen_target_extra) diff --git a/tools/aurora-bindgen/README.rst b/tools/aurora-bindgen/README.rst index 75d4c3a2480e7..35e4b6eb8a274 100644 --- a/tools/aurora-bindgen/README.rst +++ b/tools/aurora-bindgen/README.rst @@ -25,10 +25,10 @@ locked dependency graph is used, with at most two build jobs. ``--archive`` can supply an already downloaded source archive; its digest is still checked. The script builds and runs compile-only valid-input checks, then prints the exact ``BINDGEN=/absolute/path/to/bindgen`` make argument. It installs nothing globally. -The kernel build passes ``--no-layout-tests`` to bindgen unless -``BINDGEN_LAYOUT_TESTS=1`` is given, so add that variable to check layouts -with the private tool, and build the complete kernel Image and matching -modules with the same tool. +The kernel always retains layout assertions. Pass the printed ``BINDGEN=`` +argument on the make command line; an exported environment variable is +overridden by the top-level Makefile. Build the complete kernel Image and +matching modules with the same tool. Both scripts reject optimized Python (``-O``, ``-OO`` or ``PYTHONOPTIMIZE``) before creating output, since their validation assertions must remain enabled. @@ -70,14 +70,14 @@ generator, and its changes and both tool digests belong in the build record. Repository guard and supported tool versions -------------------------------------------- -By default the kernel passes ``--no-layout-tests``, as upstream, so stock -bindgen builds it; ``make BINDGEN_LAYOUT_TESTS=1`` keeps the layout checks -(see ``Documentation/rust/bindgen-layout-checks.rst``). Its minimum bindgen is +Every kernel build keeps the layout checks (see +``Documentation/rust/bindgen-layout-checks.rst``). Stock bindgen can fail these +checks for valid configurations; use the corrected generator rather than +disabling the assertions or changing configuration to hide a layout bug. Its minimum bindgen is 0.71.1, and its fixed ``--rust-target 1.85`` enables the ``offset_of`` feature (available from Rust 1.77). In both official 0.71.1 and the pinned 0.73.2 generator, that feature selects unconditional const size/alignment/offset -assertions rather than ``#[test]`` functions. A bindings crate compile with -``BINDGEN_LAYOUT_TESTS=1`` therefore checks layouts without requiring a Rust +assertions rather than ``#[test]`` functions. A bindings crate compile therefore checks layouts without requiring a Rust unit-test configuration. This is source-backed for the accepted minimum and pinned tool; future generator changes must retain that capability. @@ -87,6 +87,6 @@ Primary sources: * https://github.com/rust-lang/rust-bindgen/blob/v0.71.1/bindgen/codegen/mod.rs * https://github.com/rust-lang/rust-bindgen/tree/v0.73.2 -A complete kernel build with ``BINDGEN_LAYOUT_TESTS=1`` remains the +A complete kernel build remains the configuration-specific layout check; these small generator fixtures do not replace it or establish runtime GPU stability. diff --git a/tools/aurora-bindgen/build.py b/tools/aurora-bindgen/build.py index 7aa222cc028bb..fa87873098480 100644 --- a/tools/aurora-bindgen/build.py +++ b/tools/aurora-bindgen/build.py @@ -22,4 +22,4 @@ cmd=['cargo','build','--locked','--release','-p','bindgen-cli','-j'+str(a.jobs)] with (O/'build.log').open('w') as log:subprocess.run(cmd,cwd=S,stdout=log,stderr=subprocess.STDOUT,check=True) binary=S/'target/release/bindgen';subprocess.run([sys.executable,str(P/'test.py'),'--bindgen',str(binary),'--output-dir',str(O/'tests')],check=True) -r={'status':'BUILT_AND_COMPILE_TESTED','binary':str(binary),'binary_sha256':sha(binary),'source_manifest_sha256':sha(P/'sources.json'),'build_command':cmd,'rustc_version':subprocess.check_output(['rustc','-vV'],text=True),'cargo_version':subprocess.check_output(['cargo','-vV'],text=True),'tests_manifest_sha256':sha(O/'tests/result.json'),'installed_globally':False};(O/'result.json').write_text(json.dumps(r,indent=2)+'\n');print(json.dumps(r,indent=2));print('Use make BINDGEN='+str(binary)+' BINDGEN_LAYOUT_TESTS=1 ... to build the kernel with layout assertions.') +r={'status':'BUILT_AND_COMPILE_TESTED','binary':str(binary),'binary_sha256':sha(binary),'source_manifest_sha256':sha(P/'sources.json'),'build_command':cmd,'rustc_version':subprocess.check_output(['rustc','-vV'],text=True),'cargo_version':subprocess.check_output(['cargo','-vV'],text=True),'tests_manifest_sha256':sha(O/'tests/result.json'),'installed_globally':False};(O/'result.json').write_text(json.dumps(r,indent=2)+'\n');print(json.dumps(r,indent=2));print('Use make BINDGEN='+str(binary)+' ... to build the kernel with layout assertions.') From 551e4e5d6dc51116269ff925c4bc41c8f9c3c092 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 20:06:26 +1100 Subject: [PATCH 182/388] arm64: restore common distro and peripheral requirements in asahi.config Keep defconfig plus the single shared fragment while restoring SELinux and its LSM policy order, nftables and NAT, container and systemd requirements, filesystems, hardening, desktop compatibility and attached USB peripherals. Do not enable scheduler statistics to hide an invalid Rust binding layout. --- arch/arm64/configs/asahi.config | 1265 ++++++++++++++++++++++++++++++- 1 file changed, 1262 insertions(+), 3 deletions(-) diff --git a/arch/arm64/configs/asahi.config b/arch/arm64/configs/asahi.config index d07b70afd9294..60d8a679594ef 100644 --- a/arch/arm64/configs/asahi.config +++ b/arch/arm64/configs/asahi.config @@ -1,7 +1,7 @@ -# Shared Apple Silicon hardware defaults for M1 and later, including Neo. -# Merge over ARM64 defconfig or a distribution base config, then run +# Shared Apple Silicon hardware and distribution defaults for M1 and later. +# Merge over ARM64 defconfig, then run # olddefconfig and verify that the requested settings survive resolution. -# Device-specific boot, debug and distribution policy stays in the base. +# One fragment serves every device; distribution policy is selected at runtime. CONFIG_RUST=y CONFIG_ARM64_16K_PAGES=y CONFIG_ARM64_VA_BITS_48=y @@ -98,3 +98,1262 @@ CONFIG_RDMA_RXE=m CONFIG_RDMA_SIW=m CONFIG_DMABUF_HEAPS_SYSTEM=y CONFIG_UDMABUF=y + +# Distribution requirements for the universal defconfig recipe. + +# Security policy, audit and labelled filesystems for the supported distributions. +CONFIG_AUDIT=y +CONFIG_AUDITSYSCALL=y +CONFIG_DEFAULT_SECURITY_SELINUX=y +CONFIG_EXT4_FS_POSIX_ACL=y +CONFIG_EXT4_FS_SECURITY=y +CONFIG_KEYS=y +CONFIG_KEYS_REQUEST_CACHE=y +CONFIG_LOAD_UEFI_KEYS=y +CONFIG_LSM="lockdown,yama,integrity,selinux,bpf,landlock,ipe" +CONFIG_SECONDARY_TRUSTED_KEYRING=y +CONFIG_SECURITY=y +CONFIG_SECURITYFS=y +CONFIG_SECURITY_DMESG_RESTRICT=y +CONFIG_SECURITY_INFINIBAND=y +CONFIG_SECURITY_IPE=y +CONFIG_SECURITY_LANDLOCK=y +CONFIG_SECURITY_LANDLOCK_LOG=y +CONFIG_SECURITY_LOCKDOWN_LSM=y +CONFIG_SECURITY_LOCKDOWN_LSM_EARLY=y +CONFIG_SECURITY_NETWORK=y +CONFIG_SECURITY_NETWORK_XFRM=y +CONFIG_SECURITY_PATH=y +CONFIG_SECURITY_SELINUX=y +CONFIG_SECURITY_SELINUX_AVC_STATS=y +CONFIG_SECURITY_SELINUX_BOOTPARAM=y +CONFIG_SECURITY_SELINUX_DEVELOP=y +CONFIG_SECURITY_YAMA=y +CONFIG_SYSTEM_BLACKLIST_KEYRING=y +CONFIG_SYSTEM_TRUSTED_KEYRING=y +CONFIG_TMPFS_POSIX_ACL=y +CONFIG_TMPFS_XATTR=y +CONFIG_XFRM=y +CONFIG_XFRM_USER=y + +# Integrity appraisal and verified storage; distribution policy controls enforcement. +CONFIG_EVM=y +CONFIG_EVM_ATTR_FSUUID=y +CONFIG_FS_ENCRYPTION=y +CONFIG_FS_ENCRYPTION_ALGS=y +CONFIG_FS_ENCRYPTION_INLINE_CRYPT=y +CONFIG_FS_VERITY=y +CONFIG_IMA=y +CONFIG_IMA_APPRAISE=y +CONFIG_IMA_APPRAISE_BOOTPARAM=y +CONFIG_IMA_APPRAISE_MODSIG=y +CONFIG_IMA_ARCH_POLICY=y +CONFIG_IMA_DEFAULT_HASH="sha256" +CONFIG_IMA_DEFAULT_HASH_SHA256=y +CONFIG_IMA_DEFAULT_TEMPLATE="ima-ng" +CONFIG_IMA_KEXEC=y +CONFIG_IMA_KEXEC_EXTRA_MEMORY_KB=0 +CONFIG_IMA_KEYRINGS_PERMIT_SIGNED_BY_BUILTIN_OR_SECONDARY=y +CONFIG_IMA_LSM_RULES=y +CONFIG_IMA_MEASURE_ASYMMETRIC_KEYS=y +CONFIG_IMA_MEASURE_PCR_IDX=10 +CONFIG_IMA_NG_TEMPLATE=y +CONFIG_IMA_QUEUE_EARLY_BOOT_KEYS=y +CONFIG_IMA_READ_POLICY=y +CONFIG_IMA_SECURE_AND_OR_TRUSTED_BOOT=y +CONFIG_IMA_WRITE_POLICY=y +CONFIG_INTEGRITY=y +CONFIG_INTEGRITY_ASYMMETRIC_KEYS=y +CONFIG_INTEGRITY_AUDIT=y +CONFIG_INTEGRITY_CA_MACHINE_KEYRING=y +CONFIG_INTEGRITY_CA_MACHINE_KEYRING_MAX=y +CONFIG_INTEGRITY_MACHINE_KEYRING=y +CONFIG_INTEGRITY_PLATFORM_KEYRING=y +CONFIG_INTEGRITY_SIGNATURE=y +CONFIG_INTEGRITY_TRUSTED_KEYRING=y + +# Firewalld nftables, IPv4/IPv6/bridge filtering, NAT, sets and iptables compatibility. +CONFIG_BRIDGE=m +CONFIG_BRIDGE_EBT_802_3=m +CONFIG_BRIDGE_EBT_AMONG=m +CONFIG_BRIDGE_EBT_ARP=m +CONFIG_BRIDGE_EBT_ARPREPLY=m +CONFIG_BRIDGE_EBT_BROUTE=m +CONFIG_BRIDGE_EBT_DNAT=m +CONFIG_BRIDGE_EBT_IP=m +CONFIG_BRIDGE_EBT_IP6=m +CONFIG_BRIDGE_EBT_LIMIT=m +CONFIG_BRIDGE_EBT_LOG=m +CONFIG_BRIDGE_EBT_MARK=m +CONFIG_BRIDGE_EBT_MARK_T=m +CONFIG_BRIDGE_EBT_NFLOG=m +CONFIG_BRIDGE_EBT_PKTTYPE=m +CONFIG_BRIDGE_EBT_REDIRECT=m +CONFIG_BRIDGE_EBT_SNAT=m +CONFIG_BRIDGE_EBT_STP=m +CONFIG_BRIDGE_EBT_T_FILTER=m +CONFIG_BRIDGE_EBT_T_NAT=m +CONFIG_BRIDGE_EBT_VLAN=m +CONFIG_BRIDGE_NETFILTER=m +CONFIG_BRIDGE_NF_EBTABLES=m +CONFIG_BRIDGE_NF_EBTABLES_LEGACY=m +CONFIG_IP6_NF_FILTER=m +CONFIG_IP6_NF_IPTABLES=m +CONFIG_IP6_NF_IPTABLES_LEGACY=m +CONFIG_IP6_NF_MANGLE=m +CONFIG_IP6_NF_MATCH_AH=m +CONFIG_IP6_NF_MATCH_EUI64=m +CONFIG_IP6_NF_MATCH_FRAG=m +CONFIG_IP6_NF_MATCH_HL=m +CONFIG_IP6_NF_MATCH_IPV6HEADER=m +CONFIG_IP6_NF_MATCH_MH=m +CONFIG_IP6_NF_MATCH_OPTS=m +CONFIG_IP6_NF_MATCH_RPFILTER=m +CONFIG_IP6_NF_MATCH_RT=m +CONFIG_IP6_NF_MATCH_SRH=m +CONFIG_IP6_NF_NAT=m +CONFIG_IP6_NF_RAW=m +CONFIG_IP6_NF_SECURITY=m +CONFIG_IP6_NF_TARGET_HL=m +CONFIG_IP6_NF_TARGET_MASQUERADE=m +CONFIG_IP6_NF_TARGET_NPT=m +CONFIG_IP6_NF_TARGET_REJECT=m +CONFIG_IP6_NF_TARGET_SYNPROXY=m +CONFIG_IP_NF_ARPFILTER=m +CONFIG_IP_NF_ARPTABLES=m +CONFIG_IP_NF_ARP_MANGLE=m +CONFIG_IP_NF_FILTER=m +CONFIG_IP_NF_IPTABLES=m +CONFIG_IP_NF_IPTABLES_LEGACY=m +CONFIG_IP_NF_MANGLE=m +CONFIG_IP_NF_MATCH_AH=m +CONFIG_IP_NF_MATCH_ECN=m +CONFIG_IP_NF_MATCH_RPFILTER=m +CONFIG_IP_NF_MATCH_TTL=m +CONFIG_IP_NF_NAT=m +CONFIG_IP_NF_RAW=m +CONFIG_IP_NF_SECURITY=m +CONFIG_IP_NF_TARGET_ECN=m +CONFIG_IP_NF_TARGET_MASQUERADE=m +CONFIG_IP_NF_TARGET_NETMAP=m +CONFIG_IP_NF_TARGET_REDIRECT=m +CONFIG_IP_NF_TARGET_REJECT=m +CONFIG_IP_NF_TARGET_SYNPROXY=m +CONFIG_IP_NF_TARGET_TTL=m +CONFIG_IP_SET=m +CONFIG_IP_SET_BITMAP_IP=m +CONFIG_IP_SET_BITMAP_IPMAC=m +CONFIG_IP_SET_BITMAP_PORT=m +CONFIG_IP_SET_HASH_IP=m +CONFIG_IP_SET_HASH_IPMAC=m +CONFIG_IP_SET_HASH_IPMARK=m +CONFIG_IP_SET_HASH_IPPORT=m +CONFIG_IP_SET_HASH_IPPORTIP=m +CONFIG_IP_SET_HASH_IPPORTNET=m +CONFIG_IP_SET_HASH_MAC=m +CONFIG_IP_SET_HASH_NET=m +CONFIG_IP_SET_HASH_NETIFACE=m +CONFIG_IP_SET_HASH_NETNET=m +CONFIG_IP_SET_HASH_NETPORT=m +CONFIG_IP_SET_HASH_NETPORTNET=m +CONFIG_IP_SET_LIST_SET=m +CONFIG_IP_SET_MAX=256 +CONFIG_NETFILTER=y +CONFIG_NETFILTER_ADVANCED=y +CONFIG_NETFILTER_BPF_LINK=y +CONFIG_NETFILTER_CONNCOUNT=m +CONFIG_NETFILTER_EGRESS=y +CONFIG_NETFILTER_FAMILY_ARP=y +CONFIG_NETFILTER_FAMILY_BRIDGE=y +CONFIG_NETFILTER_INGRESS=y +CONFIG_NETFILTER_NETLINK=y +CONFIG_NETFILTER_NETLINK_ACCT=m +CONFIG_NETFILTER_NETLINK_GLUE_CT=y +CONFIG_NETFILTER_NETLINK_HOOK=m +CONFIG_NETFILTER_NETLINK_LOG=m +CONFIG_NETFILTER_NETLINK_OSF=m +CONFIG_NETFILTER_NETLINK_QUEUE=m +CONFIG_NETFILTER_SKIP_EGRESS=y +CONFIG_NETFILTER_SYNPROXY=m +CONFIG_NETFILTER_XTABLES=y +CONFIG_NETFILTER_XTABLES_LEGACY=y +CONFIG_NETFILTER_XT_CONNMARK=m +CONFIG_NETFILTER_XT_MARK=m +CONFIG_NETFILTER_XT_MATCH_ADDRTYPE=m +CONFIG_NETFILTER_XT_MATCH_BPF=m +CONFIG_NETFILTER_XT_MATCH_CGROUP=m +CONFIG_NETFILTER_XT_MATCH_CLUSTER=m +CONFIG_NETFILTER_XT_MATCH_COMMENT=m +CONFIG_NETFILTER_XT_MATCH_CONNBYTES=m +CONFIG_NETFILTER_XT_MATCH_CONNLABEL=m +CONFIG_NETFILTER_XT_MATCH_CONNLIMIT=m +CONFIG_NETFILTER_XT_MATCH_CONNMARK=m +CONFIG_NETFILTER_XT_MATCH_CONNTRACK=m +CONFIG_NETFILTER_XT_MATCH_CPU=m +CONFIG_NETFILTER_XT_MATCH_DCCP=m +CONFIG_NETFILTER_XT_MATCH_DEVGROUP=m +CONFIG_NETFILTER_XT_MATCH_DSCP=m +CONFIG_NETFILTER_XT_MATCH_ECN=m +CONFIG_NETFILTER_XT_MATCH_ESP=m +CONFIG_NETFILTER_XT_MATCH_HASHLIMIT=m +CONFIG_NETFILTER_XT_MATCH_HELPER=m +CONFIG_NETFILTER_XT_MATCH_HL=m +CONFIG_NETFILTER_XT_MATCH_IPCOMP=m +CONFIG_NETFILTER_XT_MATCH_IPRANGE=m +CONFIG_NETFILTER_XT_MATCH_IPVS=m +CONFIG_NETFILTER_XT_MATCH_L2TP=m +CONFIG_NETFILTER_XT_MATCH_LENGTH=m +CONFIG_NETFILTER_XT_MATCH_LIMIT=m +CONFIG_NETFILTER_XT_MATCH_MAC=m +CONFIG_NETFILTER_XT_MATCH_MARK=m +CONFIG_NETFILTER_XT_MATCH_MULTIPORT=m +CONFIG_NETFILTER_XT_MATCH_NFACCT=m +CONFIG_NETFILTER_XT_MATCH_OSF=m +CONFIG_NETFILTER_XT_MATCH_OWNER=m +CONFIG_NETFILTER_XT_MATCH_PHYSDEV=m +CONFIG_NETFILTER_XT_MATCH_PKTTYPE=m +CONFIG_NETFILTER_XT_MATCH_POLICY=m +CONFIG_NETFILTER_XT_MATCH_QUOTA=m +CONFIG_NETFILTER_XT_MATCH_RATEEST=m +CONFIG_NETFILTER_XT_MATCH_REALM=m +CONFIG_NETFILTER_XT_MATCH_RECENT=m +CONFIG_NETFILTER_XT_MATCH_SCTP=m +CONFIG_NETFILTER_XT_MATCH_SOCKET=m +CONFIG_NETFILTER_XT_MATCH_STATE=m +CONFIG_NETFILTER_XT_MATCH_STATISTIC=m +CONFIG_NETFILTER_XT_MATCH_STRING=m +CONFIG_NETFILTER_XT_MATCH_TCPMSS=m +CONFIG_NETFILTER_XT_MATCH_TIME=m +CONFIG_NETFILTER_XT_MATCH_U32=m +CONFIG_NETFILTER_XT_NAT=m +CONFIG_NETFILTER_XT_SET=m +CONFIG_NETFILTER_XT_TARGET_AUDIT=m +CONFIG_NETFILTER_XT_TARGET_CHECKSUM=m +CONFIG_NETFILTER_XT_TARGET_CLASSIFY=m +CONFIG_NETFILTER_XT_TARGET_CONNMARK=m +CONFIG_NETFILTER_XT_TARGET_CONNSECMARK=m +CONFIG_NETFILTER_XT_TARGET_CT=m +CONFIG_NETFILTER_XT_TARGET_DSCP=m +CONFIG_NETFILTER_XT_TARGET_HL=m +CONFIG_NETFILTER_XT_TARGET_HMARK=m +CONFIG_NETFILTER_XT_TARGET_IDLETIMER=m +CONFIG_NETFILTER_XT_TARGET_LED=m +CONFIG_NETFILTER_XT_TARGET_LOG=m +CONFIG_NETFILTER_XT_TARGET_MARK=m +CONFIG_NETFILTER_XT_TARGET_MASQUERADE=m +CONFIG_NETFILTER_XT_TARGET_NETMAP=m +CONFIG_NETFILTER_XT_TARGET_NFLOG=m +CONFIG_NETFILTER_XT_TARGET_NFQUEUE=m +CONFIG_NETFILTER_XT_TARGET_NOTRACK=m +CONFIG_NETFILTER_XT_TARGET_RATEEST=m +CONFIG_NETFILTER_XT_TARGET_REDIRECT=m +CONFIG_NETFILTER_XT_TARGET_SECMARK=m +CONFIG_NETFILTER_XT_TARGET_TCPMSS=m +CONFIG_NETFILTER_XT_TARGET_TCPOPTSTRIP=m +CONFIG_NETFILTER_XT_TARGET_TEE=m +CONFIG_NETFILTER_XT_TARGET_TPROXY=m +CONFIG_NETFILTER_XT_TARGET_TRACE=m +CONFIG_NFT_BRIDGE_META=m +CONFIG_NFT_BRIDGE_REJECT=m +CONFIG_NFT_COMPAT=m +CONFIG_NFT_COMPAT_ARP=m +CONFIG_NFT_CONNLIMIT=m +CONFIG_NFT_CT=m +CONFIG_NFT_DUP_IPV4=m +CONFIG_NFT_DUP_IPV6=m +CONFIG_NFT_DUP_NETDEV=m +CONFIG_NFT_FIB=m +CONFIG_NFT_FIB_INET=m +CONFIG_NFT_FIB_IPV4=m +CONFIG_NFT_FIB_IPV6=m +CONFIG_NFT_FIB_NETDEV=m +CONFIG_NFT_FLOW_OFFLOAD=m +CONFIG_NFT_FWD_NETDEV=m +CONFIG_NFT_HASH=m +CONFIG_NFT_LIMIT=m +CONFIG_NFT_LOG=m +CONFIG_NFT_MASQ=m +CONFIG_NFT_NAT=m +CONFIG_NFT_NUMGEN=m +CONFIG_NFT_OSF=m +CONFIG_NFT_QUEUE=m +CONFIG_NFT_QUOTA=m +CONFIG_NFT_REDIR=m +CONFIG_NFT_REJECT=m +CONFIG_NFT_REJECT_INET=m +CONFIG_NFT_REJECT_IPV4=m +CONFIG_NFT_REJECT_IPV6=m +CONFIG_NFT_REJECT_NETDEV=m +CONFIG_NFT_SOCKET=m +CONFIG_NFT_SYNPROXY=m +CONFIG_NFT_TPROXY=m +CONFIG_NFT_TUNNEL=m +CONFIG_NFT_XFRM=m +CONFIG_NF_CONNTRACK=m +CONFIG_NF_CONNTRACK_AMANDA=m +CONFIG_NF_CONNTRACK_BRIDGE=m +CONFIG_NF_CONNTRACK_BROADCAST=m +CONFIG_NF_CONNTRACK_EVENTS=y +CONFIG_NF_CONNTRACK_FTP=m +CONFIG_NF_CONNTRACK_H323=m +CONFIG_NF_CONNTRACK_IRC=m +CONFIG_NF_CONNTRACK_LABELS=y +CONFIG_NF_CONNTRACK_MARK=y +CONFIG_NF_CONNTRACK_NETBIOS_NS=m +CONFIG_NF_CONNTRACK_OVS=y +CONFIG_NF_CONNTRACK_PPTP=m +CONFIG_NF_CONNTRACK_PROCFS=y +CONFIG_NF_CONNTRACK_SANE=m +CONFIG_NF_CONNTRACK_SECMARK=y +CONFIG_NF_CONNTRACK_SIP=m +CONFIG_NF_CONNTRACK_SNMP=m +CONFIG_NF_CONNTRACK_TFTP=m +CONFIG_NF_CONNTRACK_TIMEOUT=y +CONFIG_NF_CONNTRACK_TIMESTAMP=y +CONFIG_NF_CONNTRACK_ZONES=y +CONFIG_NF_CT_NETLINK=m +CONFIG_NF_CT_NETLINK_HELPER=m +CONFIG_NF_CT_NETLINK_TIMEOUT=m +CONFIG_NF_CT_PROTO_GRE=y +CONFIG_NF_CT_PROTO_SCTP=y +CONFIG_NF_DEFRAG_IPV4=m +CONFIG_NF_DEFRAG_IPV6=m +CONFIG_NF_DUP_IPV4=m +CONFIG_NF_DUP_IPV6=m +CONFIG_NF_DUP_NETDEV=m +CONFIG_NF_FLOW_TABLE=m +CONFIG_NF_FLOW_TABLE_INET=m +CONFIG_NF_FLOW_TABLE_PROCFS=y +CONFIG_NF_LOG_ARP=m +CONFIG_NF_LOG_IPV4=m +CONFIG_NF_LOG_IPV6=m +CONFIG_NF_LOG_SYSLOG=m +CONFIG_NF_NAT=m +CONFIG_NF_NAT_AMANDA=m +CONFIG_NF_NAT_FTP=m +CONFIG_NF_NAT_H323=m +CONFIG_NF_NAT_IRC=m +CONFIG_NF_NAT_MASQUERADE=y +CONFIG_NF_NAT_OVS=y +CONFIG_NF_NAT_PPTP=m +CONFIG_NF_NAT_REDIRECT=y +CONFIG_NF_NAT_SIP=m +CONFIG_NF_NAT_SNMP_BASIC=m +CONFIG_NF_NAT_TFTP=m +CONFIG_NF_REJECT_IPV4=m +CONFIG_NF_REJECT_IPV6=m +CONFIG_NF_SOCKET_IPV4=m +CONFIG_NF_SOCKET_IPV6=m +CONFIG_NF_TABLES=m +CONFIG_NF_TABLES_ARP=y +CONFIG_NF_TABLES_BRIDGE=m +CONFIG_NF_TABLES_INET=y +CONFIG_NF_TABLES_IPV4=y +CONFIG_NF_TABLES_IPV6=y +CONFIG_NF_TABLES_NETDEV=y +CONFIG_NF_TPROXY_IPV4=m +CONFIG_NF_TPROXY_IPV6=m + +# systemd and rootless containers: namespaces, cgroup v2 controllers, seccomp, BPF and pressure reporting. +CONFIG_BLK_CGROUP=y +CONFIG_BPF=y +CONFIG_BPF_EVENTS=y +CONFIG_BPF_JIT=y +CONFIG_BPF_JIT_ALWAYS_ON=y +CONFIG_BPF_LSM=y +CONFIG_BPF_SYSCALL=y +CONFIG_CFS_BANDWIDTH=y +CONFIG_CGROUPS=y +CONFIG_CGROUP_BPF=y +CONFIG_CGROUP_CPUACCT=y +CONFIG_CGROUP_DMEM=y +CONFIG_CGROUP_FREEZER=y +CONFIG_CGROUP_MISC=y +CONFIG_CGROUP_NET_CLASSID=y +CONFIG_CGROUP_NET_PRIO=y +CONFIG_CGROUP_PIDS=y +CONFIG_CGROUP_RDMA=y +CONFIG_CGROUP_SCHED=y +CONFIG_CHECKPOINT_RESTORE=y +CONFIG_CPUSETS=y +CONFIG_DEVTMPFS=y +CONFIG_DEVTMPFS_MOUNT=y +CONFIG_EFIVAR_FS=y +CONFIG_EFI_PARTITION=y +CONFIG_EPOLL=y +CONFIG_FAIR_GROUP_SCHED=y +CONFIG_FANOTIFY=y +CONFIG_FANOTIFY_ACCESS_PERMISSIONS=y +CONFIG_FHANDLE=y +CONFIG_INOTIFY_USER=y +CONFIG_IPC_NS=y +CONFIG_KCMP=y +CONFIG_MEMCG=y +CONFIG_NAMESPACES=y +CONFIG_NET_NS=y +CONFIG_NET_SCHED=y +CONFIG_NET_SCH_FQ_CODEL=y +CONFIG_PID_NS=y +CONFIG_POSIX_MQUEUE=y +CONFIG_PROC_FS=y +CONFIG_PSI=y +CONFIG_SECCOMP=y +CONFIG_SECCOMP_FILTER=y +CONFIG_SHMEM=y +CONFIG_SIGNALFD=y +CONFIG_SYSFS=y +CONFIG_TIMERFD=y +CONFIG_TIME_NS=y +CONFIG_TMPFS=y +CONFIG_UNIX=y +CONFIG_USER_NS=y +CONFIG_UTS_NS=y + +# Container networking, VPNs and the t-3155 distribution compatibility gate. +CONFIG_APPLE_PMC=y +CONFIG_BINFMT_MISC=m +CONFIG_BRCMFMAC=m +CONFIG_GENEVE=m +CONFIG_HIDRAW=y +CONFIG_INPUT_UINPUT=m +CONFIG_IPV6_MULTIPLE_TABLES=y +CONFIG_IPVLAN=m +CONFIG_IP_ADVANCED_ROUTER=y +CONFIG_IP_MULTIPLE_TABLES=y +CONFIG_IP_ROUTE_MULTIPATH=y +CONFIG_MACVLAN=m +CONFIG_MACVTAP=m +CONFIG_NET_CLS_CGROUP=y +CONFIG_NTSYNC=m +CONFIG_OPENVSWITCH=m +CONFIG_TUN=m +CONFIG_UCLAMP_TASK=y +CONFIG_UCLAMP_TASK_GROUP=y +CONFIG_USERFAULTFD=y +CONFIG_VETH=m +CONFIG_VHOST_NET=m +CONFIG_VHOST_VSOCK=m +CONFIG_VIRTIO_VSOCKETS=m +CONFIG_VSOCKETS=m +CONFIG_VXLAN=m +CONFIG_WIREGUARD=m +CONFIG_ZRAM=m + +# Filesystems, quotas and network shares used by distribution installations and removable media. +CONFIG_AUTOFS_FS=y +CONFIG_BLK_DEV_ZONED=y +CONFIG_BTRFS_FS=y +CONFIG_BTRFS_FS_POSIX_ACL=y +CONFIG_CACHEFILES=m +CONFIG_CIFS=m +CONFIG_CIFS_DFS_UPCALL=y +CONFIG_CIFS_FSCACHE=y +CONFIG_CIFS_POSIX=y +CONFIG_CIFS_SWN_UPCALL=y +CONFIG_CIFS_UPCALL=y +CONFIG_CIFS_XATTR=y +CONFIG_CUSE=m +CONFIG_DAX=y +CONFIG_EXFAT_DEFAULT_IOCHARSET="utf8" +CONFIG_EXFAT_FS=m +CONFIG_EXT4_FS=y +CONFIG_EXT4_USE_FOR_EXT2=y +CONFIG_F2FS_FS=m +CONFIG_F2FS_FS_COMPRESSION=y +CONFIG_F2FS_FS_LZ4=y +CONFIG_F2FS_FS_LZ4HC=y +CONFIG_F2FS_FS_LZO=y +CONFIG_F2FS_FS_LZORLE=y +CONFIG_F2FS_FS_POSIX_ACL=y +CONFIG_F2FS_FS_SECURITY=y +CONFIG_F2FS_FS_XATTR=y +CONFIG_F2FS_FS_ZSTD=y +CONFIG_F2FS_IOSTAT=y +CONFIG_F2FS_STAT_FS=y +CONFIG_F2FS_UNFAIR_RWSEM=y +CONFIG_FAT_FS=m +CONFIG_FSCACHE=y +CONFIG_FSCACHE_STATS=y +CONFIG_FS_DAX=y +CONFIG_FUSE_DAX=y +CONFIG_FUSE_FS=m +CONFIG_FUSE_IO_URING=y +CONFIG_FUSE_PASSTHROUGH=y +CONFIG_LCD_CLASS_DEVICE=m +CONFIG_NFSD=m +CONFIG_NFSD_BLOCKLAYOUT=y +CONFIG_NFSD_FLEXFILELAYOUT=y +CONFIG_NFSD_PNFS=y +CONFIG_NFSD_SCSILAYOUT=y +CONFIG_NFSD_V3_ACL=y +CONFIG_NFSD_V4=y +CONFIG_NFSD_V4_2_INTER_SSC=y +CONFIG_NFSD_V4_POSIX_ACLS=y +CONFIG_NFSD_V4_SECURITY_LABEL=y +CONFIG_NFS_ACL_SUPPORT=m +CONFIG_NFS_COMMON=y +CONFIG_NFS_COMMON_LOCALIO_SUPPORT=m +CONFIG_NFS_DISABLE_UDP_SUPPORT=y +CONFIG_NFS_FS=m +CONFIG_NFS_FSCACHE=y +CONFIG_NFS_LOCALIO=y +CONFIG_NFS_SWAP=y +CONFIG_NFS_USE_KERNEL_DNS=y +CONFIG_NFS_V3=m +CONFIG_NFS_V3_ACL=y +CONFIG_NFS_V4=m +CONFIG_NFS_V4_0=y +CONFIG_NFS_V4_1_IMPLEMENTATION_ID_DOMAIN="kernel.org" +CONFIG_NFS_V4_2=y +CONFIG_NFS_V4_2_SSC_HELPER=y +CONFIG_NFS_V4_SECURITY_LABEL=y +CONFIG_NLS_ASCII=y +CONFIG_NLS_CODEPAGE_1250=m +CONFIG_NLS_CODEPAGE_1251=m +CONFIG_NLS_CODEPAGE_437=y +CONFIG_NLS_CODEPAGE_737=m +CONFIG_NLS_CODEPAGE_775=m +CONFIG_NLS_CODEPAGE_850=m +CONFIG_NLS_CODEPAGE_852=m +CONFIG_NLS_CODEPAGE_855=m +CONFIG_NLS_CODEPAGE_857=m +CONFIG_NLS_CODEPAGE_860=m +CONFIG_NLS_CODEPAGE_861=m +CONFIG_NLS_CODEPAGE_862=m +CONFIG_NLS_CODEPAGE_863=m +CONFIG_NLS_CODEPAGE_864=m +CONFIG_NLS_CODEPAGE_865=m +CONFIG_NLS_CODEPAGE_866=m +CONFIG_NLS_CODEPAGE_869=m +CONFIG_NLS_CODEPAGE_874=m +CONFIG_NLS_CODEPAGE_932=m +CONFIG_NLS_CODEPAGE_936=m +CONFIG_NLS_CODEPAGE_949=m +CONFIG_NLS_CODEPAGE_950=m +CONFIG_NLS_DEFAULT="utf8" +CONFIG_NLS_ISO8859_1=m +CONFIG_NLS_ISO8859_13=m +CONFIG_NLS_ISO8859_14=m +CONFIG_NLS_ISO8859_15=m +CONFIG_NLS_ISO8859_2=m +CONFIG_NLS_ISO8859_3=m +CONFIG_NLS_ISO8859_4=m +CONFIG_NLS_ISO8859_5=m +CONFIG_NLS_ISO8859_6=m +CONFIG_NLS_ISO8859_7=m +CONFIG_NLS_ISO8859_8=m +CONFIG_NLS_ISO8859_9=m +CONFIG_NLS_KOI8_R=m +CONFIG_NLS_KOI8_U=m +CONFIG_NLS_MAC_CELTIC=m +CONFIG_NLS_MAC_CENTEURO=m +CONFIG_NLS_MAC_CROATIAN=m +CONFIG_NLS_MAC_CYRILLIC=m +CONFIG_NLS_MAC_GAELIC=m +CONFIG_NLS_MAC_GREEK=m +CONFIG_NLS_MAC_ICELAND=m +CONFIG_NLS_MAC_INUIT=m +CONFIG_NLS_MAC_ROMAN=m +CONFIG_NLS_MAC_ROMANIAN=m +CONFIG_NLS_MAC_TURKISH=m +CONFIG_NLS_UCS2_UTILS=m +CONFIG_NLS_UTF8=m +CONFIG_NTFS3_FS=m +CONFIG_NTFS3_FS_POSIX_ACL=y +CONFIG_NTFS3_LZX_XPRESS=y +CONFIG_OVERLAY_FS=m +CONFIG_OVERLAY_FS_REDIRECT_ALWAYS_FOLLOW=y +CONFIG_QFMT_V2=y +CONFIG_QUOTA=y +CONFIG_QUOTACTL=y +CONFIG_QUOTA_NETLINK_INTERFACE=y +CONFIG_QUOTA_TREE=y +CONFIG_TMPFS_INODE64=y +CONFIG_TMPFS_QUOTA=y +CONFIG_VFAT_FS=m +CONFIG_VIRTIO_FS=m +CONFIG_XFS_BTREE_IN_MEM=y +CONFIG_XFS_DRAIN_INTENTS=y +CONFIG_XFS_FS=m +CONFIG_XFS_LIVE_HOOKS=y +CONFIG_XFS_MEMORY_BUFS=y +CONFIG_XFS_ONLINE_REPAIR=y +CONFIG_XFS_ONLINE_SCRUB=y +CONFIG_XFS_ONLINE_SCRUB_STATS=y +CONFIG_XFS_POSIX_ACL=y +CONFIG_XFS_QUOTA=y +CONFIG_XFS_RT=y +CONFIG_XFS_SUPPORT_ASCII_CI=y +CONFIG_XFS_SUPPORT_V4=y +CONFIG_ZONE_DEVICE=y + +# Encrypted/root storage, software RAID, compression and zram backends. +CONFIG_ASYMMETRIC_KEY_TYPE=y +CONFIG_ASYMMETRIC_PUBLIC_KEY_SUBTYPE=y +CONFIG_BLK_DEV_DM=y +CONFIG_BLK_DEV_DM_BUILTIN=y +CONFIG_BLK_DEV_MD=y +CONFIG_CRYPTO_842=y +CONFIG_CRYPTO_ACOMP2=y +CONFIG_CRYPTO_ADIANTUM=m +CONFIG_CRYPTO_AEAD=y +CONFIG_CRYPTO_AEAD2=y +CONFIG_CRYPTO_AEGIS128=m +CONFIG_CRYPTO_AEGIS128_SIMD=y +CONFIG_CRYPTO_AES=y +CONFIG_CRYPTO_AES_ARM64_BS=m +CONFIG_CRYPTO_AES_ARM64_CE_BLK=y +CONFIG_CRYPTO_AES_ARM64_CE_CCM=y +CONFIG_CRYPTO_AES_ARM64_NEON_BLK=y +CONFIG_CRYPTO_AKCIPHER=y +CONFIG_CRYPTO_AKCIPHER2=y +CONFIG_CRYPTO_ALGAPI=y +CONFIG_CRYPTO_ALGAPI2=y +CONFIG_CRYPTO_AUTHENC=y +CONFIG_CRYPTO_BENCHMARK=m +CONFIG_CRYPTO_BLAKE2B=y +CONFIG_CRYPTO_BLOWFISH=m +CONFIG_CRYPTO_BLOWFISH_COMMON=m +CONFIG_CRYPTO_CAMELLIA=m +CONFIG_CRYPTO_CAST5=m +CONFIG_CRYPTO_CAST6=m +CONFIG_CRYPTO_CAST_COMMON=m +CONFIG_CRYPTO_CBC=y +CONFIG_CRYPTO_CCM=y +CONFIG_CRYPTO_CHACHA20=m +CONFIG_CRYPTO_CHACHA20POLY1305=m +CONFIG_CRYPTO_CMAC=y +CONFIG_CRYPTO_CRC32=m +CONFIG_CRYPTO_CRC32C=y +CONFIG_CRYPTO_CRYPTD=y +CONFIG_CRYPTO_CTR=y +CONFIG_CRYPTO_CTS=y +CONFIG_CRYPTO_DEFLATE=y +CONFIG_CRYPTO_DES=m +CONFIG_CRYPTO_DH=y +CONFIG_CRYPTO_DH_RFC7919_GROUPS=y +CONFIG_CRYPTO_DRBG=y +CONFIG_CRYPTO_ECB=y +CONFIG_CRYPTO_ECC=y +CONFIG_CRYPTO_ECDH=y +CONFIG_CRYPTO_ECDSA=y +CONFIG_CRYPTO_ECHAINIV=m +CONFIG_CRYPTO_ECRDSA=m +CONFIG_CRYPTO_ESSIV=m +CONFIG_CRYPTO_GCM=y +CONFIG_CRYPTO_GENIV=y +CONFIG_CRYPTO_GHASH_ARM64_CE=m +CONFIG_CRYPTO_HASH=y +CONFIG_CRYPTO_HASH2=y +CONFIG_CRYPTO_HASH_INFO=y +CONFIG_CRYPTO_HCTR2=m +CONFIG_CRYPTO_HMAC=y +CONFIG_CRYPTO_HW=y +CONFIG_CRYPTO_JITTERENTROPY=y +CONFIG_CRYPTO_JITTERENTROPY_MEMORY_BLOCKS=64 +CONFIG_CRYPTO_JITTERENTROPY_MEMORY_BLOCKSIZE=32 +CONFIG_CRYPTO_JITTERENTROPY_OSR=1 +CONFIG_CRYPTO_KPP=y +CONFIG_CRYPTO_KPP2=y +CONFIG_CRYPTO_KRB5=m +CONFIG_CRYPTO_KRB5ENC=m +CONFIG_CRYPTO_LIB_AES=y +CONFIG_CRYPTO_LIB_AES_ARCH=y +CONFIG_CRYPTO_LIB_AES_CBC=y +CONFIG_CRYPTO_LIB_AES_CBC_MACS=y +CONFIG_CRYPTO_LIB_AES_CCM=y +CONFIG_CRYPTO_LIB_AES_CTR=y +CONFIG_CRYPTO_LIB_AES_ECB=y +CONFIG_CRYPTO_LIB_AES_GCM=y +CONFIG_CRYPTO_LIB_AES_XTS=y +CONFIG_CRYPTO_LIB_ARC4=m +CONFIG_CRYPTO_LIB_BLAKE2B=y +CONFIG_CRYPTO_LIB_CHACHA_ARCH=y +CONFIG_CRYPTO_LIB_CURVE25519=m +CONFIG_CRYPTO_LIB_CURVE25519_GENERIC=y +CONFIG_CRYPTO_LIB_DES=m +CONFIG_CRYPTO_LIB_GF128HASH=y +CONFIG_CRYPTO_LIB_GF128HASH_ARCH=y +CONFIG_CRYPTO_LIB_GF128MUL=m +CONFIG_CRYPTO_LIB_MD5=y +CONFIG_CRYPTO_LIB_NH=m +CONFIG_CRYPTO_LIB_NH_ARCH=y +CONFIG_CRYPTO_LIB_POLY1305_ARCH=y +CONFIG_CRYPTO_LIB_POLY1305_GENERIC=y +CONFIG_CRYPTO_LIB_POLY1305_RSIZE=9 +CONFIG_CRYPTO_LIB_SHA1=y +CONFIG_CRYPTO_LIB_SHA1_ARCH=y +CONFIG_CRYPTO_LIB_SHA256=y +CONFIG_CRYPTO_LIB_SHA256_ARCH=y +CONFIG_CRYPTO_LIB_SHA3=y +CONFIG_CRYPTO_LIB_SHA3_ARCH=y +CONFIG_CRYPTO_LIB_SHA512=y +CONFIG_CRYPTO_LIB_SHA512_ARCH=y +CONFIG_CRYPTO_LIB_UTILS=y +CONFIG_CRYPTO_LRW=m +CONFIG_CRYPTO_LZ4=y +CONFIG_CRYPTO_LZ4HC=m +CONFIG_CRYPTO_LZO=y +CONFIG_CRYPTO_MANAGER=y +CONFIG_CRYPTO_MANAGER2=y +CONFIG_CRYPTO_MD4=m +CONFIG_CRYPTO_MD5=y +CONFIG_CRYPTO_NULL=y +CONFIG_CRYPTO_PCRYPT=m +CONFIG_CRYPTO_RMD160=m +CONFIG_CRYPTO_RNG=y +CONFIG_CRYPTO_RNG2=y +CONFIG_CRYPTO_RSA=y +CONFIG_CRYPTO_SELFTESTS=y +CONFIG_CRYPTO_SEQIV=y +CONFIG_CRYPTO_SERPENT=m +CONFIG_CRYPTO_SHA1=y +CONFIG_CRYPTO_SHA256=y +CONFIG_CRYPTO_SHA3=y +CONFIG_CRYPTO_SHA512=y +CONFIG_CRYPTO_SIG=y +CONFIG_CRYPTO_SIG2=y +CONFIG_CRYPTO_SKCIPHER=y +CONFIG_CRYPTO_SKCIPHER2=y +CONFIG_CRYPTO_STREEBOG=m +CONFIG_CRYPTO_TWOFISH=m +CONFIG_CRYPTO_TWOFISH_COMMON=m +CONFIG_CRYPTO_USER=m +CONFIG_CRYPTO_USER_API=y +CONFIG_CRYPTO_USER_API_AEAD=y +CONFIG_CRYPTO_USER_API_HASH=y +CONFIG_CRYPTO_USER_API_RNG=y +CONFIG_CRYPTO_USER_API_SKCIPHER=y +CONFIG_CRYPTO_WP512=m +CONFIG_CRYPTO_XCBC=m +CONFIG_CRYPTO_XCTR=m +CONFIG_CRYPTO_XTS=y +CONFIG_CRYPTO_XXHASH=y +CONFIG_CRYPTO_ZSTD=m +CONFIG_DM_AUDIT=y +CONFIG_DM_BIO_PRISON=m +CONFIG_DM_BUFIO=y +CONFIG_DM_CACHE=m +CONFIG_DM_CACHE_SMQ=m +CONFIG_DM_CLONE=m +CONFIG_DM_CRYPT=m +CONFIG_DM_DELAY=m +CONFIG_DM_DUST=m +CONFIG_DM_EBS=m +CONFIG_DM_ERA=m +CONFIG_DM_FLAKEY=m +CONFIG_DM_INIT=y +CONFIG_DM_INTEGRITY=m +CONFIG_DM_LOG_USERSPACE=m +CONFIG_DM_LOG_WRITES=m +CONFIG_DM_MIRROR=y +CONFIG_DM_MULTIPATH=m +CONFIG_DM_MULTIPATH_HST=m +CONFIG_DM_MULTIPATH_IOA=m +CONFIG_DM_MULTIPATH_QL=m +CONFIG_DM_MULTIPATH_ST=m +CONFIG_DM_PERSISTENT_DATA=m +CONFIG_DM_RAID=m +CONFIG_DM_SNAPSHOT=y +CONFIG_DM_SWITCH=m +CONFIG_DM_THIN_PROVISIONING=m +CONFIG_DM_UEVENT=y +CONFIG_DM_UNSTRIPED=m +CONFIG_DM_VDO=m +CONFIG_DM_VERITY=m +CONFIG_DM_VERITY_FEC=y +CONFIG_DM_VERITY_VERIFY_ROOTHASH_SIG=y +CONFIG_DM_VERITY_VERIFY_ROOTHASH_SIG_PLATFORM_KEYRING=y +CONFIG_DM_VERITY_VERIFY_ROOTHASH_SIG_SECONDARY_KEYRING=y +CONFIG_DM_WRITECACHE=m +CONFIG_DM_ZERO=y +CONFIG_DM_ZONED=m +CONFIG_MD_AUTODETECT=y +CONFIG_MD_BITMAP=y +CONFIG_MD_BITMAP_FILE=y +CONFIG_MD_LINEAR=m +CONFIG_MD_RAID0=m +CONFIG_MD_RAID1=m +CONFIG_MD_RAID10=m +CONFIG_MD_RAID456=m +CONFIG_SYSTEM_DATA_VERIFICATION=y +CONFIG_ZRAM_BACKEND_842=y +CONFIG_ZRAM_BACKEND_DEFLATE=y +CONFIG_ZRAM_BACKEND_LZ4=y +CONFIG_ZRAM_BACKEND_LZ4HC=y +CONFIG_ZRAM_BACKEND_LZO=y +CONFIG_ZRAM_BACKEND_ZSTD=y +CONFIG_ZRAM_DEF_COMP="lzo-rle" +CONFIG_ZRAM_DEF_COMP_LZORLE=y +CONFIG_ZRAM_MULTI_COMP=y +CONFIG_ZRAM_WRITEBACK=y +CONFIG_ZSMALLOC_CHAIN_SIZE=8 + +# Attached USB networking, serial adapters, HID, audio and storage peripherals. +CONFIG_DRAGONRISE_FF=y +CONFIG_GREENASIA_FF=y +CONFIG_HID=y +CONFIG_HID_A4TECH=m +CONFIG_HID_ACCUTOUCH=m +CONFIG_HID_ACRUX=m +CONFIG_HID_ACRUX_FF=y +CONFIG_HID_ALPS=m +CONFIG_HID_APPLEIR=m +CONFIG_HID_AUREAL=m +CONFIG_HID_BATTERY_STRENGTH=y +CONFIG_HID_BELKIN=m +CONFIG_HID_BETOP_FF=m +CONFIG_HID_BIGBEN_FF=m +CONFIG_HID_BPF=y +CONFIG_HID_CHERRY=m +CONFIG_HID_CHICONY=m +CONFIG_HID_CMEDIA=m +CONFIG_HID_CORSAIR=m +CONFIG_HID_COUGAR=m +CONFIG_HID_CP2112=m +CONFIG_HID_CREATIVE_SB0540=m +CONFIG_HID_CYPRESS=m +CONFIG_HID_DRAGONRISE=m +CONFIG_HID_ELAN=m +CONFIG_HID_ELECOM=m +CONFIG_HID_ELO=m +CONFIG_HID_EMS_FF=m +CONFIG_HID_EVISION=m +CONFIG_HID_EZKEY=m +CONFIG_HID_FT260=m +CONFIG_HID_GEMBIRD=m +CONFIG_HID_GENERIC=y +CONFIG_HID_GFRM=m +CONFIG_HID_GLORIOUS=m +CONFIG_HID_GOODIX_SPI=m +CONFIG_HID_GOOGLE_STADIA_FF=m +CONFIG_HID_GREENASIA=m +CONFIG_HID_GT683R=m +CONFIG_HID_GYRATION=m +CONFIG_HID_HAPTIC=y +CONFIG_HID_HOLTEK=m +CONFIG_HID_ICADE=m +CONFIG_HID_ITE=m +CONFIG_HID_JABRA=m +CONFIG_HID_KENSINGTON=m +CONFIG_HID_KEYTOUCH=m +CONFIG_HID_KYE=m +CONFIG_HID_KYSONA=m +CONFIG_HID_LCPOWER=m +CONFIG_HID_LED=m +CONFIG_HID_LENOVO=m +CONFIG_HID_LETSKETCH=m +CONFIG_HID_LOGITECH=m +CONFIG_HID_LOGITECH_DJ=m +CONFIG_HID_LOGITECH_HIDPP=m +CONFIG_HID_MACALLY=m +CONFIG_HID_MALTRON=m +CONFIG_HID_MAYFLASH=m +CONFIG_HID_MCP2221=m +CONFIG_HID_MEGAWORLD_FF=m +CONFIG_HID_MICROSOFT=m +CONFIG_HID_MONTEREY=m +CONFIG_HID_MULTITOUCH=m +CONFIG_HID_NINTENDO=m +CONFIG_HID_NTI=m +CONFIG_HID_NTRIG=y +CONFIG_HID_NVIDIA_SHIELD=m +CONFIG_HID_ORTEK=m +CONFIG_HID_PANTHERLORD=m +CONFIG_HID_PENMOUNT=m +CONFIG_HID_PETALYNX=m +CONFIG_HID_PICOLCD=m +CONFIG_HID_PICOLCD_BACKLIGHT=y +CONFIG_HID_PICOLCD_FB=y +CONFIG_HID_PICOLCD_LCD=y +CONFIG_HID_PICOLCD_LEDS=y +CONFIG_HID_PID=y +CONFIG_HID_PLANTRONICS=m +CONFIG_HID_PLAYSTATION=m +CONFIG_HID_PRIMAX=m +CONFIG_HID_PRODIKEYS=m +CONFIG_HID_PXRC=m +CONFIG_HID_RAPOO=m +CONFIG_HID_RAZER=m +CONFIG_HID_RETRODE=m +CONFIG_HID_RMI=m +CONFIG_HID_ROCCAT=m +CONFIG_HID_SAITEK=m +CONFIG_HID_SAMSUNG=m +CONFIG_HID_SEMITEK=m +CONFIG_HID_SENSOR_HUB=m +CONFIG_HID_SIGMAMICRO=m +CONFIG_HID_SMARTJOYPLUS=m +CONFIG_HID_SONY=m +CONFIG_HID_SPEEDLINK=m +CONFIG_HID_STEAM=m +CONFIG_HID_STEELSERIES=m +CONFIG_HID_SUNPLUS=m +CONFIG_HID_SUPPORT=y +CONFIG_HID_THINGM=m +CONFIG_HID_THRUSTMASTER=m +CONFIG_HID_TIVO=m +CONFIG_HID_TOPRE=m +CONFIG_HID_TOPSEED=m +CONFIG_HID_TWINHAN=m +CONFIG_HID_U2FZERO=m +CONFIG_HID_UCLOGIC=m +CONFIG_HID_UDRAW_PS3=m +CONFIG_HID_UNIVERSAL_PIDFF=m +CONFIG_HID_VIEWSONIC=m +CONFIG_HID_VIVALDI=m +CONFIG_HID_VIVALDI_COMMON=m +CONFIG_HID_WACOM=m +CONFIG_HID_WALTOP=m +CONFIG_HID_WIIMOTE=m +CONFIG_HID_WINWING=m +CONFIG_HID_XIAOMI=m +CONFIG_HID_XINMO=m +CONFIG_HID_ZEROPLUS=m +CONFIG_HID_ZYDACRON=m +CONFIG_HOLTEK_FF=y +CONFIG_I2C_HID=y +CONFIG_I2C_HID_ACPI=m +CONFIG_I2C_HID_CORE=y +CONFIG_I2C_HID_OF=y +CONFIG_I2C_HID_OF_ELAN=m +CONFIG_I2C_HID_OF_GOODIX=m +CONFIG_LOGIG940_FF=y +CONFIG_LOGIRUMBLEPAD2_FF=y +CONFIG_LOGITECH_FF=y +CONFIG_LOGIWHEELS_FF=y +CONFIG_NINTENDO_FF=y +CONFIG_NVIDIA_SHIELD_FF=y +CONFIG_PANTHERLORD_FF=y +CONFIG_PLAYSTATION_FF=y +CONFIG_REALTEK_AUTOPM=y +CONFIG_SMARTJOYPLUS_FF=y +CONFIG_SND_BCD2000=m +CONFIG_SND_USB=y +CONFIG_SND_USB_6FIRE=m +CONFIG_SND_USB_AUDIO=m +CONFIG_SND_USB_AUDIO_MIDI_V2=y +CONFIG_SND_USB_AUDIO_QMI=m +CONFIG_SND_USB_AUDIO_USE_MEDIA_CONTROLLER=y +CONFIG_SND_USB_CAIAQ=m +CONFIG_SND_USB_CAIAQ_INPUT=y +CONFIG_SND_USB_HIFACE=m +CONFIG_SND_USB_LINE6=m +CONFIG_SND_USB_POD=m +CONFIG_SND_USB_PODHD=m +CONFIG_SND_USB_TONEPORT=m +CONFIG_SND_USB_UA101=m +CONFIG_SND_USB_VARIAX=m +CONFIG_SONY_FF=y +CONFIG_SPI_HID_APPLE_CORE=m +CONFIG_STEAM_FF=y +CONFIG_THRUSTMASTER_FF=y +CONFIG_UHID=m +CONFIG_USB_ALI_M5632=y +CONFIG_USB_AN2720=y +CONFIG_USB_ARMLINUX=y +CONFIG_USB_BELKIN=y +CONFIG_USB_CATC=m +CONFIG_USB_EPSON2888=y +CONFIG_USB_HID=y +CONFIG_USB_HIDDEV=y +CONFIG_USB_HSO=m +CONFIG_USB_IPHETH=m +CONFIG_USB_KAWETH=m +CONFIG_USB_KC2190=y +CONFIG_USB_LAN78XX=m +CONFIG_USB_NET_AQC111=m +CONFIG_USB_NET_AX88179_178A=m +CONFIG_USB_NET_AX8817X=m +CONFIG_USB_NET_CDCETHER=m +CONFIG_USB_NET_CDC_EEM=m +CONFIG_USB_NET_CDC_MBIM=m +CONFIG_USB_NET_CDC_NCM=m +CONFIG_USB_NET_CDC_SUBSET=m +CONFIG_USB_NET_CDC_SUBSET_ENABLE=m +CONFIG_USB_NET_CH9200=m +CONFIG_USB_NET_CX82310_ETH=m +CONFIG_USB_NET_DM9601=m +CONFIG_USB_NET_DRIVERS=y +CONFIG_USB_NET_GL620A=m +CONFIG_USB_NET_HUAWEI_CDC_NCM=m +CONFIG_USB_NET_INT51X1=m +CONFIG_USB_NET_KALMIA=m +CONFIG_USB_NET_MCS7830=m +CONFIG_USB_NET_NET1080=m +CONFIG_USB_NET_PLUSB=m +CONFIG_USB_NET_QMI_WWAN=m +CONFIG_USB_NET_RNDIS_HOST=m +CONFIG_USB_NET_SMSC75XX=m +CONFIG_USB_NET_SMSC95XX=m +CONFIG_USB_NET_SR9700=m +CONFIG_USB_NET_ZAURUS=m +CONFIG_USB_PEGASUS=m +CONFIG_USB_RTL8150=m +CONFIG_USB_RTL8152=m +CONFIG_USB_RTL8153_ECM=m +CONFIG_USB_SERIAL=y +CONFIG_USB_SERIAL_AIRCABLE=m +CONFIG_USB_SERIAL_ARK3116=m +CONFIG_USB_SERIAL_BELKIN=m +CONFIG_USB_SERIAL_CH341=m +CONFIG_USB_SERIAL_CONSOLE=y +CONFIG_USB_SERIAL_CP210X=m +CONFIG_USB_SERIAL_CYBERJACK=m +CONFIG_USB_SERIAL_CYPRESS_M8=m +CONFIG_USB_SERIAL_DIGI_ACCELEPORT=m +CONFIG_USB_SERIAL_EDGEPORT=m +CONFIG_USB_SERIAL_EDGEPORT_TI=m +CONFIG_USB_SERIAL_EMPEG=m +CONFIG_USB_SERIAL_F81232=m +CONFIG_USB_SERIAL_F8153X=m +CONFIG_USB_SERIAL_FTDI_SIO=m +CONFIG_USB_SERIAL_GARMIN=m +CONFIG_USB_SERIAL_GENERIC=y +CONFIG_USB_SERIAL_IPAQ=m +CONFIG_USB_SERIAL_IPW=m +CONFIG_USB_SERIAL_IR=m +CONFIG_USB_SERIAL_IUU=m +CONFIG_USB_SERIAL_KEYSPAN=m +CONFIG_USB_SERIAL_KEYSPAN_PDA=m +CONFIG_USB_SERIAL_KLSI=m +CONFIG_USB_SERIAL_KOBIL_SCT=m +CONFIG_USB_SERIAL_MCT_U232=m +CONFIG_USB_SERIAL_MOS7720=m +CONFIG_USB_SERIAL_MOS7840=m +CONFIG_USB_SERIAL_NAVMAN=m +CONFIG_USB_SERIAL_OMNINET=m +CONFIG_USB_SERIAL_OPTICON=m +CONFIG_USB_SERIAL_OPTION=m +CONFIG_USB_SERIAL_OTI6858=m +CONFIG_USB_SERIAL_PL2303=m +CONFIG_USB_SERIAL_QCAUX=m +CONFIG_USB_SERIAL_QT2=m +CONFIG_USB_SERIAL_QUALCOMM=m +CONFIG_USB_SERIAL_SAFE=m +CONFIG_USB_SERIAL_SAFE_PADDED=y +CONFIG_USB_SERIAL_SIERRAWIRELESS=m +CONFIG_USB_SERIAL_SIMPLE=m +CONFIG_USB_SERIAL_SPCP8X5=m +CONFIG_USB_SERIAL_SSU100=m +CONFIG_USB_SERIAL_SYMBOL=m +CONFIG_USB_SERIAL_TI=m +CONFIG_USB_SERIAL_UPD78F0730=m +CONFIG_USB_SERIAL_VISOR=m +CONFIG_USB_SERIAL_WHITEHEAT=m +CONFIG_USB_SERIAL_WWAN=m +CONFIG_USB_SERIAL_XR=m +CONFIG_USB_SERIAL_XSENS_MT=m +CONFIG_USB_SIERRA_NET=m +CONFIG_USB_STORAGE=m +CONFIG_USB_STORAGE_ALAUDA=m +CONFIG_USB_STORAGE_CYPRESS_ATACB=m +CONFIG_USB_STORAGE_DATAFAB=m +CONFIG_USB_STORAGE_ENE_UB6250=m +CONFIG_USB_STORAGE_FREECOM=m +CONFIG_USB_STORAGE_ISD200=m +CONFIG_USB_STORAGE_JUMPSHOT=m +CONFIG_USB_STORAGE_KARMA=m +CONFIG_USB_STORAGE_ONETOUCH=m +CONFIG_USB_STORAGE_REALTEK=m +CONFIG_USB_STORAGE_SDDR09=m +CONFIG_USB_STORAGE_SDDR55=m +CONFIG_USB_STORAGE_USBAT=m +CONFIG_USB_UAS=m +CONFIG_USB_USBNET=m +CONFIG_USB_VL600=m +CONFIG_ZEROPLUS_FF=y + +# Desktop compatibility and distribution diagnostics; these do not select a device. +CONFIG_COMPAT=y +CONFIG_COMPAT_VDSO=y +CONFIG_DEBUG_INFO_BTF=y +CONFIG_DEBUG_INFO_BTF_MODULES=y +CONFIG_DEBUG_INFO_DWARF5=y +CONFIG_DEBUG_INFO_REDUCED=n +CONFIG_DYNAMIC_DEBUG=y +CONFIG_DYNAMIC_FTRACE=y +CONFIG_DYNAMIC_FTRACE_WITH_DIRECT_CALLS=y +CONFIG_EXPERT=y +CONFIG_FTRACE=y +CONFIG_FUNCTION_TRACER=y +CONFIG_IKCONFIG=y +CONFIG_IKCONFIG_PROC=y +CONFIG_KALLSYMS=y +CONFIG_KALLSYMS_ALL=y +CONFIG_KPROBES=y +CONFIG_PERF_EVENTS=y +CONFIG_UPROBES=y + +# Memory and userspace hardening retained from the distribution kernel. +CONFIG_BIG_KEYS=y +CONFIG_BUG_ON_DATA_CORRUPTION=y +CONFIG_CC_HAS_AUTO_VAR_INIT_PATTERN=y +CONFIG_CC_HAS_AUTO_VAR_INIT_ZERO=y +CONFIG_CC_HAS_AUTO_VAR_INIT_ZERO_BARE=y +CONFIG_CC_HAS_RANDSTRUCT=y +CONFIG_CC_HAS_SANCOV_STACK_DEPTH_CALLBACK=y +CONFIG_CC_HAS_ZERO_CALL_USED_REGS=y +CONFIG_ENCRYPTED_KEYS=y +CONFIG_FORTIFY_SOURCE=y +CONFIG_HARDENED_USERCOPY=y +CONFIG_HARDENED_USERCOPY_DEFAULT_ON=y +CONFIG_INIT_ON_ALLOC_DEFAULT_ON=y +CONFIG_INIT_STACK_ALL_ZERO=y +CONFIG_LIST_HARDENED=y +CONFIG_NETLABEL=y +CONFIG_NETWORK_SECMARK=y +CONFIG_PERSISTENT_KEYRINGS=y +CONFIG_RANDSTRUCT_NONE=y +CONFIG_SYN_COOKIES=y + +# Bluetooth protocols and common removable/immutable filesystem formats. +CONFIG_BT_AOSPEXT=y +CONFIG_BT_BNEP=m +CONFIG_BT_BNEP_MC_FILTER=y +CONFIG_BT_BNEP_PROTO_FILTER=y +CONFIG_BT_LE=y +CONFIG_BT_LEDS=y +CONFIG_BT_LE_L2CAP_ECRED=y +CONFIG_BT_MSFTEXT=y +CONFIG_BT_RFCOMM=m +CONFIG_BT_RFCOMM_TTY=y +CONFIG_EROFS_FS=m +CONFIG_EROFS_FS_BACKED_BY_FILE=y +CONFIG_EROFS_FS_POSIX_ACL=y +CONFIG_EROFS_FS_SECURITY=y +CONFIG_EROFS_FS_XATTR=y +CONFIG_EROFS_FS_ZIP=y +CONFIG_EROFS_FS_ZIP_ACCEL=y +CONFIG_EROFS_FS_ZIP_DEFLATE=y +CONFIG_EROFS_FS_ZIP_LZMA=y +CONFIG_EROFS_FS_ZIP_LZMA_DEFAULT_MAX_STREAMS=16 +CONFIG_EROFS_FS_ZIP_ZSTD=y +CONFIG_HFSPLUS_FS=m +CONFIG_HFS_FS=m +CONFIG_ISO9660_FS=m +CONFIG_JOLIET=y +CONFIG_MSDOS_FS=m +CONFIG_SQUASHFS=m +CONFIG_SQUASHFS_COMPILE_DECOMP_MULTI_PERCPU=y +CONFIG_SQUASHFS_DECOMP_MULTI_PERCPU=y +CONFIG_SQUASHFS_FILE_DIRECT=y +CONFIG_SQUASHFS_FRAGMENT_CACHE_SIZE=3 +CONFIG_SQUASHFS_LZ4=y +CONFIG_SQUASHFS_LZO=y +CONFIG_SQUASHFS_XATTR=y +CONFIG_SQUASHFS_XZ=y +CONFIG_SQUASHFS_ZLIB=y +CONFIG_SQUASHFS_ZSTD=y +CONFIG_UDF_FS=m +CONFIG_ZISOFS=y + +# IPsec, socket diagnostics and traffic control for VPNs and container networking. +CONFIG_CLS_U32_MARK=y +CONFIG_CLS_U32_PERF=y +CONFIG_INET6_AH=m +CONFIG_INET6_ESP=m +CONFIG_INET6_ESPINTCP=y +CONFIG_INET6_ESP_OFFLOAD=m +CONFIG_INET6_IPCOMP=m +CONFIG_INET6_XFRM_TUNNEL=m +CONFIG_INET_AH=m +CONFIG_INET_ESP=m +CONFIG_INET_ESPINTCP=y +CONFIG_INET_ESP_OFFLOAD=m +CONFIG_INET_IPCOMP=m +CONFIG_INET_RAW_DIAG=y +CONFIG_INET_UDP_DIAG=y +CONFIG_INET_XFRM_TUNNEL=m +CONFIG_IPV6_VTI=m +CONFIG_NETLINK_DIAG=y +CONFIG_NET_ACT_BPF=m +CONFIG_NET_ACT_CONNMARK=m +CONFIG_NET_ACT_CSUM=m +CONFIG_NET_ACT_CT=m +CONFIG_NET_ACT_CTINFO=m +CONFIG_NET_ACT_GACT=m +CONFIG_NET_ACT_GATE=m +CONFIG_NET_ACT_IFE=m +CONFIG_NET_ACT_MIRRED=m +CONFIG_NET_ACT_MPLS=m +CONFIG_NET_ACT_NAT=m +CONFIG_NET_ACT_PEDIT=m +CONFIG_NET_ACT_POLICE=m +CONFIG_NET_ACT_SAMPLE=m +CONFIG_NET_ACT_SIMP=m +CONFIG_NET_ACT_SKBEDIT=m +CONFIG_NET_ACT_SKBMOD=m +CONFIG_NET_ACT_TUNNEL_KEY=m +CONFIG_NET_ACT_VLAN=m +CONFIG_NET_CLS_ACT=y +CONFIG_NET_CLS_BASIC=m +CONFIG_NET_CLS_BPF=m +CONFIG_NET_CLS_FLOW=m +CONFIG_NET_CLS_FLOWER=m +CONFIG_NET_CLS_FW=m +CONFIG_NET_CLS_MATCHALL=m +CONFIG_NET_CLS_ROUTE4=m +CONFIG_NET_CLS_U32=m +CONFIG_NET_EMATCH=y +CONFIG_NET_EMATCH_CANID=m +CONFIG_NET_EMATCH_CMP=m +CONFIG_NET_EMATCH_IPSET=m +CONFIG_NET_EMATCH_IPT=m +CONFIG_NET_EMATCH_META=m +CONFIG_NET_EMATCH_NBYTE=m +CONFIG_NET_EMATCH_STACK=32 +CONFIG_NET_EMATCH_TEXT=m +CONFIG_NET_EMATCH_U32=m +CONFIG_NET_IFE=m +CONFIG_NET_IFE_SKBMARK=m +CONFIG_NET_IFE_SKBPRIO=m +CONFIG_NET_IFE_SKBTCINDEX=m +CONFIG_NET_IPVTI=m +CONFIG_NET_KEY=m +CONFIG_NET_KEY_MIGRATE=y +CONFIG_NET_SCH_BPF=y +CONFIG_NET_SCH_CAKE=m +CONFIG_NET_SCH_CBS=m +CONFIG_NET_SCH_CHOKE=m +CONFIG_NET_SCH_CODEL=m +CONFIG_NET_SCH_DRR=m +CONFIG_NET_SCH_DUALPI2=m +CONFIG_NET_SCH_ETF=m +CONFIG_NET_SCH_ETS=m +CONFIG_NET_SCH_FIFO=y +CONFIG_NET_SCH_FQ=m +CONFIG_NET_SCH_FQ_PIE=m +CONFIG_NET_SCH_GRED=m +CONFIG_NET_SCH_HFSC=m +CONFIG_NET_SCH_HHF=m +CONFIG_NET_SCH_HTB=m +CONFIG_NET_SCH_INGRESS=m +CONFIG_NET_SCH_MQPRIO=m +CONFIG_NET_SCH_MQPRIO_LIB=m +CONFIG_NET_SCH_MULTIQ=m +CONFIG_NET_SCH_NETEM=m +CONFIG_NET_SCH_PIE=m +CONFIG_NET_SCH_PLUG=m +CONFIG_NET_SCH_PRIO=m +CONFIG_NET_SCH_QFQ=m +CONFIG_NET_SCH_RED=m +CONFIG_NET_SCH_SFB=m +CONFIG_NET_SCH_SFQ=m +CONFIG_NET_SCH_TAPRIO=m +CONFIG_NET_SCH_TBF=m +CONFIG_NET_SCH_TEQL=m +CONFIG_PACKET_DIAG=y +CONFIG_PSAMPLE=m +CONFIG_UNIX_DIAG=y +CONFIG_XFRM_AH=m +CONFIG_XFRM_ALGO=y +CONFIG_XFRM_ESP=m +CONFIG_XFRM_ESPINTCP=y +CONFIG_XFRM_INTERFACE=m +CONFIG_XFRM_IPCOMP=m +CONFIG_XFRM_IPTFS=m +CONFIG_XFRM_MIGRATE=y +CONFIG_XFRM_OFFLOAD=y +CONFIG_XFRM_STATISTICS=y +CONFIG_XFRM_SUB_POLICY=y From d282380c3baa7a596846df8670e8d993deec39bf Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 20:29:57 +1100 Subject: [PATCH 183/388] arm64: retain the distro mmap floors and key notification support --- arch/arm64/configs/asahi.config | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/arch/arm64/configs/asahi.config b/arch/arm64/configs/asahi.config index 60d8a679594ef..ec2215594d521 100644 --- a/arch/arm64/configs/asahi.config +++ b/arch/arm64/configs/asahi.config @@ -109,6 +109,8 @@ CONFIG_EXT4_FS_POSIX_ACL=y CONFIG_EXT4_FS_SECURITY=y CONFIG_KEYS=y CONFIG_KEYS_REQUEST_CACHE=y +CONFIG_KEY_DH_OPERATIONS=y +CONFIG_KEY_NOTIFICATIONS=y CONFIG_LOAD_UEFI_KEYS=y CONFIG_LSM="lockdown,yama,integrity,selinux,bpf,landlock,ipe" CONFIG_SECONDARY_TRUSTED_KEYRING=y @@ -133,6 +135,7 @@ CONFIG_SYSTEM_BLACKLIST_KEYRING=y CONFIG_SYSTEM_TRUSTED_KEYRING=y CONFIG_TMPFS_POSIX_ACL=y CONFIG_TMPFS_XATTR=y +CONFIG_WATCH_QUEUE=y CONFIG_XFRM=y CONFIG_XFRM_USER=y @@ -1194,6 +1197,7 @@ CONFIG_CC_HAS_AUTO_VAR_INIT_ZERO_BARE=y CONFIG_CC_HAS_RANDSTRUCT=y CONFIG_CC_HAS_SANCOV_STACK_DEPTH_CALLBACK=y CONFIG_CC_HAS_ZERO_CALL_USED_REGS=y +CONFIG_DEFAULT_MMAP_MIN_ADDR=65536 CONFIG_ENCRYPTED_KEYS=y CONFIG_FORTIFY_SOURCE=y CONFIG_HARDENED_USERCOPY=y @@ -1201,6 +1205,7 @@ CONFIG_HARDENED_USERCOPY_DEFAULT_ON=y CONFIG_INIT_ON_ALLOC_DEFAULT_ON=y CONFIG_INIT_STACK_ALL_ZERO=y CONFIG_LIST_HARDENED=y +CONFIG_LSM_MMAP_MIN_ADDR=65535 CONFIG_NETLABEL=y CONFIG_NETWORK_SECMARK=y CONFIG_PERSISTENT_KEYRINGS=y From b18158c5ab592eebef397e455f7c28fb377a9fec Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 21:41:02 +1100 Subject: [PATCH 184/388] rust: select the prepared corrected bindgen by default Prepare the pinned generator at a stable source-tree entry point and prefer it for in-tree and O= builds. Keep every generated layout assertion mandatory and retain stock-tool rejection coverage. Document upstream status and the scope of aligned empty records. Signed-off-by: Ryan Murray Assisted-by: LLM --- Documentation/rust/bindgen-layout-checks.rst | 22 +++++--- Makefile | 5 +- tools/aurora-bindgen/.gitignore | 1 + tools/aurora-bindgen/README.rst | 58 ++++++++++++++++---- tools/aurora-bindgen/build.py | 7 ++- 5 files changed, 72 insertions(+), 21 deletions(-) create mode 100644 tools/aurora-bindgen/.gitignore diff --git a/Documentation/rust/bindgen-layout-checks.rst b/Documentation/rust/bindgen-layout-checks.rst index 5af9951a64338..4e0a996bd701d 100644 --- a/Documentation/rust/bindgen-layout-checks.rst +++ b/Documentation/rust/bindgen-layout-checks.rst @@ -46,11 +46,17 @@ Corrected generator ------------------- ``tools/aurora-bindgen/`` contains pinned patches for bindgen 0.73.2 that -repair both cases, a private build recipe and compile-only tests; see -``tools/aurora-bindgen/README.rst``. It installs nothing system-wide. To check -the layouts with it:: +repair both cases, a local build recipe and compile-only tests; see +``tools/aurora-bindgen/README.rst``. Prepare it once before configuration:: - make LLVM=1 BINDGEN=/path/to/bindgen ... + python3 tools/aurora-bindgen/build.py + make LLVM=1 ... + +The top-level Makefile automatically selects +``tools/aurora-bindgen/build/bin/bindgen`` from the source tree, also for ``O=`` +builds. Packagers prepare this entry point in their build roots. Make does not +fetch tools or install anything system-wide. If the entry point is absent, +Kbuild uses ``bindgen`` from PATH and the same mandatory assertions apply. The generated bindings are consumed by the Rust kernel crate and by several drivers. After changing the bindings generator, rebuild the kernel Image and @@ -61,7 +67,9 @@ the configured ``--rust-target 1.85``: that selects unconditional constant assertions rather than ``#[test]`` functions. They are checked without a Rust test configuration. -Pass ``BINDGEN=/absolute/path/to/bindgen`` as a make command-line argument. -Exporting it in the environment is insufficient: the top-level Makefile's -``BINDGEN = bindgen`` assignment takes precedence over an environment value. Check the recorded bindings command, not just the toolchain version banner. +Command-line overrides remain available for generator regression tests. +The defect can affect any generated aligned empty record and containing types +under other configurations; all emitted size/alignment/offset checks remain +mandatory. They do not cover C-only types or opaque internal fields. See the +generator README for the upstream status and related empty-struct issues. diff --git a/Makefile b/Makefile index c61ea07ee5051..956665f4965fe 100644 --- a/Makefile +++ b/Makefile @@ -550,7 +550,10 @@ RUSTC = rustc RUSTDOC = rustdoc RUSTFMT = rustfmt CLIPPY_DRIVER = clippy-driver -BINDGEN = bindgen +# Prefer the locally prepared, layout-corrected generator, including O= builds. +# Keep command-line overrides for toolchain testing; layouts are always checked. +aurora_bindgen := $(abs_srctree)/tools/aurora-bindgen/build/bin/bindgen +BINDGEN = $(if $(wildcard $(aurora_bindgen)),$(aurora_bindgen),bindgen) PAHOLE = pahole RESOLVE_BTFIDS = $(objtree)/tools/bpf/resolve_btfids/resolve_btfids LEX = flex diff --git a/tools/aurora-bindgen/.gitignore b/tools/aurora-bindgen/.gitignore new file mode 100644 index 0000000000000..84c048a73cc2e --- /dev/null +++ b/tools/aurora-bindgen/.gitignore @@ -0,0 +1 @@ +/build/ diff --git a/tools/aurora-bindgen/README.rst b/tools/aurora-bindgen/README.rst index 35e4b6eb8a274..9399e5da50c03 100644 --- a/tools/aurora-bindgen/README.rst +++ b/tools/aurora-bindgen/README.rst @@ -1,6 +1,6 @@ .. SPDX-License-Identifier: GPL-2.0-only -Private bindgen with checked C/Rust layouts +Default bindgen with checked C/Rust layouts ========================================== This package builds bindgen 0.73.2 with two generic binding-generation repairs: @@ -16,19 +16,30 @@ compiler or build path produces identical bytes. The manifest identifies when that digest predates comment-only patch changes; each rebuild must record its own binary digest. -Build in a fresh private directory:: +Prepare the default generator once before configuring or building the kernel:: - python3 tools/aurora-bindgen/build.py --output-dir /absolute/new/tool-build + python3 tools/aurora-bindgen/build.py + make LLVM=1 ... Python 3.12 or newer, Cargo/Rust, Clang/libclang and patch are required. Cargo's -locked dependency graph is used, with at most two build jobs. ``--archive`` can -supply an already downloaded source archive; its digest is still checked. The -script builds and runs compile-only valid-input checks, then prints the exact -``BINDGEN=/absolute/path/to/bindgen`` make argument. It installs nothing globally. -The kernel always retains layout assertions. Pass the printed ``BINDGEN=`` -argument on the make command line; an exported environment variable is -overridden by the top-level Makefile. Build the complete kernel Image and -matching modules with the same tool. +locked dependency graph is used, with at most two build jobs. The script builds +in ``tools/aurora-bindgen/build/`` and publishes ``build/bin/bindgen`` only after +its compile-only checks pass. The top-level Makefile selects that entry point +automatically for both in-tree and ``O=`` builds. No make variable or environment +setting is needed. It installs nothing globally and make never downloads or +builds the generator implicitly. Preserve the build log and ``build/result.json``. + +``--archive`` can supply an already downloaded source archive; its digest is +still checked. ``--output-dir`` supports separate tool experiments; a nondefault +location is not automatically selected. Packagers may instead place their +independently built and tested pinned tool at the same ``build/bin/bindgen`` +entry point (a symlink is sufficient). Remove or move a previous build directory +before rebuilding; the script refuses to overwrite it. + +If the prepared entry point is absent, Kbuild falls back to ``bindgen`` on PATH. +The mandatory layout assertions still reject incorrect bindings. A command-line +override remains available for generator regression testing. + Both scripts reject optimized Python (``-O``, ``-OO`` or ``PYTHONOPTIMIZE``) before creating output, since their validation assertions must remain enabled. @@ -90,3 +101,28 @@ Primary sources: A complete kernel build remains the configuration-specific layout check; these small generator fixtures do not replace it or establish runtime GPU stability. + +Upstream status and affected configurations +------------------------------------------ + +As checked on 2026-10-26, the latest upstream release is 0.73.2 and Linux's +minimum is 0.71.1. Both versions, and bindgen main, skip explicit alignment +handling for zero-sized non-opaque structs. Raising the minimum therefore does +not fix this defect. Upstream issue #551 and fix #552 concern C empty-struct +size, while #1683 concerns the Windows ABI; neither fixes this aligned-empty +case. No exact upstream issue or merged fix was found in the issue/PR search. + +The defect is generic: any explicitly aligned empty C record can have the wrong +Rust alignment, and embedding it can shift later fields or change enclosing +size/alignment. Configurations that empty conditional records, change cacheline +alignment or change preceding fields can expose it; it is not specific to +Asahi or ARM64. SCHEDSTATS=y masks this particular scheduler case. Every emitted +layout assertion is compiled regardless of KUnit, but this only covers types +and fields emitted by bindgen, not arbitrary C-only or opaque internals. + +* https://github.com/rust-lang/rust-bindgen/releases/tag/v0.73.2 +* https://github.com/torvalds/linux/blob/master/scripts/min-tool-version.sh +* https://github.com/rust-lang/rust-bindgen/blob/main/bindgen/codegen/mod.rs +* https://github.com/rust-lang/rust-bindgen/issues/551 +* https://github.com/rust-lang/rust-bindgen/pull/552 +* https://github.com/rust-lang/rust-bindgen/issues/1683 diff --git a/tools/aurora-bindgen/build.py b/tools/aurora-bindgen/build.py index fa87873098480..3ae28e18625ea 100644 --- a/tools/aurora-bindgen/build.py +++ b/tools/aurora-bindgen/build.py @@ -6,7 +6,7 @@ import argparse,hashlib,json,subprocess,tarfile,urllib.request,shutil,os,sys from pathlib import Path -P=Path(__file__).resolve().parent;ap=argparse.ArgumentParser();ap.add_argument('--output-dir',required=True,type=Path);ap.add_argument('--archive',type=Path);ap.add_argument('--jobs',type=int,default=2);a=ap.parse_args();assert 1<=a.jobs<=2 +P=Path(__file__).resolve().parent;ap=argparse.ArgumentParser();ap.add_argument('--output-dir',default=P/'build',type=Path);ap.add_argument('--archive',type=Path);ap.add_argument('--jobs',type=int,default=2);a=ap.parse_args();assert 1<=a.jobs<=2 O=a.output_dir.resolve();assert not O.exists(),'refusing to overwrite an existing source or tool';O.mkdir(parents=True) m=json.loads((P/'sources.json').read_text());sha=lambda p:hashlib.sha256(p.read_bytes()).hexdigest();archive=O/'source.tar.gz' if a.archive:shutil.copyfile(a.archive,archive) @@ -22,4 +22,7 @@ cmd=['cargo','build','--locked','--release','-p','bindgen-cli','-j'+str(a.jobs)] with (O/'build.log').open('w') as log:subprocess.run(cmd,cwd=S,stdout=log,stderr=subprocess.STDOUT,check=True) binary=S/'target/release/bindgen';subprocess.run([sys.executable,str(P/'test.py'),'--bindgen',str(binary),'--output-dir',str(O/'tests')],check=True) -r={'status':'BUILT_AND_COMPILE_TESTED','binary':str(binary),'binary_sha256':sha(binary),'source_manifest_sha256':sha(P/'sources.json'),'build_command':cmd,'rustc_version':subprocess.check_output(['rustc','-vV'],text=True),'cargo_version':subprocess.check_output(['cargo','-vV'],text=True),'tests_manifest_sha256':sha(O/'tests/result.json'),'installed_globally':False};(O/'result.json').write_text(json.dumps(r,indent=2)+'\n');print(json.dumps(r,indent=2));print('Use make BINDGEN='+str(binary)+' ... to build the kernel with layout assertions.') +r={'status':'BUILT_AND_COMPILE_TESTED','binary':str(binary),'binary_sha256':sha(binary),'source_manifest_sha256':sha(P/'sources.json'),'build_command':cmd,'rustc_version':subprocess.check_output(['rustc','-vV'],text=True),'cargo_version':subprocess.check_output(['cargo','-vV'],text=True),'tests_manifest_sha256':sha(O/'tests/result.json'),'installed_globally':False};(O/'result.json').write_text(json.dumps(r,indent=2)+'\n') +# Publish the stable entry point only after every compile-only check passed. +(O/'bin').mkdir();(O/'bin/bindgen').symlink_to(binary.relative_to(O/'bin',walk_up=True)) +print(json.dumps(r,indent=2));print('Prepared '+str(O/'bin/bindgen')+'. The default output is selected automatically by Kbuild.') From 7448de6cec9d39d206174b692820c5d472013f6a Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 22:00:16 +1100 Subject: [PATCH 185/388] arm64: keep the default ESP charset available in initramfs Asahi dracut loads vfat and mounts the system ESP without charset options before unpacking vendor firmware. Match Fedora codepage 437 and ASCII defaults with built-in converters so the mount does not depend on a modular ISO-8859-1 converter absent from initramfs. Retain XZ and Zstd firmware loading for distribution firmware packages. Signed-off-by: Ryan Murray Assisted-by: LLM --- arch/arm64/configs/asahi.config | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/arch/arm64/configs/asahi.config b/arch/arm64/configs/asahi.config index ec2215594d521..8980f8f408d75 100644 --- a/arch/arm64/configs/asahi.config +++ b/arch/arm64/configs/asahi.config @@ -542,6 +542,19 @@ CONFIG_VXLAN=m CONFIG_WIREGUARD=m CONFIG_ZRAM=m +# Asahi dracut mounts the ESP with default VFAT options before the rootfs exists. +# Match Fedora's 437/ASCII defaults and keep both converters built in: a modular +# default charset may be absent from the early vendor-firmware initramfs. +CONFIG_FAT_DEFAULT_CODEPAGE=437 +CONFIG_FAT_DEFAULT_IOCHARSET="ascii" +CONFIG_NLS_CODEPAGE_437=y +CONFIG_NLS_ASCII=y + +# Distribution firmware packages can contain .xz and .zst files after extraction. +CONFIG_FW_LOADER_COMPRESS=y +CONFIG_FW_LOADER_COMPRESS_XZ=y +CONFIG_FW_LOADER_COMPRESS_ZSTD=y + # Filesystems, quotas and network shares used by distribution installations and removable media. CONFIG_AUTOFS_FS=y CONFIG_BLK_DEV_ZONED=y @@ -610,10 +623,8 @@ CONFIG_NFS_V4_1_IMPLEMENTATION_ID_DOMAIN="kernel.org" CONFIG_NFS_V4_2=y CONFIG_NFS_V4_2_SSC_HELPER=y CONFIG_NFS_V4_SECURITY_LABEL=y -CONFIG_NLS_ASCII=y CONFIG_NLS_CODEPAGE_1250=m CONFIG_NLS_CODEPAGE_1251=m -CONFIG_NLS_CODEPAGE_437=y CONFIG_NLS_CODEPAGE_737=m CONFIG_NLS_CODEPAGE_775=m CONFIG_NLS_CODEPAGE_850=m From 9a76756bf2d7122695042095530f03c441a9bcad Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Mon, 26 Oct 2026 06:43:39 -0400 Subject: [PATCH 186/388] drm/asahi: retire faulted G17 compute commands on the firmware's fault record When a compute command fails on the GPU the firmware writes a zero-duration completion record for it, which quarantines its queue, and reports the failure with a firmware event of type 7 carrying the queue and the stamp the command would have stored. It never stores that stamp. The driver ignored the event, so the quarantine waited for a retirement witness that could not arrive, and the queue kept its pool, its traversal scratch and its share of the lower GPU VA window for as long as the module stayed loaded. Under ray query stress that fails this way every few seconds (for example ray_query.stress.geometry_shader beside other clients) the retained queues filled the lower window within minutes. Every later allocation there failed: publications returned EBUSY, and gnome-shell crashed on ENOMEM creating a 16K texture after about 7 minutes. Decode the event as ComputeFault and accept its stamp as the faulted command's completion stamp for the quarantine's retirement witness, while it is newer than the stored one. The witness still requires the queue's ring to be fully consumed, so commands published after the failed one keep waiting for their own stamp. --- drivers/gpu/drm/asahi/g17/event.rs | 7 ++++ drivers/gpu/drm/asahi/g17/fw/channels.rs | 41 +++++++++++++++++++ drivers/gpu/drm/asahi/g17/queue/compute.rs | 31 +++++++++++++- .../drm/asahi/g17/queue/compute/retirement.rs | 15 +++++++ drivers/gpu/drm/asahi/g17/runtime/events.rs | 9 ++++ 5 files changed, 102 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/asahi/g17/event.rs b/drivers/gpu/drm/asahi/g17/event.rs index 9508a827be4b1..e726a75602b86 100644 --- a/drivers/gpu/drm/asahi/g17/event.rs +++ b/drivers/gpu/drm/asahi/g17/event.rs @@ -36,6 +36,8 @@ pub(super) trait Queues { fn retain_render_completions(&mut self, masks: [u64; 2]); fn complete_compute(&mut self, masks: [u64; 2]) -> Result; fn context_killed(&mut self, cookie: u64); + /// The firmware failed a compute command (see [`Event::ComputeFault`]). + fn compute_fault(&mut self, qid: u8, stamp: u32); } /// Work requested after releasing the device mutex. @@ -72,6 +74,11 @@ fn batch(host: &mut H, role: Role, masks: &mut [u64; 2]) -> Result { } Event::FreeListGrow(completion) => host.complete_freelist_grow(completion)?, Event::ContextKilled(cookie) => host.context_killed(cookie), + Event::ComputeFault { qid, stamp } => { + host.compute_fault(qid, stamp); + // Its queue retires or settles its quarantine on this pass. + masks[usize::from(qid) / 64] |= 1 << (qid % 64); + } Event::Unknown(_) => {} } Ok(true) diff --git a/drivers/gpu/drm/asahi/g17/fw/channels.rs b/drivers/gpu/drm/asahi/g17/fw/channels.rs index 524132c4ae7be..53e899085a0e4 100644 --- a/drivers/gpu/drm/asahi/g17/fw/channels.rs +++ b/drivers/gpu/drm/asahi/g17/fw/channels.rs @@ -136,6 +136,9 @@ pub(crate) enum Event { TvbGrow(TvbGrowRequest), FreeListGrow(FreeListCompletion), ContextKilled(u64), + /// The firmware failed a compute command of queue `qid` and stores no completion stamp for + /// it: `stamp` is the value that command would have stored. + ComputeFault { qid: u8, stamp: u32 }, Unknown(u32), } @@ -145,6 +148,8 @@ impl EventRecord { const TVB_GROW: u32 = 6; const FREE_LIST_GROW: u32 = 13; const CONTEXT_KILLED: u32 = 12; + const COMPUTE_FAULT: u32 = 7; + const COMPUTE_QIDS: u32 = 128; const DIAGNOSTIC_PAIRS_MAX: u16 = 5; const TVB_SLOTS: u32 = 127; const SUBPIPES: u32 = 64; @@ -198,6 +203,19 @@ impl EventRecord { halt_count: record.halt_count, })) } + Self::COMPUTE_FAULT => { + // SAFETY: Every constructor initializes all integer words. + let words = unsafe { self.words }; + // Only compute faults are known to use this type: any other layout stays an + // unknown event, consumed and ignored as before. + if words[3] >= Self::COMPUTE_QIDS || words[4] & 0xff != 0 { + return Ok(Event::Unknown(Self::COMPUTE_FAULT)); + } + Ok(Event::ComputeFault { + qid: words[3] as u8, + stamp: words[4], + }) + } Self::CONTEXT_KILLED => { // SAFETY: Every constructor initializes all integer words. let words = unsafe { self.words }; @@ -525,3 +543,26 @@ impl ContextKill { } } } + +#[cfg_attr(not(test), kernel::macros::kunit_tests(asahi_g17_event_records))] +mod tests { + use super::*; + + fn record(words: &[u32]) -> EventRecord { + let mut all = [0u32; EVENT_WORDS]; + all[..words.len()].copy_from_slice(words); + EventRecord::from_words(all) + } + + #[test] + fn a_compute_fault_names_its_queue_and_stamp() { + let event = record(&[7, 1, 2, 0x39, 0x300, 0xffff_fc00, 1]).decode(); + assert_eq!(event, Ok(Event::ComputeFault { qid: 0x39, stamp: 0x300 })); + } + + #[test] + fn an_unrecognized_type_7_layout_stays_unknown() { + assert_eq!(record(&[7, 1, 2, 128, 0x300]).decode(), Ok(Event::Unknown(7))); + assert_eq!(record(&[7, 1, 2, 9, 0x301]).decode(), Ok(Event::Unknown(7))); + } +} diff --git a/drivers/gpu/drm/asahi/g17/queue/compute.rs b/drivers/gpu/drm/asahi/g17/queue/compute.rs index 9c4c5c92d049d..3d5e12be64842 100644 --- a/drivers/gpu/drm/asahi/g17/queue/compute.rs +++ b/drivers/gpu/drm/asahi/g17/queue/compute.rs @@ -575,6 +575,9 @@ pub(crate) struct Queue { config_cache: Option<(QueueConfig, u64)>, ordinal: u32, submitted: u32, + /// The stamp of the newest command the firmware reported failed ([`Self::note_fault`]), + /// while it is newer than the stored stamp. + fault_stamp: Option, /// Prepared packets of the bound owner about to publish (see [`Self::claim`]). claims: u32, last_published: u64, @@ -694,6 +697,7 @@ impl Queue { config_cache: None, ordinal: 0, submitted: 0, + fault_stamp: None, claims: 0, last_published: 0, last_end: 0, @@ -714,6 +718,29 @@ impl Queue { withheld_doorbell: None, }) } + /// The firmware failed the command whose completion stamp is `stamp`. It writes a + /// zero-duration completion record for it, which quarantines the queue, but stores no + /// stamp: without this the quarantine would wait for its retirement witness forever and + /// keep the queue's backing. Commands published after the failed one still need their own + /// stamp or record. + pub(crate) fn note_fault(&mut self, stamp: u32) { + if self + .fault_stamp + .is_none_or(|old| retirement::stamp_newer(stamp, old)) + { + self.fault_stamp = Some(stamp); + } + } + + /// The stamp that proves retirement: the stored one, or a newer reported fault's. + fn witness_stamp(&self) -> Result { + let stored = self.graph.stamp()?; + Ok(match self.fault_stamp { + Some(fault) if retirement::stamp_newer(fault, stored) => fault, + _ => stored, + }) + } + pub(crate) fn qid(&self) -> u8 { self.kick.id().qid() } @@ -1096,7 +1123,7 @@ impl Queue { if !retirement::quarantined_retired( &newest, self.graph.cursors()?, - self.graph.stamp()?, + self.witness_stamp()?, self.active.len as u32, self.submitted, )? { @@ -1140,6 +1167,7 @@ impl Queue { drop(self.active.pop()); } self.awaiting_witness = false; + self.fault_stamp = None; self.retirement_proved = false; self.replay_retirement = false; self.spared_deferred = false; @@ -1584,6 +1612,7 @@ impl Queue { self.spared_deferred = false; self.retire_pending = false; self.awaiting_witness = false; + self.fault_stamp = None; self.retirement_proved = false; self.replay_retirement = false; self.replay_error = None; diff --git a/drivers/gpu/drm/asahi/g17/queue/compute/retirement.rs b/drivers/gpu/drm/asahi/g17/queue/compute/retirement.rs index c2b49e7b436d7..8a737546cb7cf 100644 --- a/drivers/gpu/drm/asahi/g17/queue/compute/retirement.rs +++ b/drivers/gpu/drm/asahi/g17/queue/compute/retirement.rs @@ -52,6 +52,12 @@ impl Cursors { } } +/// Whether stamp `a` was stored after stamp `b` (both kick stamps, modulo wrap). +pub(crate) const fn stamp_newer(a: u32, b: u32) -> bool { + let delta = a.wrapping_sub(b); + delta != 0 && delta < 0x8000_0000 +} + /// Outstanding kicks span fewer than half the 24-bit stamp sequence. Zero is a valid wrap. pub(crate) const fn stamp_covers(word: u32, kick: u64) -> bool { let delta = word.wrapping_sub((kick as u32).wrapping_shl(8)); @@ -447,6 +453,15 @@ mod tests { assert!(stamp_covers(0, 0xff_ffff)); } + #[test] + fn a_reported_fault_stamp_is_newer_only_until_stored() { + assert!(stamp_newer(own(KICK), own(KICK - 1))); + assert!(!stamp_newer(own(KICK), own(KICK))); + assert!(!stamp_newer(own(KICK - 1), own(KICK))); + // Across the wrap of the 24-bit kick in the stamp word. + assert!(stamp_newer(own(0), own(0xff_ffff))); + } + #[test] fn a_late_stamp_is_waited_for() { // Under games on J700 the stamp lands up to about 4 ms after the record while the diff --git a/drivers/gpu/drm/asahi/g17/runtime/events.rs b/drivers/gpu/drm/asahi/g17/runtime/events.rs index 4cf2dd0fafb97..0057a8481bf05 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/events.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/events.rs @@ -72,6 +72,15 @@ impl event::Queues for Service<'_> { fn context_killed(&mut self, cookie: u64) { self.firmware.queues.acknowledge_compute_exit(cookie); } + fn compute_fault(&mut self, qid: u8, stamp: u32) { + for entry in self.firmware.queues.compute.iter_mut().flatten() { + if let Some(mut queue) = entry.queue.as_deref_mut() { + if queue.qid() == qid { + queue.note_fault(stamp); + } + } + } + } fn flush_tvb_reply(&mut self) -> Result { self.firmware.flush_tvb_reply() } From 75e950202a6192587c79e0ba5e197ee63dfa2ad7 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 18:26:51 +1100 Subject: [PATCH 187/388] drm/apple: publish J616s panel identity with notch-hidden timings The J616s panel has no adj-height-mm property in the r5g DT. With its notch hidden, probe therefore leaves height_mm zero and the panel EDID builder returns EINVAL. Read the full height before the optional adjusted height and describe the 216 mm viewport in DT. Even with dimensions repaired, the real 118-line vertical porch and 82-line notch-expanded pulse cannot fit in a base EDID DTD. Bound those fields only in the synthetic identity summary, retaining resolution, clock and blanking. Enumerate and program the unchanged DCP catalogue. Restore exact connector millimetres after DRM's EDID update. Test the absent ADT panel nodes with synthetic machine-level properties, real mode geometry, missing adjusted height, and the actual iomfb probe and DRM EDID blob allocation. The publication KUnit case fails on r5g's builder even after repairing the height read. Assisted-by: LLM Signed-off-by: Ryan Murray --- Documentation/gpu/apple-dcp-t6041-panel.rst | 16 ++- arch/arm64/boot/dts/apple/t6040-j616s.dts | 1 + drivers/gpu/drm/apple/dcp-display-metadata.c | 24 +++- drivers/gpu/drm/apple/dcp-panel-adt.c | 1 + drivers/gpu/drm/apple/dcp-panel-test.h | 57 ++++++++++ drivers/gpu/drm/apple/dcp.c | 21 ++-- drivers/gpu/drm/apple/dcp.h | 1 + drivers/gpu/drm/apple/iomfb.c | 14 ++- drivers/gpu/drm/apple/parser-test.c | 106 ++++++++++++++++++ .../selftests/drivers/gpu/apple-dcp/j616s.py | 99 ++++++++++++++++ .../selftests/drivers/gpu/apple-dcp/rate.py | 2 + .../selftests/drivers/gpu/apple-dcp/run.py | 2 + 12 files changed, 331 insertions(+), 13 deletions(-) create mode 100644 drivers/gpu/drm/apple/dcp-panel-test.h create mode 100644 tools/testing/selftests/drivers/gpu/apple-dcp/j616s.py diff --git a/Documentation/gpu/apple-dcp-t6041-panel.rst b/Documentation/gpu/apple-dcp-t6041-panel.rst index 2566c3fd36a94..3e0d107fa7d1b 100644 --- a/Documentation/gpu/apple-dcp-t6041-panel.rst +++ b/Documentation/gpu/apple-dcp-t6041-panel.rst @@ -117,7 +117,15 @@ mappings, not a claim that a particular firmware supplies every property. node is handled the same way. Machine serials from other nodes are ignored. * ``width-mm`` and ``height-mm`` supply bounded physical dimensions. Missing dimensions retain the OF panel node's ``width-mm`` and ``height-mm`` (or - ``adj-height-mm`` when the notch is hidden). + ``adj-height-mm`` when the notch is hidden). The full height is read first, + so a missing optional adjusted height cannot leave the height at zero. + J616s has a 345x223 mm full panel and a 345x216 mm notch-hidden viewport. + The connector retains these exact millimetres after the EDID update. + +A successfully parsed ADT does not necessarily contain a panel node. In +particular, a machine-level ``/product`` ID and ``/arm-io/disp0`` timing data +are not panel identity properties. They do not override OF panel dimensions +or prevent the generic fallback from being published. If the ADT or its identity fields are absent, the desktop J616 fallback is manufacturer APP, product 0616, name Apple Panel and unspecified numeric @@ -125,6 +133,12 @@ serial zero, represented by the non-empty serial descriptor ``00000000``. This generic identity does not claim to identify an individual panel. The preferred representable timing and range limits come from the retained DCP TimingElements catalog. Exact mode enumeration continues to use that catalog. +For notch-hidden modes, the synthetic identity DTD retains the clock, active +area and total blanking but bounds the vertical front porch and sync pulse to +EDID's six-bit fields. It is an identity summary, never an enumerated or +programmed mode; the original DCP sync positions and mode IDs are unchanged. +This also lets a 118-line porch and 82-line notch-expanded pulse publish an +identity instead of failing the base EDID size check. The EDID records model year 2024 (not a manufacture date) and a nominal D65 reference white point required by EDID parsing. No calibrated primaries, gamma, new mode or machine serial is claimed. The ADT reader, diff --git a/arch/arm64/boot/dts/apple/t6040-j616s.dts b/arch/arm64/boot/dts/apple/t6040-j616s.dts index 5707ec4a632d2..7abbd491489d0 100644 --- a/arch/arm64/boot/dts/apple/t6040-j616s.dts +++ b/arch/arm64/boot/dts/apple/t6040-j616s.dts @@ -639,6 +639,7 @@ compatible = "apple,panel-j616", "apple,panel-mini-led", "apple,panel"; width-mm = <345>; height-mm = <223>; + adj-height-mm = <216>; status = "disabled"; }; }; diff --git a/drivers/gpu/drm/apple/dcp-display-metadata.c b/drivers/gpu/drm/apple/dcp-display-metadata.c index f04f651a4ac57..c5921bf67cfa9 100644 --- a/drivers/gpu/drm/apple/dcp-display-metadata.c +++ b/drivers/gpu/drm/apple/dcp-display-metadata.c @@ -205,6 +205,28 @@ const struct drm_edid *dcp_metadata_edid(const struct dcp_display_metadata *meta return edid; } +/* + * This identity-only DTD is not enumerated or programmed. Keep the DCP clock, + * active area and blanking, but fit vertical sync placement into EDID's six-bit + * fields. Notch hiding expands the real sync pulse beyond those fields; the + * exact syncs remain in the authoritative TimingElements catalogue. + */ +static int dcp_panel_identity_timing(struct detailed_timing *timing, + const struct drm_display_mode *mode, + const struct dcp_display_metadata *metadata) +{ + struct drm_display_mode summary = *mode; + + /* Refuse invalid source geometry before reducing the EDID summary. */ + if (mode->vdisplay <= 0 || mode->vdisplay > 4095 || + mode->vsync_start < mode->vdisplay || mode->vsync_end <= mode->vsync_start || + mode->vsync_end > mode->vtotal) + return -EINVAL; + summary.vsync_start = mode->vdisplay + min(mode->vsync_start - mode->vdisplay, 63); + summary.vsync_end = summary.vsync_start + min(mode->vsync_end - mode->vsync_start, 63); + return dcp_metadata_timing(timing, &summary, metadata); +} + /* EDID 1.4 range offsets extend each one-byte limit by 255. */ static int dcp_panel_range_byte(u32 limit, u8 *out, u8 *offsets, u8 flag) { @@ -273,7 +295,7 @@ const struct drm_edid *dcp_panel_edid(const struct dcp_display_metadata *metadat clock = max_t(u32, clock, mode.clock); /* The EDID base timing clock has units of 10 kHz. */ mode.clock = DIV_ROUND_CLOSEST(mode.clock, 10) * 10; - ret = dcp_metadata_timing(&candidate, &mode, &dimensions); + ret = dcp_panel_identity_timing(&candidate, &mode, &dimensions); if (!ret && (!timing || (!preferred && (mode.type & DRM_MODE_TYPE_PREFERRED)))) { struct detailed_pixel_timing *pixel = &candidate.data.pixel_data; diff --git a/drivers/gpu/drm/apple/dcp-panel-adt.c b/drivers/gpu/drm/apple/dcp-panel-adt.c index e3a9b5e4f238e..5887091aa57cf 100644 --- a/drivers/gpu/drm/apple/dcp-panel-adt.c +++ b/drivers/gpu/drm/apple/dcp-panel-adt.c @@ -117,6 +117,7 @@ static int dcp_adt_node(struct dcp_adt_cursor *c, struct dcp_panel_adt *panel, dcp_adt_panel_property(panel, name, prop + 36, size); c->pos += 36 + ALIGN((size_t)size, 4); } + /* A valid ADT may have neither panel node; retain the OF defaults. */ panel->found |= selected; c->pos = props_end; for (u32 i = 0; i < children; i++) { diff --git a/drivers/gpu/drm/apple/dcp-panel-test.h b/drivers/gpu/drm/apple/dcp-panel-test.h new file mode 100644 index 0000000000000..05d575d975fd5 --- /dev/null +++ b/drivers/gpu/drm/apple/dcp-panel-test.h @@ -0,0 +1,57 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ +/* Synthetic J616s-shaped input: no /panel or /product/panel. */ +static const u8 panel_test_adt[] = { + 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x6e, 0x61, 0x6d, 0x65, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x64, 0x65, 0x76, 0x69, + 0x63, 0x65, 0x2d, 0x74, 0x72, 0x65, 0x65, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x6e, 0x61, 0x6d, 0x65, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x00, + 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x2d, 0x69, 0x64, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, + 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x70, 0x61, 0x72, 0x74, + 0x69, 0x61, 0x6c, 0x6c, 0x79, 0x2d, 0x6f, 0x63, 0x63, 0x6c, 0x75, 0x64, + 0x65, 0x64, 0x2d, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x6e, 0x61, 0x6d, 0x65, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x61, 0x72, 0x6d, 0x2d, + 0x69, 0x6f, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x6e, 0x61, 0x6d, 0x65, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x64, 0x69, 0x73, 0x70, 0x30, 0x00, 0x00, 0x00, 0x64, 0x69, 0x73, 0x70, + 0x6c, 0x61, 0x79, 0x2d, 0x74, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x2d, 0x69, + 0x6e, 0x66, 0x6f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x6e, 0x61, 0x6d, 0x65, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x63, 0x68, 0x6f, 0x73, 0x65, 0x6e, 0x00, 0x00, 0x73, 0x65, 0x72, 0x69, + 0x61, 0x6c, 0x2d, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x53, 0x59, 0x4e, 0x54, + 0x48, 0x45, 0x54, 0x49, 0x43, 0x2d, 0x4d, 0x41, 0x43, 0x48, 0x49, 0x4e, + 0x45, 0x00, 0x00, 0x00, +}; + +/* Notch-hidden mode geometry observed through the public DRM interface. */ +static const struct dcp_display_mode panel_test_mode = { + .mode = { + .clock = 521065, .hdisplay = 3456, .hsync_start = 3464, + .hsync_end = 3496, .htotal = 3536, .vdisplay = 2160, + .vsync_start = 2278, .vsync_end = 2360, .vtotal = 2456, + .flags = DRM_MODE_FLAG_PHSYNC | DRM_MODE_FLAG_PVSYNC, + .type = DRM_MODE_TYPE_DRIVER | DRM_MODE_TYPE_PREFERRED, + }, + .vrr = true, .min_vrr = 24U << 16, .max_vrr = 120U << 16, +}; diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 1d0b81aa3ad65..628c1b7e05660 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -785,6 +785,15 @@ const struct drm_edid *dcp_firmware_edid(struct apple_dcp *dcp, u64 *generation) * without an MTD dependency. Only the desktop panel calls this; other DCP * profiles retain their established identity and brightness paths. */ +void dcp_panel_read_dimensions(struct apple_dcp *dcp, struct device_node *panel_np) +{ + of_property_read_u32(panel_np, "width-mm", &dcp->panel.width_mm); + /* Read the full height first: adj-height-mm is optional. */ + of_property_read_u32(panel_np, "height-mm", &dcp->panel.height_mm); + if (dcp->notch_height) + of_property_read_u32(panel_np, "adj-height-mm", &dcp->panel.height_mm); +} + static void dcp_panel_adt_identity(struct apple_dcp *dcp) { struct dcp_panel_adt panel = { @@ -818,8 +827,9 @@ static void dcp_panel_adt_identity(struct apple_dcp *dcp) dcp->panel.width_mm = panel.width_mm; dcp->panel.height_mm = panel.height_mm; dcp->desktop->panel_adt_identity = panel.identity; - dev_info(dcp->dev, "panel identity fallback: ADT %s, OF dimensions %ux%u mm\n", - ret ? "unavailable" : "read", panel.width_mm, panel.height_mm); + dev_info(dcp->dev, "panel identity fallback: ADT %s, panel node %s, dimensions %ux%u mm\n", + ret ? "unavailable" : "parsed", panel.found ? "present" : "absent", + panel.width_mm, panel.height_mm); } /* Internal panel identity has no detachable Type-C service or EDID transport. */ @@ -4918,8 +4928,6 @@ static int dcp_comp_bind_pipe(struct device *dev, struct device *main, void *dat panel_np = of_get_compatible_child(dev->of_node, "apple,panel"); if (panel_np) { - const char height_prop[2][16] = { "adj-height-mm", "height-mm" }; - if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) { /* * Desktop 26.x swaps carry nits, and the panel node @@ -4939,10 +4947,7 @@ static int dcp_comp_bind_pipe(struct device *dev, struct device *main, void *dat /* J616 ProMotion capability belongs to the panel, not a boot option. */ dcp->panel.has_vrr = of_device_is_compatible(panel_np, "apple,panel-j616"); - of_property_read_u32(panel_np, "width-mm", &dcp->panel.width_mm); - /* use adjusted height as long as the notch is hidden */ - of_property_read_u32(panel_np, height_prop[!dcp->notch_height], - &dcp->panel.height_mm); + dcp_panel_read_dimensions(dcp, panel_np); if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) dcp_panel_adt_identity(dcp); diff --git a/drivers/gpu/drm/apple/dcp.h b/drivers/gpu/drm/apple/dcp.h index 30b728820e2c7..02e13b32b62c7 100644 --- a/drivers/gpu/drm/apple/dcp.h +++ b/drivers/gpu/drm/apple/dcp.h @@ -101,6 +101,7 @@ bool dcp_crtc_mode_fixup(struct drm_crtc *crtc, const struct drm_display_mode *mode, struct drm_display_mode *adjusted_mode); void dcp_set_dimensions(struct apple_dcp *dcp, u64 generation); +void dcp_panel_read_dimensions(struct apple_dcp *dcp, struct device_node *panel_np); void dcp_update_connector_dimensions(struct apple_dcp *dcp, struct drm_connector *connector); void dcp_send_message(struct apple_dcp *dcp, u8 endpoint, u64 message); diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index 67a6ff3dfedb1..27c7ba4b39b66 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -2076,9 +2076,13 @@ int dcp_get_modes(struct drm_connector *connector) dev_info(dcp->dev, "using firmware-derived EDID metadata for connection %llu\n", generation); } - } else if (apple_connector->drm_edid_firmware_derived) { - drm_edid_connector_update(connector, NULL); - apple_connector->drm_edid_firmware_derived = false; + } else { + if (panel && nr_modes) + dev_dbg(dcp->dev, "panel EDID generation failed: %pe\n", derived); + if (apple_connector->drm_edid_firmware_derived) { + drm_edid_connector_update(connector, NULL); + apple_connector->drm_edid_firmware_derived = false; + } } } @@ -2132,6 +2136,10 @@ int dcp_get_modes(struct drm_connector *connector) connector->display_info.monitor_range.max_vfreq = max_vfreq; } + /* EDID base dimensions are rounded to cm; the panel's OF/ADT size is exact. */ + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && dcp_has_panel(dcp)) + dcp_update_connector_dimensions(dcp, connector); + return published; } diff --git a/drivers/gpu/drm/apple/parser-test.c b/drivers/gpu/drm/apple/parser-test.c index 85b6c0df99753..dca33251ad5ed 100644 --- a/drivers/gpu/drm/apple/parser-test.c +++ b/drivers/gpu/drm/apple/parser-test.c @@ -4,6 +4,7 @@ #include #include #include +#include #include #include "dcp.h" @@ -15,6 +16,7 @@ #include "iomfb_internal.h" #include "parser.h" #include "dcp-panel-adt.h" +#include "dcp-panel-test.h" static struct device parser_device = { .init_name = "apple-display-parser-test", @@ -1427,7 +1429,111 @@ static void parser_adt_bounds(struct kunit *test) KUNIT_EXPECT_EQ(test, dcp_panel_adt_parse(NULL, 0, &panel), -EINVAL); } +static void parser_panel_of_dimensions(struct kunit *test) +{ + __be32 width = cpu_to_be32(345), height = cpu_to_be32(223); + __be32 adjusted = cpu_to_be32(216); + struct property adj = { .name = "adj-height-mm", .length = 4, .value = &adjusted }; + struct property h = { .name = "height-mm", .length = 4, .value = &height }; + struct property w = { .name = "width-mm", .length = 4, .value = &width, .next = &h }; + struct device_node node = { .properties = &w }; + struct apple_dcp *dcp = kunit_kzalloc(test, sizeof(*dcp), GFP_KERNEL); + + KUNIT_ASSERT_NOT_NULL(test, dcp); + dcp->notch_height = 74; + /* A missing optional adj-height-mm must not erase the full height. */ + dcp_panel_read_dimensions(dcp, &node); + KUNIT_EXPECT_EQ(test, dcp->panel.width_mm, 345U); + KUNIT_EXPECT_EQ(test, dcp->panel.height_mm, 223U); + h.next = &adj; + dcp_panel_read_dimensions(dcp, &node); + KUNIT_EXPECT_EQ(test, dcp->panel.height_mm, 216U); + dcp->notch_height = 0; + dcp_panel_read_dimensions(dcp, &node); + KUNIT_EXPECT_EQ(test, dcp->panel.height_mm, 223U); +} + +static void parser_panel_j616s_publication(struct kunit *test) +{ + struct apple_dcp *dcp = kunit_kzalloc(test, sizeof(*dcp), GFP_KERNEL); + struct dcp_desktop_state *desktop = kunit_kzalloc(test, sizeof(*desktop), GFP_KERNEL); + struct platform_device *pipeline = kunit_kzalloc(test, sizeof(*pipeline), GFP_KERNEL); + struct apple_connector *connector = kunit_kzalloc(test, sizeof(*connector), GFP_KERNEL); + struct dcp_panel_adt panel = { + .identity = { .manufacturer = "APP", .product = 0x616, .name = "Apple Panel" }, + .width_mm = 345, .height_mm = 216, + }; + struct dcp_panel_adt before = panel; + struct device *dev = drm_kunit_helper_alloc_device(test); + struct drm_device *drm; + struct drm_display_mode *published; + struct dcp_display_mode mode = panel_test_mode; + const struct edid *raw; + int count, ret; + + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, dev); + drm = __drm_kunit_helper_alloc_drm_device(test, dev, sizeof(*drm), 0, + DRIVER_MODESET | DRIVER_ATOMIC); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, drm); + KUNIT_ASSERT_NOT_NULL(test, dcp); + KUNIT_ASSERT_NOT_NULL(test, desktop); + KUNIT_ASSERT_NOT_NULL(test, pipeline); + KUNIT_ASSERT_NOT_NULL(test, connector); + ret = drmm_connector_init(drm, &connector->base, &parser_route_connector_funcs, + DRM_MODE_CONNECTOR_eDP, NULL); + KUNIT_ASSERT_EQ(test, ret, 0); + ret = dcp_panel_adt_parse(panel_test_adt, sizeof(panel_test_adt), &panel); + KUNIT_ASSERT_EQ(test, ret, 0); + /* /product's machine ID is not a panel product; neither panel node exists. */ + KUNIT_EXPECT_FALSE(test, panel.found); + KUNIT_EXPECT_MEMEQ(test, &panel, &before, sizeof(panel)); + parser_modes_init(dcp); + mutex_init(&dcp->dcpavserv.lock); + mutex_init(&dcp->hpd_mutex); + dcp->dev = dev; + dcp->desktop = desktop; + dcp->fw_compat = DCP_FIRMWARE_V_26_6_DESKTOP; + dcp->panel.width_mm = panel.width_mm; + dcp->panel.height_mm = panel.height_mm; + desktop->panel_adt_identity = panel.identity; + dcp->connector = connector; + dcp->modes = &mode; + dcp->nr_modes = 1; + dcp->modes_admitted = true; + connector->dcp = pipeline; + connector->edid_live = true; + platform_set_drvdata(pipeline, dcp); + + /* Probe through the real iomfb publication path and DRM blob allocator. */ + mutex_lock(&drm->mode_config.mutex); + count = dcp_get_modes(&connector->base); + mutex_unlock(&drm->mode_config.mutex); + KUNIT_EXPECT_EQ(test, count, 1); + KUNIT_ASSERT_NOT_NULL(test, connector->base.edid_blob_ptr); + KUNIT_EXPECT_EQ(test, connector->base.edid_blob_ptr->length, (size_t)128); + KUNIT_EXPECT_TRUE(test, connector->drm_edid_firmware_derived); + raw = connector->base.edid_blob_ptr->data; + KUNIT_EXPECT_MEMEQ(test, raw->detailed_timings[1].data.other_data.data.str.str, + "Apple Panel", 11); + KUNIT_EXPECT_EQ(test, connector->base.display_info.width_mm, 345U); + KUNIT_EXPECT_EQ(test, connector->base.display_info.height_mm, 216U); + KUNIT_EXPECT_MEMEQ(test, &mode, &panel_test_mode, sizeof(mode)); + KUNIT_ASSERT_FALSE(test, list_empty(&connector->base.probed_modes)); + published = list_first_entry(&connector->base.probed_modes, struct drm_display_mode, head); + KUNIT_EXPECT_EQ(test, published->clock, 521065); + KUNIT_EXPECT_EQ(test, published->vsync_start, 2278); + KUNIT_EXPECT_EQ(test, published->vsync_end, 2360); + list_del(&published->head); + drm_mode_destroy(drm, published); + /* The stack catalogue is not owned by dcp_modes_release(). */ + dcp->modes = NULL; + dcp->nr_modes = 0; + dcp_modes_release(dcp); +} + static struct kunit_case parser_cases[] = { + KUNIT_CASE(parser_panel_of_dimensions), + KUNIT_CASE(parser_panel_j616s_publication), KUNIT_CASE(parser_adt_bounds), KUNIT_CASE(parser_identity_bounds), KUNIT_CASE(parser_panel_identity_stable), diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/j616s.py b/tools/testing/selftests/drivers/gpu/apple-dcp/j616s.py new file mode 100644 index 0000000000000..3d903694e9e5c --- /dev/null +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/j616s.py @@ -0,0 +1,99 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Panel identity with absent ADT panel nodes and notch-hidden timings.""" +import re +from common import block +import desktop + + +def identity(root, output): + base = root / 'drivers/gpu/drm/apple' + unit = desktop.panel_identity(root).split('int main(int argc, char **argv)')[0] + unit = unit.replace('int fw_compat;bool has_panel;', + 'int fw_compat;unsigned notch_height;bool has_panel;') + unit += r''' +#define ALIGN(x,n) (((x)+(n)-1)&~((n)-1)) +#define SZ_16M (16U<<20) +static u32 get_unaligned_le32(const void *p) {u32 n;memcpy(&n,p,4);return n;} +''' + unit += block((base / 'dcp-panel-adt.h').read_text(), 'struct dcp_panel_adt {') + ';\n' + unit += '\n'.join(line for line in (base / 'dcp-panel-adt.c').read_text().splitlines() + if not line.startswith('#include')) + unit += (base / 'dcp-panel-test.h').read_text() + source = (base / 'dcp.c').read_text() + # Also exercise the pre-fix property read, so the regression fails on r5g. + if 'void dcp_panel_read_dimensions(' in source: + reader = block(source, 'void dcp_panel_read_dimensions(') + else: + start = source.index('\t\tof_property_read_u32(panel_np, "width-mm"') + end = source.index('\n\n', start) + reader = ('void dcp_panel_read_dimensions(struct apple_dcp *dcp, ' + 'struct device_node *panel_np) {\n' + 'const char height_prop[2][16] = {"adj-height-mm", "height-mm"};\n' + + source[start:end] + '\n}') + dts = (root / 'arch/arm64/boot/dts/apple/t6040-j616s.dts').read_text() + values = [] + for name in ('width-mm', 'height-mm', 'adj-height-mm'): + match = re.search(r'^\s*' + name + r'\s*=\s*<(\d+)>;', dts, re.M) + values.append(int(match[1]) if match else 0) + unit += r''' +struct device_node { u32 width, height, adjusted; }; +static int of_property_read_u32(struct device_node *np, const char *name, u32 *out) +{ + u32 value = !strcmp(name,"width-mm") ? np->width : + !strcmp(name,"height-mm") ? np->height : np->adjusted; + if (!value) return -EINVAL; + *out=value; + return 0; +} +''' + reader + '\n' + r''' +int main(int argc, char **argv) +{ + assert(argc==3); + struct apple_dcp d={.fw_compat=5,.has_panel=true}; + __typeof__(*d.desktop) state={0};d.desktop=&state; + struct device_node np={DTS_VALUES}; + d.notch_height=74; + if (!strcmp(argv[1],"dimensions")) { + dcp_panel_read_dimensions(&d,&np); + assert(d.panel.width_mm==345 && d.panel.height_mm==216); + /* A missing optional adjusted height must retain the full height. */ + np.adjusted=0;d.panel.height_mm=0; + dcp_panel_read_dimensions(&d,&np); + assert(d.panel.width_mm==345 && d.panel.height_mm==223); + d.notch_height=0;np.adjusted=216; + dcp_panel_read_dimensions(&d,&np); + assert(d.panel.height_mm==223); + return 0; + } + struct dcp_panel_adt panel={ + .identity={.manufacturer="APP",.product=0x616,.name="Apple Panel"}, + .width_mm=345,.height_mm=216, + }, before=panel; + assert(!dcp_panel_adt_parse(panel_test_adt,sizeof(panel_test_adt),&panel)); + assert(!panel.found && !memcmp(&panel,&before,sizeof(panel))); + for (unsigned n=0;nraw; + assert(!memcmp(raw+77,"Apple Panel",11)); + assert((raw[66] | (raw[68]>>4)<<8)==345); + assert((raw[67] | (raw[68]&15)<<8)==216); + FILE *file=fopen(argv[2],"wb");assert(file); + assert(fwrite(raw,128,1,file)==1);fclose(file); + drm_edid_free(edid); + /* Invalid geometry must still be refused rather than repaired. */ + mode.mode.vsync_end=mode.mode.vtotal+1; + assert(IS_ERR(dcp_panel_edid_snapshot(&d,&mode,1,&generation))); + return 0; +} +'''.replace('DTS_VALUES', ','.join(map(str, values))) + return unit, (("dimensions", str(output / 'j616s-panel.edid')), + ("identity", str(output / 'j616s-panel.edid'))) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py b/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py index 02e23247f50b2..466688aff3638 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py @@ -700,6 +700,8 @@ def publication(root): /* This fixture selects an external H17P sink, never desktop adaptive sync. */ static bool dcp_desktop_vrr_supported(struct apple_dcp *d) { assert(d->fw_compat != DCP_FIRMWARE_V_26_6_DESKTOP); return false; } +static void dcp_update_connector_dimensions(struct apple_dcp *d,struct drm_connector *c) +{ (void)c;assert(d->fw_compat != DCP_FIRMWARE_V_26_6_DESKTOP); } '''+firmware_lineage(root)+internal_inline(root,'dcp_fw_is_14_7')+r'''/* Off T6030 machines the direct crossbar output is not clocked separately. */ static int __attribute__((unused)) dcp_direct_crossbar_link(struct apple_dcp *d,bool up) { (void)d;(void)up;return 0; } ''' diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/run.py b/tools/testing/selftests/drivers/gpu/apple-dcp/run.py index 1b27400946e06..b78b93def55b2 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/run.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/run.py @@ -27,6 +27,7 @@ import accounting import idle import desktop +import j616s import rate import boot import m4 @@ -61,6 +62,7 @@ def main(): ('display', 'panel-brightness-takeover', display.takeover, ((),), ()), ('display', 'panel-mode-catalog', display.modes, ((),), ()), ('display', 'desktop-brightness-notifications', desktop.brightness_notifications, ((),), ()), + ('display', 'j616s-panel-identity', lambda r: j616s.identity(r, output), ((),), ('-Wno-unused-function', '-Wno-unused-const-variable')), ('display', 'desktop-adt-identity', desktop.adt_identity, ((),), ()), ('display', 'desktop-identity-trace-budget', desktop.identity_trace_budget, ((),), ()), ('display', 'desktop-identity-keys', desktop.identity_keys, ((),), ('-Wno-unused-function',)), From af0a664046fbb77c07828055e36d319427697c4f Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 18:33:13 +1100 Subject: [PATCH 188/388] drm/apple: label an unspecified panel serial without a dummy value Keep numeric serial zero when no panel identity source provides one, but use an explicit "unspecified" text descriptor instead of eight zeroes. Both remain non-null to userspace, and edid-decode no longer warns about a dummy serial on the generic J616s fallback. Do not invent an identity. Assisted-by: LLM Signed-off-by: Ryan Murray --- Documentation/gpu/apple-dcp-t6041-panel.rst | 2 +- drivers/gpu/drm/apple/dcp-display-metadata.c | 5 ++++- tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py | 2 +- tools/testing/selftests/drivers/gpu/apple-dcp/j616s.py | 1 + 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Documentation/gpu/apple-dcp-t6041-panel.rst b/Documentation/gpu/apple-dcp-t6041-panel.rst index 3e0d107fa7d1b..bdf15e644d68f 100644 --- a/Documentation/gpu/apple-dcp-t6041-panel.rst +++ b/Documentation/gpu/apple-dcp-t6041-panel.rst @@ -129,7 +129,7 @@ or prevent the generic fallback from being published. If the ADT or its identity fields are absent, the desktop J616 fallback is manufacturer APP, product 0616, name Apple Panel and unspecified numeric -serial zero, represented by the non-empty serial descriptor ``00000000``. +serial zero, represented by the non-empty serial descriptor ``unspecified``. This generic identity does not claim to identify an individual panel. The preferred representable timing and range limits come from the retained DCP TimingElements catalog. Exact mode enumeration continues to use that catalog. diff --git a/drivers/gpu/drm/apple/dcp-display-metadata.c b/drivers/gpu/drm/apple/dcp-display-metadata.c index c5921bf67cfa9..729eb58002f21 100644 --- a/drivers/gpu/drm/apple/dcp-display-metadata.c +++ b/drivers/gpu/drm/apple/dcp-display-metadata.c @@ -335,7 +335,10 @@ const struct drm_edid *dcp_panel_edid(const struct dcp_display_metadata *metadat metadata->name[0] ? metadata->name : name); if (ret) return ERR_PTR(ret); - snprintf(serial, sizeof(serial), "%08x", metadata->serial); + if (metadata->serial) + snprintf(serial, sizeof(serial), "%08x", metadata->serial); + else + snprintf(serial, sizeof(serial), "unspecified"); ret = dcp_metadata_text(&block.detailed_timings[2], EDID_DETAIL_MONITOR_SERIAL, metadata->serial_text[0] ? metadata->serial_text : serial); if (ret) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py index c5e655ee46da7..60b903f85d22f 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py @@ -464,7 +464,7 @@ def panel_identity(root, output=None): const struct drm_edid *minimal_edid=dcp_panel_edid_snapshot(&d,modes,3,&generation); assert(!IS_ERR(minimal_edid) && drm_edid_valid(minimal_edid)); const u8 *m=(const void *)&minimal_edid->raw; - assert(!memcmp(m+72+5,"TST 002a",8) && !memcmp(m+90+5,"00000000",8)); + assert(!memcmp(m+72+5,"TST 002a",8) && !memcmp(m+90+5,"unspecified",11)); drm_edid_free(minimal_edid); } /* Null and unrelated setters preserve the existing ACK and identity. */ diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/j616s.py b/tools/testing/selftests/drivers/gpu/apple-dcp/j616s.py index 3d903694e9e5c..45af01294a55e 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/j616s.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/j616s.py @@ -84,6 +84,7 @@ def identity(root, output): assert(!memcmp(&mode,&saved,sizeof(mode)) && generation==0); const u8 *raw=(const u8 *)&edid->raw; assert(!memcmp(raw+77,"Apple Panel",11)); + assert(!memcmp(raw+95,"unspecified",11)); assert((raw[66] | (raw[68]>>4)<<8)==345); assert((raw[67] | (raw[68]&15)<<8)==216); FILE *file=fopen(argv[2],"wb");assert(file); From dbc1bbb9084f7063aa9be318975d23b04b6f3d9c Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 22:17:11 +1100 Subject: [PATCH 189/388] selftests: asahi-m4: exercise the retained timestamp CPU mapping The shared timestamp writer delegates to TimestampVmap after the G17 mapping reuse change. Compile that production helper in the host test instead of providing a Map without write(), so the stores, cache clean and overlapping-destination checks run again. Assisted-by: LLM Signed-off-by: Ryan Murray --- .../drivers/gpu/asahi-m4/test-timestamps.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tools/testing/selftests/drivers/gpu/asahi-m4/test-timestamps.py b/tools/testing/selftests/drivers/gpu/asahi-m4/test-timestamps.py index 842ca85c090fb..9a159e849e86b 100755 --- a/tools/testing/selftests/drivers/gpu/asahi-m4/test-timestamps.py +++ b/tools/testing/selftests/drivers/gpu/asahi-m4/test-timestamps.py @@ -24,6 +24,7 @@ def function(source, marker): poll = (backend / 'g16/poll.rs').read_text() cache_policy = function((backend / 'gem.rs').read_text(), 'pub(crate) fn is_writeback(') store = function(mmu, 'pub(crate) fn write(&self, value: u64)') +vmap_store = function(mmu, 'pub(crate) fn write(&self, offset: usize, value: u64)') write = function(shared, 'fn write(&self, ticks: u64, frequency_hz: u64)') checked_offset = function(shared, 'fn checked_offset(') publish = function(shared, 'pub(crate) fn publish(&self, ticks: [u64; 4], frequency_hz: u64)') @@ -50,7 +51,12 @@ def function(source, marker): unsafe impl Send for Map {} unsafe impl Sync for Map {} struct View(*mut u8);impl View{fn as_mut_ptr(&self)->*mut u8{self.0}} impl Map{fn get(&self)->View{View(self.backing.words.as_ptr().cast_mut().cast())}fn owner(&self)->&Backing{&self.backing}} -struct TimestampMapping{mapping:Map,offset:usize} +struct TimestampVmap(Map); +impl TimestampVmap { + fn size(&self)->usize{core::mem::size_of_val(&self.0.backing.words)} + __VMAP_STORE__ +} +struct TimestampMapping{mapping:TimestampVmap,offset:usize} impl TimestampMapping { __STORE__ } __CHECKED_OFFSET__ struct Destination{cpu:TimestampMapping} @@ -63,7 +69,7 @@ def function(source, marker): fn backing()->Arc{backing_with_flags(0)} fn backing_with_flags(flags:u32)->Arc{Arc::new(Backing{flags,words:[const{AtomicU64::new(u64::MAX)};4],timestamp_lock:std::sync::Mutex::new(())})} fn destination(memory:&Arc,offset:usize)->Destination{ - Destination{cpu:TimestampMapping{mapping:Map{backing:memory.clone(),addr:memory.words.as_ptr().cast_mut().cast()},offset}} + Destination{cpu:TimestampMapping{mapping:TimestampVmap(Map{backing:memory.clone(),addr:memory.words.as_ptr().cast_mut().cast()}),offset}} } fn outputs(memory:&Arc)->Destinations{ Destinations(core::array::from_fn(|i|Some(destination(memory,i*8)))) @@ -191,7 +197,7 @@ def function(source, marker): assert_eq!(firmware().publish_timestamps(&flight,&claims),Err(EINVAL)); assert_eq!(memory.words[0].load(Ordering::Relaxed),u64::MAX); } -'''.replace('__CHECKED_OFFSET__', checked_offset).replace('__STORE__', store).replace('__CACHE_POLICY__', cache_policy).replace('__WRITE__', write).replace('__PUBLISH__', publish).replace('__RETIRE__', retire.replace('fn publish_timestamps', 'pub(crate) fn publish_timestamps')) +'''.replace('__CHECKED_OFFSET__', checked_offset).replace('__STORE__', store).replace('__VMAP_STORE__', vmap_store.replace('.timestamp_lock.lock()', '.timestamp_lock.lock().unwrap()')).replace('__CACHE_POLICY__', cache_policy).replace('__WRITE__', write).replace('__PUBLISH__', publish).replace('__RETIRE__', retire.replace('fn publish_timestamps', 'pub(crate) fn publish_timestamps')) path = out / 'timestamps-host.rs' path.write_text(source) subprocess.run(rustc() + ['--edition=2021', '--test', str(path), '-o', str(out / 'timestamps-host')], check=True) From 029698013e0a32c7c6f76542bb6487c06ac1a91b Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 22:20:06 +1100 Subject: [PATCH 190/388] tools/asahi: follow shared compute ownership and release notifications Update the host adapters for the shared availability notifications and compute source loans introduced by the G17 submission changes. Exercise the production ReaderHint predicate and verify a loan prevents sharing, reuse and rearming until its last reader leaves. Keep the close, recovery, growth and first-publication negative controls. Point the sharing mutations at the current shareable predicate so both retirement and kill guards are still tested. Assisted-by: LLM Signed-off-by: Ryan Murray --- tools/asahi/g17-capacity-fault-tests.py | 29 ++++++++++++++++---- tools/asahi/g17-compute-constructor-tests.py | 2 +- tools/asahi/g17-growth-gate-tests.py | 1 + tools/asahi/g17-uninstalled-cleanup-tests.py | 5 ++-- 4 files changed, 28 insertions(+), 9 deletions(-) diff --git a/tools/asahi/g17-capacity-fault-tests.py b/tools/asahi/g17-capacity-fault-tests.py index 09a70caaaccb4..2d0e755b5b01f 100644 --- a/tools/asahi/g17-capacity-fault-tests.py +++ b/tools/asahi/g17-capacity-fault-tests.py @@ -30,7 +30,7 @@ def fixture(): methods = '\n'.join(item(source, sig) for sig in [ 'pub(crate) fn release_owner(', 'pub(crate) fn reusable(', 'pub(crate) fn recycle_quarantined_idle(', 'pub(crate) fn recycle_killed(', - 'fn recycle_settled(', 'fn release_witnessed_inner(', 'pub(crate) fn share_idle(', 'pub(crate) fn can_rearm(', 'pub(crate) fn recycled_pending(']) + 'fn recycle_settled(', 'fn release_witnessed_inner(', 'pub(crate) fn share_idle(', 'pub(crate) fn shareable(', 'pub(crate) fn can_rearm(', 'pub(crate) fn recycled_pending(']) exit_source = (ROOT / 'queue/compute/exit.rs').read_text() methods += '\n' + item(exit_source, 'pub(crate) fn settle_exit(').replace('super::Previous', 'Previous').replace('crate::g17::qos::Owner', 'qos::Owner') drain = item((ROOT / 'mod.rs').read_text(), 'fn drain_reclaims(') @@ -48,6 +48,7 @@ def fixture(): fn now_ns() -> u64 { 123 } struct Context { id: u64, live: Arc, killed: AtomicU32 } impl Context { + fn note_compute_binding(&self, _: bool) {} fn new(id: u64, live: &Arc) -> Arc { live.fetch_add(1, Ordering::SeqCst); Arc::new(Self { id, live: live.clone(), killed: AtomicU32::new(1) }) @@ -55,7 +56,7 @@ def fixture(): } impl Drop for Context { fn drop(&mut self) { self.live.fetch_sub(1, Ordering::SeqCst); } } struct Binding { context: Arc } -''' + previous + r''' +''' + previous + '\n' + item((ROOT / 'sync_input.rs').read_text(), 'pub(super) struct ReaderHint').replace('pub(super) ', '') + '\nimpl ReaderHint {\n' + item((ROOT / 'sync_input.rs').read_text(), 'pub(super) fn has_readers(').replace('pub(super) ', '') + '\n}\n' + r''' #[derive(Clone, Copy, PartialEq)] enum PoolPhase { Populated, ReleaseSent(u32), Vacant } #[derive(Clone, Copy)] struct Ticket { owner: u64, item_count: u32, descriptor: u64, item_producer: u32, kick: u64, @@ -107,6 +108,7 @@ def fixture(): retired_by_teardown: bool, spared_quarantine: bool, spared_deferred: bool, retire_pending: bool, retirement_proved: bool, replay_retirement: bool, replay_error: Option, replay_prepared: bool, submitted: u32, kick: Kick, + config_cache: Option<()>, source_hint: ReaderHint, source_readers: AtomicU32, claims: u32, } impl Queue { fn valid_ticket(&self, _: &Active, _: &Ticket) -> Result { Ok(()) } @@ -126,6 +128,7 @@ def fixture(): retired_by_teardown: false, spared_quarantine: false, spared_deferred: false, retire_pending: false, retirement_proved: false, replay_retirement: false, replay_error: None, replay_prepared: false, submitted: 0, kick: Kick, + config_cache: None, source_hint: ReaderHint { possible: false }, source_readers: AtomicU32::new(0), claims: 0, } } fn fault(live: &Arc) -> Queue { @@ -146,20 +149,34 @@ def fixture(): impl Reclaims { fn take_one(&mut self) -> Option { self.0.pop() } } struct Mutex(std::sync::Mutex); impl Mutex { fn lock(&self) -> std::sync::MutexGuard<'_, T> { self.0.lock().unwrap() } } -struct Shared { reclaim: Mutex, wakes: AtomicUsize } +struct Available(AtomicUsize); impl Available {fn notify(&self){self.0.fetch_add(1, Ordering::SeqCst);}} +struct Shared { reclaim: Mutex, wakes: AtomicUsize, queues: Available } impl Shared { fn queue_events(&self) { self.wakes.fetch_add(1, Ordering::SeqCst); } ''' + drain + r''' } fn main() { let finished = Arc::new(AtomicUsize::new(0)); - let shared = Arc::new(Shared { reclaim: Mutex(std::sync::Mutex::new(Reclaims(vec![Reclaim(finished.clone())]))), wakes: AtomicUsize::new(0) }); + let shared = Arc::new(Shared { reclaim: Mutex(std::sync::Mutex::new(Reclaims(vec![Reclaim(finished.clone())]))), wakes: AtomicUsize::new(0), queues: Available(AtomicUsize::new(0)) }); shared.drain_reclaims(); assert_eq!(finished.load(Ordering::SeqCst), 1); assert_eq!(shared.wakes.load(Ordering::SeqCst), 1); shared.drain_reclaims(); assert_eq!(shared.wakes.load(Ordering::SeqCst), 1); + assert_eq!(shared.queues.0.load(Ordering::SeqCst), 1); let live = Arc::new(AtomicUsize::new(0)); + let mut borrowed = idle(&live); + borrowed.retirement_ready = true; + borrowed.source_hint.possible = true; + borrowed.source_readers.store(1, Ordering::Release); + assert!(!borrowed.shareable()); + borrowed.release_owner().unwrap(); + assert!(!borrowed.reusable()); + borrowed.pool_phase = PoolPhase::Vacant; + assert!(!borrowed.can_rearm()); + borrowed.source_readers.store(0, Ordering::Release); + assert!(borrowed.reusable() && borrowed.can_rearm()); + drop(borrowed); for _ in 0..512 { // Rebound detached queue; lost VM / killed publisher / allocation error // before the first new command. No future completion exists to free it. @@ -266,8 +283,8 @@ def main(): ('missing-fault-release', source.replace(fault_fix, '', 1), False), ('missing-kill-release', source.replace('Previous::retire_witnessed(&mut self.previous);\n self.exit', 'self.exit'), False), ('missing-reclaim-wake', source.replace('self.queue_events();', ''), False), - ('share-before-retirement', source.replace('|| !self.retirement_ready', ''), False), - ('share-during-kill', source.replace('|| self.exit.is_some() || self.active.len', '|| self.active.len'), False)] + ('share-before-retirement', source.replace('&& self.owner.is_some() && self.retirement_ready', '&& self.owner.is_some()'), False), + ('share-during-kill', source.replace('&& self.exit.is_none() && self.active.len', '&& self.active.len'), False)] with tempfile.TemporaryDirectory(prefix='g17-capacity-') as tmp: for name, rust, passes in controls: path = Path(tmp) / (name + '.rs') diff --git a/tools/asahi/g17-compute-constructor-tests.py b/tools/asahi/g17-compute-constructor-tests.py index f4fdcf2c68222..8c0c4dc772885 100644 --- a/tools/asahi/g17-compute-constructor-tests.py +++ b/tools/asahi/g17-compute-constructor-tests.py @@ -50,7 +50,7 @@ def fixture(): fn notify_all(&self){} } struct Shared{preparations:Gate,state:State,changed:Changed,crashed:AtomicBool} -impl Shared{fn drm(&self)->Result<()>{Ok(())}} +impl Shared{fn drm(&self)->Result<()>{Ok(())} fn note_changed(&self){self.changed.notify_all();}} struct Context;impl Context{fn vm(&self)->&(){&()}} struct Backend{shared:Shared,context:Context} struct Allocator<'a>{dev:&'a (),uat:&'a ()} diff --git a/tools/asahi/g17-growth-gate-tests.py b/tools/asahi/g17-growth-gate-tests.py index d6ad5f9beef30..8b4285782f25b 100644 --- a/tools/asahi/g17-growth-gate-tests.py +++ b/tools/asahi/g17-growth-gate-tests.py @@ -38,6 +38,7 @@ def fixture(): struct Gate{state:AtomicU64,wait:Mutex,changed:CondVar} struct Lease{gate:Arc,epoch:u64} impl Gate { + fn wake_waiters(&self) {} ''' + methods + '\n}\n' + item(gate,'impl Drop for Lease') + r''' struct Backend {gate:Arc,ready:mpsc::Sender<()>,proceed:StdMutex>} impl Backend { diff --git a/tools/asahi/g17-uninstalled-cleanup-tests.py b/tools/asahi/g17-uninstalled-cleanup-tests.py index 33d2f252eb81c..d74f67aba6267 100644 --- a/tools/asahi/g17-uninstalled-cleanup-tests.py +++ b/tools/asahi/g17-uninstalled-cleanup-tests.py @@ -61,7 +61,8 @@ def fixture(): impl Drop for Guard<'_>{fn drop(&mut self){LOCKED.store(false,Ordering::SeqCst);}} struct State(Mutex>>); impl State{fn lock(&self)->Guard<'_>{let g=self.0.lock().unwrap();assert!(!LOCKED.swap(true,Ordering::SeqCst));Guard(g)}} -struct Shared{state:State} +struct Available; impl Available {fn notify(&self){}} +struct Shared{state:State,channels:Available} impl Shared{fn queue_events(&self){}} struct Backend{shared:Arc} impl Backend { @@ -76,7 +77,7 @@ def fixture(): } } fn main(){ - let b=Backend{shared:Arc::new(Shared{state:State(Mutex::new(Some(Box::new(Firmware{queues:Registry}))))})}; + let b=Backend{shared:Arc::new(Shared{state:State(Mutex::new(Some(Box::new(Firmware{queues:Registry})))),channels:Available})}; b.finish_uninstalled_compute(None).unwrap(); for _ in 0..512 { MAPPED.store(true,Ordering::SeqCst);RESERVED.store(true,Ordering::SeqCst); From 989406b403571dc0e7429fdd1433c8fd72ca12a8 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 22:20:53 +1100 Subject: [PATCH 191/388] drm/asahi: test the complete T6040 performance selector ladder Use valid sixteen-slot handoffs in the calibration KUnit fixture, with both eleven-state maps ending at raw OPP 15. Check the highest state's second voltage lane as well as the intermediate-state ordering and relative-frequency consistency. The former 0..10 fixture could not pass the production map reader and never exercised the final five raw operating points. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/asahi/g17/calibration.rs | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/calibration.rs b/drivers/gpu/drm/asahi/g17/calibration.rs index d973349d35d40..704a91f49b0c1 100644 --- a/drivers/gpu/drm/asahi/g17/calibration.rs +++ b/drivers/gpu/drm/asahi/g17/calibration.rs @@ -157,10 +157,10 @@ mod tests { fn chip() -> Calibration { Calibration { perf: PerfConfig { - states_a: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], - states_b: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + states_a: map([0, 1, 2, 3, 4, 6, 8, 10, 12, 14, 15, 0, 0, 0, 0, 0]).unwrap(), + states_b: map([0, 1, 2, 3, 4, 6, 7, 9, 11, 13, 15, 0, 0, 0, 0, 0]).unwrap(), relative_a: [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100], - relative_b: [0, 0, 11, 22, 33, 44, 55, 66, 77, 88, 100], + relative_b: [0, 0, 7, 14, 21, 35, 50, 64, 78, 92, 100], sram_min_mv: 810, }, leakage: [0; 56], @@ -188,14 +188,21 @@ mod tests { assert!(chip.check_cores(&[0x3ff, 0x3ff]).is_ok()); assert!(chip.check_cores(&[0x3ff, 0x1ff]).is_err()); assert!(chip.check_cores(&[0xfffff]).is_err()); - opps[5].volt_uv[1] = 0; + // Both eleven-state maps must reach raw OPP 15, including its second voltage lane. + assert_eq!(chip.perf.states_a[PERF_STATES - 1], 15); + assert_eq!(chip.perf.states_b[PERF_STATES - 1], 15); + opps[15].volt_uv[1] = 0; assert!(chip.check_opps(&opps).is_err()); - opps[5].volt_uv[1] = 800_000; - opps[5].freq_hz = opps[4].freq_hz; + opps[15].volt_uv[1] = 800_000; + assert!(chip.check_opps(&opps).is_ok()); + opps[6].volt_uv[1] = 0; + assert!(chip.check_opps(&opps).is_err()); + opps[6].volt_uv[1] = 800_000; + opps[6].freq_hz = opps[4].freq_hz; assert!(chip.check_opps(&opps).is_err()); - opps[5].freq_hz = 501_000_000; + opps[6].freq_hz = 601_000_000; assert!(chip.check_opps(&opps).is_ok()); - opps[5].freq_hz = 511_000_000; + opps[6].freq_hz = 611_000_000; assert!(chip.check_opps(&opps).is_err()); } From 61aca8fb8212c8479a9e4830164ac2acf17c24e4 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 22:22:05 +1100 Subject: [PATCH 192/388] drm/asahi: cover the T6040 firmware queue floor in QID tests Run both dynamic compute growth and the full logical queue budget checks with the 25G83 floor of 32, alongside the existing floors. This checks render headroom and firmware-owned QID exclusion on the M4 Pro layout. Bring the host resource runner forward to the shared reserve_for helper and current render reserve, retaining its capacity and stale-ID controls. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/asahi/g17/kick.rs | 4 ++-- tools/asahi/g17-resource-tests.py | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/kick.rs b/drivers/gpu/drm/asahi/g17/kick.rs index a1f8f70f597d4..0f13c4e148563 100644 --- a/drivers/gpu/drm/asahi/g17/kick.rs +++ b/drivers/gpu/drm/asahi/g17/kick.rs @@ -309,7 +309,7 @@ mod accounting_tests { #[test] fn dynamic_compute_growth_preserves_render_headroom_above_firmware_floor() { - for floor in [0, 4] { + for floor in [0, 4, 32] { let mut ids = KBox::init(QueueIds::new(floor), GFP_KERNEL).unwrap(); let queues = 2; let mut owner = 1; @@ -328,7 +328,7 @@ mod accounting_tests { #[test] fn full_logical_queue_budget_keeps_compute_floor_and_excludes_firmware_ids() { - for floor in [0, 4] { + for floor in [0, 4, 32] { let mut ids = KBox::init(QueueIds::new(floor), GFP_KERNEL).unwrap(); let queues = queue_limit(floor) as usize; for owner in 1..=COMPUTE_FLOOR as u64 { diff --git a/tools/asahi/g17-resource-tests.py b/tools/asahi/g17-resource-tests.py index 5f87d2ea9e0f5..cf93704966c33 100644 --- a/tools/asahi/g17-resource-tests.py +++ b/tools/asahi/g17-resource-tests.py @@ -134,7 +134,7 @@ def item(s,start): return s[at:end] s=(source/'kick.rs').read_text() fw=(source/'fw/kick.rs').read_text() - constants=re.search(r'const QID_MAX: [^;]+;',fw).group(0)+'\n'+'\n'.join(re.search(r'const '+name+r': [^;]+;',s).group(0) for name in ['QID_COUNT','COMPUTE_RENDER_RESERVE']) + constants=re.search(r'const QID_MAX: [^;]+;',fw).group(0)+'\n'+'\n'.join(re.search(r'const '+name+r': [^;]+;',s).group(0) for name in ['QID_COUNT','RENDER_HEADROOM','FULL_RENDER_RESERVE']) header='''#![allow(dead_code, unused_macros)] type Result = std::result::Result; mod g17 { pub mod capacity { @@ -148,9 +148,9 @@ def item(s,start): #[derive(Clone,Copy,PartialEq,Eq,Debug)] enum DataMaster {Compute,Tiling,Fragment} #[derive(Clone,Copy,PartialEq,Eq,Debug)] struct Id {qid:u8,owner:u64,engine:DataMaster} #[derive(Clone,Copy)] struct Identity {id:Id,published:bool} - '''+constants+'\n'+item(s,'pub(crate) struct QueueIds')+'\nimpl QueueIds {\n'+''.join(item(s,'pub(crate) fn '+name+'(')+'\n' for name in ['reserve_initial_render','reserve','publish','can_cancel_unpublished','cancel_unpublished'])+'}\n' + '''+constants+'\n'+item(s,'const fn render_reserve(')+'\n'+item(s,'pub(crate) struct QueueIds')+'\nimpl QueueIds {\n'+''.join(item(s,'pub(crate) fn '+name+'(')+'\n' for name in ['reserve_initial_render','reserve','reserve_for','publish','can_cancel_unpublished','cancel_unpublished'])+''.join(item(s, sig)+'\n' for sig in ['fn render_ids(', 'fn compute_room('])+'}\n' test=''' - fn fresh()->QueueIds {QueueIds {entries:[None;QID_COUNT],initial_render:[None;2],publication:0,leased:0,floor:0}} + fn fresh()->QueueIds {QueueIds {entries:[None;QID_COUNT],initial_render:[None;2],leased:0,floor:0}} fn main(){ for floor in [0,32] { for render in [0,2,16,32,80,96,112,126].into_iter().filter(|render|*render<=QID_COUNT-floor) { @@ -158,7 +158,7 @@ def item(s,start): for i in 0..render {q.reserve(i as u64+1,if i%2==0 {DataMaster::Tiling}else{DataMaster::Fragment},None).unwrap();} let mut compute=0; while q.reserve(1000+compute,DataMaster::Compute,None).is_ok(){compute+=1;} - assert_eq!(compute as usize,QID_COUNT-floor-render.max(COMPUTE_RENDER_RESERVE-floor)); + assert_eq!(compute as usize,QID_COUNT-floor-render.max(FULL_RENDER_RESERVE-floor)); assert!(q.entries[..floor].iter().all(Option::is_none)); } } @@ -195,8 +195,8 @@ def item(s,start): assert_eq!(q.entries[0].unwrap().id,protected); // Replacement must not consume the protected render minimum. let mut q=fresh();q.reserve_initial_render().unwrap(); - for owner in 3..=COMPUTE_RENDER_RESERVE as u64 {q.reserve(owner,DataMaster::Tiling,None).unwrap();} - for owner in 1000..1000+(QID_COUNT-COMPUTE_RENDER_RESERVE) as u64 {q.reserve(owner,DataMaster::Compute,None).unwrap();} + for owner in 3..=FULL_RENDER_RESERVE as u64 {q.reserve(owner,DataMaster::Tiling,None).unwrap();} + for owner in 1000..1000+(QID_COUNT-FULL_RENDER_RESERVE) as u64 {q.reserve(owner,DataMaster::Compute,None).unwrap();} assert_eq!(q.leased,QID_COUNT);assert_eq!(q.reserve(2000,DataMaster::Compute,None),Err(ENOSPC)); assert!(q.initial_render.iter().all(Option::is_some)); // Ordinary free capacity leaves the initialization reservations untouched. From 4d0010e5f2271262c51b6430ea2ea964c0d3fd3e Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 22:24:18 +1100 Subject: [PATCH 193/388] tools/asahi: update pool and scheduler-release fixture boundaries Follow the split reusable-binding, raw publication and domain-wait helpers when checking scheduler-release ordering. Keep the timeout check at the shared wait helper. Update growth and pool-admission adapters for source loans, reserve_for and the off-lock compute slot allocation boundary. Assisted-by: LLM Signed-off-by: Ryan Murray --- tools/asahi/g17-growth-tests.py | 8 +++++--- tools/asahi/g17-owner-release-order-tests.py | 9 +++++---- tools/asahi/g17-pool-release-tests.py | 11 +++++++---- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/tools/asahi/g17-growth-tests.py b/tools/asahi/g17-growth-tests.py index 1236f166876af..2c46ccbf6359a 100644 --- a/tools/asahi/g17-growth-tests.py +++ b/tools/asahi/g17-growth-tests.py @@ -23,7 +23,7 @@ def fixture(): const EIO:i32=-5;const EBUSY:i32=-16;const EINVAL:i32=-22; fn now_ns()->u64{123} struct Context {live:Arc} -impl Context {fn new(live:&Arc)->Arc{live.fetch_add(1,Ordering::SeqCst);Arc::new(Self{live:live.clone()})}} +impl Context {fn note_compute_binding(&self,_:bool){} fn new(live:&Arc)->Arc{live.fetch_add(1,Ordering::SeqCst);Arc::new(Self{live:live.clone()})}} impl Drop for Context {fn drop(&mut self){self.live.fetch_sub(1,Ordering::SeqCst);}} struct Binding {context:Arc,pool_alias:Option,descriptors:Option} struct Previous {context:Arc,binding:Option,publication:Option<(u64,u64)>} @@ -31,17 +31,19 @@ def fixture(): struct Fifo {len:usize} struct Graph; impl Graph {fn set_owner(&mut self,_:&Context)->Result{Ok(())}} +struct ReaderHint; impl ReaderHint {fn has_readers(&self,count:&AtomicU32)->bool{count.load(Ordering::Acquire)!=0}} struct Queue { owner:Option,installed:bool,growing:bool,growth_blocks:usize,free_list_generation:u64, pool_phase:PoolPhase,release_sent_ns:Option,pool:Pool,binding:Option,previous:Option,graph:Graph, active:Fifo,replays:Fifo,quarantined:bool,released:bool,retirement_ready:bool, retired_by_teardown:bool,released_at:u64,exit:Option<()>,needs_fresh_backing:bool, + source_hint:ReaderHint,source_readers:AtomicU32, } fn idle(live:&Arc)->Queue{Queue{owner:Some(1),installed:true,growing:false,growth_blocks:0, free_list_generation:0,pool_phase:PoolPhase::Populated,release_sent_ns:None,pool:Pool{pages:Some(Pages(10)),fail:false}, binding:Some(Binding{context:Context::new(live),pool_alias:Some(1),descriptors:Some(2)}),previous:None,graph:Graph, active:Fifo{len:0},replays:Fifo{len:0},quarantined:false,released:false,retirement_ready:true, - retired_by_teardown:false,released_at:0,exit:None,needs_fresh_backing:false}} + retired_by_teardown:false,released_at:0,exit:None,needs_fresh_backing:false,source_hint:ReaderHint,source_readers:AtomicU32::new(0)}} impl Queue { ''' + item(source,'pub(crate) fn release_owner(') + item(source,'pub(crate) fn reusable(') + '\n}\n' methods = '\n'.join(item(source, sig) for sig in ['pub(crate) fn begin_growth(', 'pub(crate) fn finish_growth(', 'pub(crate) fn growth_token(', 'pub(crate) fn abort_growth(', 'pub(crate) fn observe_vacancy(']) @@ -156,7 +158,7 @@ def fixture(): def main(): source = fixture() runtime = (ROOT / 'runtime.rs').read_text() - assert 'growth = queue.growth_token();' in runtime and 'queue.abort_growth(token)?' in runtime + assert 'growth = queue.growth_token();' in runtime and 'queue.abort_growth(token)' in runtime cases = [('fixed', source), ('missing-abort-transfer', source.replace('self.growing = false;\n self.growth_blocks = 0;\n self.release_owner().map(Some)', 'self.release_owner().map(Some)')), ('ignore-consumption', source.replace('if !crate::g17::freelist::control_consumed(consumer, producer, cursor)? {', 'if false {')), diff --git a/tools/asahi/g17-owner-release-order-tests.py b/tools/asahi/g17-owner-release-order-tests.py index fd0e2c4a80936..ad17fa4048726 100755 --- a/tools/asahi/g17-owner-release-order-tests.py +++ b/tools/asahi/g17-owner-release-order-tests.py @@ -64,16 +64,17 @@ def check_guards(): ordered(detach, '.park();', 'parked.release_consumed(context.scheduler_va());') runtime = (ROOT / 'runtime.rs').read_text() # A compute queue does not change owner while its scheduler is released. - ordered(item(runtime, 'pub(super) fn rebind_compute('), '.filter(|queue| queue.reusable())', + ordered(item(runtime, 'fn bind_reusable('), '.filter(|queue| queue.reusable())', 'teardown.releasing(bound)', 'continue;', 'queue.bind_owner(owner, context.clone())?;') ordered(item(runtime, 'pub(super) fn bind_rearmed('), 'teardown.releasing(bound)', 'return Err(EAGAIN);', 'queue.bind_owner(') - ordered(item(runtime, 'fn publish_compute('), '.pending_switch_from()', 'queues.teardown.releasing(previous)', + ordered(item(runtime, 'fn publish_compute_raw('), '.pending_switch_from()', 'queues.teardown.releasing(previous)', 'return Err(EAGAIN);', 'queue.publish(') # A busy pair (for example one whose scheduler release is still being consumed) # kicks the event worker, which observes the release and parks the pair. - ordered(item(submit, 'fn wait_render_busy('), 'WORK_RING_WAIT_NS', 'return Err(ETIMEDOUT);', - 'self.shared.queue_events();', 'fsleep(') + ordered(item(submit, 'fn wait_render_busy('), 'WORK_RING_WAIT_NS', + 'self.shared.queue_events();', 'self.wait_domain(') + ordered(item(submit, 'fn wait_domain('), 'if remaining == 0', 'return Err(ETIMEDOUT);', 'domain.block(') for name, path in [('take_pair', 'runtime/render.rs'), ('take_render_observation', 'runtime/render/observe.rs')]: body = item((ROOT / path).read_text(), f'fn {name}(') ordered(body, 'entry.borrowed_switch = pair.switch_in_flight();', 'entry.pair.take()') diff --git a/tools/asahi/g17-pool-release-tests.py b/tools/asahi/g17-pool-release-tests.py index b7c715727acab..2ad4c6901cba4 100644 --- a/tools/asahi/g17-pool-release-tests.py +++ b/tools/asahi/g17-pool-release-tests.py @@ -31,7 +31,7 @@ def fixture(): predicate = '.any(|queue| queue.retirement_polling() || queue.pool_release_pending())' assert events.count(predicate) == 1 methods = '\n'.join(item(runtime, name) for name in [ - 'fn compute_admission_error(', 'pub(super) fn reserve_compute(']).replace('pub(super) ', '') + 'fn compute_admission_error(', 'pub(super) fn reserve_compute(']).replace('pub(super) ', '').replace('queue: ComputeSlot::new()?', 'queue: None') pending = item(compute, 'pub(crate) fn pool_release_pending(') + '\n' + \ item(compute, 'pub(crate) fn release_settling(') return r''' @@ -72,10 +72,12 @@ def fixture(): struct Id { qid: u8 } #[derive(Clone, Copy, Debug, PartialEq, Eq)] struct ComputeReservation { slot: usize, id: Id, owner: u64 } -struct ComputeEntry { reservation: ComputeReservation, queue: Option> } +type ComputeSlot = Option>; +struct ComputeEntry { reservation: ComputeReservation, queue: ComputeSlot } +struct Admission; impl Admission {fn queues(&self)->u32{4}} struct QueueIds { free: bool, fail: Option } impl QueueIds { - fn reserve(&mut self, _: u64, _: DataMaster, preferred: Option) -> Result { + fn reserve_for(&mut self, _: u64, _: DataMaster, preferred: Option, _: usize) -> Result { if let Some(error) = self.fail { return Err(error); } if !self.free { return Err(ENOSPC); } self.free = false; @@ -93,6 +95,7 @@ def fixture(): compute: Vec>, identities: Identities, qids: QueueIds, + admission: Admission, } impl Registry { METHODS @@ -109,7 +112,7 @@ def fixture(): })); } entries.push(None); - Registry { compute: entries, identities: Identities { next: 100 }, qids: QueueIds { free, fail: None } } + Registry { compute: entries, identities: Identities { next: 100 }, qids: QueueIds { free, fail: None }, admission: Admission } } fn main() { // Captured failure: four idle released queues, full physical namespace, From d24f314570992103cedad0fa3092ec78b199356c Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 22:28:05 +1100 Subject: [PATCH 194/388] tools/asahi: run compute deadline fixtures through release waits Adapt the controlled-clock tests to the progress-domain waits and bound compute fast-path fallback. Keep one admission budget across retries, growth and work-state waits, with cancellation and VM-status checks. Replace the obsolete single-check mutation with a disabled shared-deadline control: release waits now perform additional admission checks. Retain negative controls for renewed budgets, gate wake storms and alias lifetime. Assisted-by: LLM Signed-off-by: Ryan Murray --- tools/asahi/g17-compute-deadline-tests.py | 31 +++++++++++++------ .../g17-compute-growth-deadline-tests.py | 12 +++++-- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/tools/asahi/g17-compute-deadline-tests.py b/tools/asahi/g17-compute-deadline-tests.py index eca8b73c5b12c..88c89f6857799 100644 --- a/tools/asahi/g17-compute-deadline-tests.py +++ b/tools/asahi/g17-compute-deadline-tests.py @@ -49,7 +49,7 @@ def fixture(): static MODE:AtomicUsize=AtomicUsize::new(0);static CANCEL:AtomicU64=AtomicU64::new(u64::MAX); static STATUS:AtomicU64=AtomicU64::new(u64::MAX);static CLOSED:AtomicBool=AtomicBool::new(false); fn now_ns()->u64{NOW.load(Ordering::SeqCst)} -fn advance(ns:u64){NOW.fetch_add(ns,Ordering::SeqCst);} +fn advance(ns:u64){NOW.fetch_add(ns,Ordering::SeqCst);assert!(now_ns()<=6_000_000_000,"admission budget exceeded");} mod kernel{pub mod time{ pub fn msecs_to_jiffies(ms:u32)->u32{ms} pub struct Delta(pub u64);impl Delta{pub fn from_millis(ms:u64)->Self{Self(ms*1_000_000)}pub fn from_micros(us:i64)->Self{Self(us as u64*1_000)}} @@ -76,20 +76,33 @@ def fixture(): impl Packet{fn check_dependencies(&self)->Result{Ok(())}fn cancel_requested(&self)->bool{now_ns()>=CANCEL.load(Ordering::SeqCst)}fn is_published(&self)->bool{false}} struct DeferredBatch;impl DeferredBatch{fn publication()->Result{Ok(Self)}fn finish(&mut self){}} struct Recovery;impl Recovery{fn pending(&self)->bool{MODE.load(Ordering::SeqCst)==1}} -struct Memory;impl Memory{fn recovery_state(_:&())->Result{Ok(0)}} +struct Init;impl Init{fn compute_vm_cached(&self,_:&())->bool{true}} +struct Context;impl Context{fn vm(&self)->&(){&()}} +struct Memory;impl Memory{fn recovery_state(_:&Init)->Result{Ok(0)}} struct Queues; impl Queues{ + fn compute_bound(&self,_:u64,_:&Context,_:usize)->Result{Ok(MODE.load(Ordering::SeqCst)!=0)} fn compute(&self,_:u64)->Option<()>{if MODE.load(Ordering::SeqCst)==0{None}else{Some(())}} fn render_prefix(&self,_:u64,_:&Arc)->Result>{Ok(vec![])} - fn release_compute_witness(&self,_:&(),_:u64,_:&mut DeferredBatch)->Result{Ok(MODE.load(Ordering::SeqCst)==2)} + fn release_compute_witness(&self,_:&Init,_:&(),_:u64,_:&mut DeferredBatch)->Result{Ok(MODE.load(Ordering::SeqCst)==2)} } -struct Firmware{recovery:Recovery,init:(),queues:Queues} +struct Firmware{recovery:Recovery,init:Init,queues:Queues,port:()} impl Firmware{fn replay_compute(&mut self,_:&mut DeferredBatch)->Result{Ok(())}fn publish_compute(&mut self,_:u64,_:Arc,_:&[()],_:&mut DeferredBatch)->Result{Err(EAGAIN)}} struct State(Mutex>>);impl State{fn lock(&self)->MutexGuard<'_,Option>>{self.0.lock().unwrap()}} -struct Shared{preparations:Arc,state:State,crashed:AtomicBool,active_cores:u32}impl Shared{fn queue_events(&self){}} -struct Backend{shared:Shared,owner:u64} +struct Available;impl Available{fn observe(&self)->u64{now_ns()}} +struct Shared{preparations:Arc,state:State,crashed:AtomicBool,active_cores:u32,progress:Available}impl Shared{fn queue_events(&self){}} +struct ComputeDemand;impl ComputeDemand{fn new(_:&Shared)->Self{Self}} +struct Backend{shared:Shared,owner:u64,context:Context} fn required_blocks(_:u32,_:u16)->Result{Ok(1)} impl Backend{ + fn compute_direct(&self,_:&Arc,_:usize,_:&mut DeferredBatch)->Option{None} + fn sources_published(&self,_:&Arc)->Result{Ok(true)} + fn remember_direct(&self,_:&Queues){} + fn wait_release(&self,_:&Available,_:u64,admission:&Admission<'_>)->Result{ + let _=admission.slice_ms()?; + advance(1_000_000); + admission.check() + } fn admission_open(&self)->Result{Ok(())} // The real ensure_compute forwards the original context into this allocation // boundary. Each success loses its binding before the publication mutex. @@ -99,7 +112,7 @@ def fixture(): ''' + ensure + '\n' + packet + r''' } fn main(){ - let b=Backend{owner:1,shared:Shared{active_cores:1,preparations:Arc::new(Gate{wait:Wait,changed:Changed}),state:State(Mutex::new(Some(Box::new(Firmware{recovery:Recovery,init:(),queues:Queues})))),crashed:AtomicBool::new(false)}}; + let b=Backend{owner:1,context:Context,shared:Shared{active_cores:1,progress:Available,preparations:Arc::new(Gate{wait:Wait,changed:Changed}),state:State(Mutex::new(Some(Box::new(Firmware{recovery:Recovery,init:Init,queues:Queues,port:()})))),crashed:AtomicBool::new(false)}}; let p=Arc::new(Packet{command:Validated::Compute{scratch:Scratch},completion:Completion}); for mode in 0..4 { MODE.store(mode,Ordering::SeqCst);NOW.store(0,Ordering::SeqCst);CALLS.store(0,Ordering::SeqCst); @@ -144,8 +157,8 @@ def main(): source = fixture() mutants = [ ('fixed', source, ''), - ('renew-on-binding-loss', source.replace('let admission = Admission::new(deadline, &check);\n loop {', 'loop {\n let admission = Admission::new(now_ns().saturating_add(WORK_RING_WAIT_NS), &check);'), 'one admission budget'), - ('omit-nested-check', source.replace(' admission.check()?;\n let pending', ' let pending'), 'nested unavailable-capacity'), + ('renew-on-binding-loss', source.replace('let admission = Admission::new(deadline, &check);\n let progress = &self.shared.progress;\n loop {', 'let progress = &self.shared.progress;\n loop {\n let admission = Admission::new(now_ns().saturating_add(WORK_RING_WAIT_NS), &check);'), 'admission budget exceeded'), + ('ignore-deadline', source.replace('if now_ns() >= self.deadline', 'if false'), 'admission budget exceeded'), ('omit-gate-check', source.replace(' admission.check()?;\n match self.try_enter()', ' match self.try_enter()').replace('admission.slice_ms()?', '10'), 'gate wake storm'), ] # Bind the old unbounded mutant with the review's eventual cancellation. diff --git a/tools/asahi/g17-compute-growth-deadline-tests.py b/tools/asahi/g17-compute-growth-deadline-tests.py index 1e1e715de8377..3c652a96df759 100644 --- a/tools/asahi/g17-compute-growth-deadline-tests.py +++ b/tools/asahi/g17-compute-growth-deadline-tests.py @@ -50,7 +50,8 @@ def fixture(): impl Lease{fn is_current(&self)->bool{true}} struct State(Mutex>>); impl State{fn lock(&self)->MutexGuard<'_,Option>>{self.0.lock().unwrap()}} -struct Shared{preparations:Gate,state:State,crashed:AtomicBool} +struct Available;impl Available{fn observe(&self)->u64{now_ns()}fn notify(&self){}} +struct Shared{preparations:Gate,state:State,crashed:AtomicBool,progress:Available,channels:Available} impl Shared{fn drm(&self)->Result<()>{Ok(())}fn queue_events(&self){}} struct Context;impl Context{fn vm(&self)->&(){&()}} struct Backend{shared:Shared,context:Context,owner:u64} @@ -85,12 +86,17 @@ def fixture(): impl Firmware{fn scan_compute(&mut self,_:Option<()>,_:&mut DeferredBatch)->Result{Ok(())}} struct DeferredBatch;impl DeferredBatch{fn publication()->Result{Ok(Self)}fn finish(&mut self){}} impl Backend{ + fn wait_release(&self,_:&Available,_:u64,admission:&Admission<'_>)->Result{ + let _=admission.slice_ms()?; + kernel::time::delay::fsleep(kernel::time::Delta::from_millis(1)); + admission.check() + } ''' + methods + r''' } fn main(){ for fresh in [false,true]{for cancel in [false,true]{ NOW.store(0,Ordering::SeqCst);ABORTED.store(false,Ordering::SeqCst);RESERVED.store(true,Ordering::SeqCst); - let b=Backend{owner:1,context:Context,shared:Shared{preparations:Gate,crashed:AtomicBool::new(false),state:State(Mutex::new(Some(Box::new(Firmware{uat:(),init:(),primary:Primary,queues:Registry{compute:vec![Some(Entry{reservation:ComputeReservation,queue:Some(Box::new(Queue{fresh}))})]}}))))}}; + let b=Backend{owner:1,context:Context,shared:Shared{preparations:Gate,progress:Available,channels:Available,crashed:AtomicBool::new(false),state:State(Mutex::new(Some(Box::new(Firmware{uat:(),init:(),primary:Primary,queues:Registry{compute:vec![Some(Entry{reservation:ComputeReservation,queue:Some(Box::new(Queue{fresh}))})]}}))))}}; let check=||{if cancel&&now_ns()>=10_000_000{Err(ECANCELED)}else{Ok(())}}; let admission=Admission::new(20_000_000,&check); assert_eq!(b.grow_compute(42,&admission),Err(if cancel{ECANCELED}else{EBUSY}),"growth must propagate cancel/deadline"); @@ -106,7 +112,7 @@ def fixture(): def main(): source = fixture() mutants = [('fixed', source, ''), - ('omit-growth-check', source.replace(' admission.check()?;\n let firmware', ' let firmware'), 'growth admission exceeded'), + ('ignore-shared-deadline', source.replace('if now_ns() >= self.deadline', 'if false').replace('if remaining == 0 { return Err(EBUSY); }', ''), 'growth admission exceeded'), ('keep-growth-alias', source.replace(' drop(alias.take());', ''), 'growth aliases must precede')] with tempfile.TemporaryDirectory(prefix='g17-compute-growth-') as tmp: for name, code, expected in mutants: From 392b394d4dbb43e3d7f29bad70263e0f93ab94b9 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 22:29:23 +1100 Subject: [PATCH 195/388] tools/asahi: test DART range flush dispatch and error continuation Update the host DART adapter for the firmware mirror/handoff dispatch, DCP domain flush and managed-stream quarantine. Verify that a failed page-table publication still issues invalidation, its first error wins, and a power failure does not skip a second reachable DART. Keep the device-address offset, locked/unlocked and legacy controls. Assisted-by: LLM Signed-off-by: Ryan Murray --- tools/asahi/apple-dart-range-tests.py | 45 +++++++++++++++++++++------ 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/tools/asahi/apple-dart-range-tests.py b/tools/asahi/apple-dart-range-tests.py index 6fa006381918c..5d311fef0813d 100644 --- a/tools/asahi/apple-dart-range-tests.py +++ b/tools/asahi/apple-dart-range-tests.py @@ -31,6 +31,7 @@ def main(): #include #include typedef uint64_t u64; +typedef uint32_t u32; #define BITS_TO_LONGS(n) 1 #define atomic_long_read(x) (*(x)) #define bitmap_empty(x, n) (!(x)[0]) @@ -42,31 +43,36 @@ def main(): 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 { int num_streams, locked, version; void *dev; struct apple_dart_hw *hw; }; +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 { - void *pgtbl_ops; struct apple_dart_atomic_stream_map streams[2]; int count; + void *pgtbl_ops; bool dcp; struct apple_dart_atomic_stream_map streams[2]; int count; u64 dma_offset, mask; }; #define for_each_stream_map(i,d,m) for ((i)=0; (i)<(d)->count && ((m)=&(d)->streams[i],1); (i)++) -static int powered, power_error, sync_error, range_error, syncs, ranges, fulls; +static int powered, power_error, sync_error, range_error, syncs, ranges, fulls, firmware_flushes, quarantines, dcp_flushes; static u64 seen_first, seen_last; -static int pm_runtime_resume_and_get(void *dev) { if (power_error) return power_error; powered++; return 0; } +static int pm_runtime_resume_and_get(void *dev) { if (power_error && !dev) return power_error; powered++; return 0; } static void pm_runtime_put(void *dev) { assert(powered>0); powered--; } -static int apple_dart_hw_sync_locked(struct io_pgtable_cfg *cfg, struct apple_dart_stream_map *stream) { - syncs++; return sync_error; +static int apple_dart_hw_sync_locked(struct io_pgtable_cfg *cfg, struct apple_dart_stream_map *stream, bool invalidate) { + assert(invalidate); syncs++; return sync_error; } static int apple_dart_t8110_hw_tlb_command_range(struct apple_dart_stream_map *stream, int command, bool range, u64 first, u64 last) { assert(command==1 && range); ranges++; seen_first=first; seen_last=last; return range_error; } +static int apple_dart_dcp_domain_flush(struct apple_dart_domain *domain) { assert(domain->dcp); dcp_flushes++; return -EAGAIN; } +static void apple_dart_managed_quarantine(struct apple_dart *dart) { assert(dart->managed_sids); quarantines++; } +static int apple_dart_fw_invalidate(struct apple_dart_stream_map *stream, u64 first, u64 last) { + firmware_flushes++; seen_first=first; seen_last=last; return range_error; +} static int full(struct apple_dart_stream_map *stream) { fulls++; return 0; } ''' source += production source += r''' static void reset(void) { assert(powered==0); power_error=sync_error=range_error=syncs=ranges=fulls=0; - seen_first=seen_last=0; + seen_first=seen_last=0; firmware_flushes=quarantines=dcp_flushes=0; } int main(void) { struct apple_dart_hw hw={.invalidate_tlb=full}; @@ -88,14 +94,35 @@ def main(): assert(fulls==1 && ranges==0 && syncs==0); reset(); dart.locked=1; sync_error=-EBUSY; assert(apple_dart_domain_flush_tlb_range(&domain,true,0x4000,0x7fff)==-EBUSY); - assert(ranges==0 && fulls==0 && powered==0); + assert(ranges==1 && fulls==0 && powered==0); reset(); power_error=-EHOSTDOWN; assert(apple_dart_domain_flush_tlb_range(&domain,true,0x4000,0x7fff)==-EHOSTDOWN); assert(syncs==0 && ranges==0 && fulls==0 && powered==0); reset(); range_error=-ETIMEDOUT; assert(apple_dart_domain_flush_tlb_range(&domain,true,0x4000,0x7fff)==-ETIMEDOUT); assert(ranges==1 && powered==0); - reset(); domain.streams[0].sidmap[0]=0; + reset(); dart.managed_sids=true; range_error=-ETIMEDOUT; + assert(apple_dart_domain_flush_tlb_range(&domain,true,0x4000,0x7fff)==-ETIMEDOUT); + assert(ranges==1 && quarantines==1 && powered==0); + reset(); sync_error=-EBUSY; range_error=-ETIMEDOUT; + assert(apple_dart_domain_flush_tlb_range(&domain,true,0x4000,0x7fff)==-EBUSY); + assert(ranges==1 && quarantines==1 && powered==0); + reset(); dart.fw_mirror=true; + assert(!apple_dart_domain_flush_tlb_range(&domain,true,0x4000,0x7fff)); + assert(firmware_flushes==1 && ranges==0 && syncs==1); + reset(); dart.fw_mirror=false; dart.fw_handoff=true; + assert(!apple_dart_domain_flush_tlb_range(&domain,true,0x4000,0x7fff)); + assert(firmware_flushes==1 && ranges==0); + reset(); dart.fw_handoff=false; domain.dcp=true; + assert(apple_dart_domain_flush_tlb_range(&domain,true,0x4000,0x7fff)==-EAGAIN); + assert(dcp_flushes==1 && ranges==0 && syncs==0); + reset(); domain.dcp=false; + struct apple_dart second=dart; second.dev=(void *)1; + domain.streams[1].dart=&second; domain.streams[1].sidmap[0]=1; domain.count=2; + power_error=-EHOSTDOWN; + assert(apple_dart_domain_flush_tlb_range(&domain,true,0x4000,0x7fff)==-EHOSTDOWN); + assert(ranges==1 && syncs==1 && powered==0); + reset(); domain.count=1; domain.streams[0].sidmap[0]=0; assert(!apple_dart_domain_flush_tlb_range(&domain,true,0x4000,0x7fff)); assert(syncs==0 && ranges==0 && fulls==0 && powered==0); puts("PASS: device DVA, low window, legacy/unlocked fallback, sync/power/command errors and empty stream"); From bf6b0ec518342e56157e2ada7e8053d2b15ff98a Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 22:29:23 +1100 Subject: [PATCH 196/388] docs: record T6040 GPU parity and qualification gaps Trace calibration, firmware admission, queueing, power, recovery and memory paths against the shared Neo backend. Separate changes already present through #463 and #423 from firmware ABI restrictions and missing hardware measurements. Record the public-source boundary for M5 Pro. Assisted-by: LLM Signed-off-by: Ryan Murray --- GAPS.md | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 GAPS.md diff --git a/GAPS.md b/GAPS.md new file mode 100644 index 0000000000000..1f04b7ea5cde5 --- /dev/null +++ b/GAPS.md @@ -0,0 +1,55 @@ +# T6040 GPU parity audit + +Audit baseline: `integration/aurora` at +`9cbdd0734d9e`, with GPU integration #463 (`a0a9485847f8`), the +r5 display series (`905dff774561`), camera #462 (`2114ed0c6245`), and the +two r6 display patches. The reconstructed r6 branch is +`wip/j616s-display-r6` at `af0a664046fb`. + +This series repairs regression coverage. It does not change clock, voltage, +firmware policy, feature advertisement, or the submission algorithm. The major +T6040 calibration and image-admission changes requested by this audit are +already in the baseline. Software parity does not establish benchmark or CTS +parity. There are no J616s results for this series yet. + +## Gap table + +| Gap | Neo behavior | T6040 before this series | After this series | Evidence / remaining gate | +| --- | --- | --- | --- | --- | +| Per-chip leakage and power ceiling | T8140 has its own platform data | #463 requires a versioned DT handoff, finite positive leakage, a power ceiling and the measured core mask; no single-unit fallback | Same runtime behavior | `g17/calibration.rs`, `g17/initdata/v25g83.rs`; #463 commits `a8fb1380b414`, `53877eaebd4b`, `415fd2e6ee2d` | +| Raw OPP count versus logical states | Firmware consumes 11 logical states | 16 raw GPU OPPs; 11 selectors in each A/B map; both end at raw OPP 15; unused handoff slots must be zero | KUnit now uses valid complete maps and checks the final state's second voltage lane | `g17/calibration.rs`; a count of 15 in an ADT property is not evidence for expanding this firmware layout | +| Full clock eligibility | Platform OPPs feed firmware performance tables | DT supplies both cluster voltages and GPU/CS/AFR frequencies; maps and relative-B are checked against the GPU OPP table | Same runtime behavior | `g17/initdata/v25g83.rs`, `g17/fw/initdata/v25g83.rs`; confirm sustained clocks and limiting reasons on hardware | +| DVFS ramp / effort | Full effort on publication, cleared after 16 ms of continuous idle | Same `Firmware::note_submission`, direct-port accounting and race-checked idle clear | Same runtime behavior | `g17/mod.rs`, `g17/port.rs`, `g17/power.rs`; opaque firmware power words are not a documented ramp control | +| Idle / off policy | Shared host hold and firmware device-control path | Same host implementation; source documents a 25G83 effective 2 ms off delay and approximately 1 ms wake cost | Same runtime behavior; default hold unchanged | `g17/power.rs`; measure actual idle/wake latency before altering policy | +| Fast-die / PMP | Platform-specific firmware power graph | 25G83 AON table and separate power-state target/release (101/96 C) are already populated; DT enables the GFX suppliers | Same runtime behavior | `g17/fw/initdata/v25g83.rs::POWER_STATE_WORDS`, `g17/initdata/v25g83.rs`, J616s DT; verify clocks with PMP running | +| Periodic preemption | Context-switch timer mask is zero | Same zero mask; two-cluster operand-state allocation differs by hardware layout | Same runtime behavior | `g17/fw/initdata.rs::CONTEXT_SWITCH_TIMER_MASK`, `g17/fw/compute.rs::OperandState`; no evidence supports changing the mask | +| High-priority queues | HIGH / MEDIUM / LOW firmware scheduling profiles | Same scheduling-policy selector and priority work rings | Same runtime behavior | `g17/mod.rs::scheduling_policy`, `g17/fw/channels.rs`, `g17/qos.rs` | +| Timestamp publication | Retains one CPU mapping for each firmware timestamp buffer | Same shared timestamp buffer and completion publication | Host timestamp suite again compiles and executes the production retained-mapping writer | `timestamp.rs`, `mmu.rs::TimestampVmap`, `tools/testing/selftests/drivers/gpu/asahi-m4/test-timestamps.py` | +| Statistics | Drains firmware statistics; no `agx_stats` snapshot | Same; `stats_snapshot()` returns `None` on both | Same runtime behavior | `g17/channel.rs`, `g17/runtime/events.rs`, `g17/mod.rs`; statistics decoding/export remains a shared limitation | +| Alternate loaded image | T8140 uses fixed firmware selection | Both exact T6040 normalized TEXT digests are admitted in one `Build.images` slice | Same runtime behavior | #453 (`26f5eb68dbce`), #463 (`804bba86ca55`), `hw/t6040.rs`, `g17/fw/variant.rs`, `fwid.rs`; unknown images remain refused | +| Submission/completion improvements | #423 shared G17 runtime | #423 is already an ancestor of the baseline; direct compute/render publication, admission domains and completion collection are shared | Same runtime behavior | #423; `g17/runtime.rs`, `g17/runtime/render/direct.rs`, `g17/port.rs`, `g17/completion.rs` | +| G13/G14 `submit.empty` regressions | Neo uses the G17 runtime | G16S uses that same G17 runtime, not the maintained G13/G14 workqueue | Same runtime behavior | `driver.rs`, `file.rs`, `g17/queue/mod.rs`; user-space empty submits may never reach this ioctl; trace and benchmark the paired Mesa stack | +| G13/G14 idle wake regressions | Firmware-specific cost plus shared host overhead | G16S shares common ioctl/GEM/sync infrastructure, but has different firmware, power handling and queue backend | Same runtime behavior | `driver.rs`, `g17/power.rs`; G13/G14 benchmark ratios cannot be transferred to G16S | +| QID capacity | Firmware floor 0; reserve protects dedicated render pairs | 25G83 reserves QIDs 0..31; public queue limit is reduced accordingly | KUnit checks floor 32 in dynamic growth and full logical queue budget tests; host resource runner follows current reserve helper | `g17/kick.rs`, `g17/runtime.rs`, `tools/asahi/g17-resource-tests.py` | +| Compute ownership / retained mappings | Recovery and retirement witnesses protect mapping lifetime | Same production queue ownership, source loans, growth rollback and deferred release | Restored host fixtures and negative controls; source loans explicitly prevent premature reuse/sharing/rearming | `tools/asahi/g17-capacity-fault-tests.py`, constructor/growth/pool/release-order fixtures | +| Restart and fault classification | Shared recovery handshake plus validated T8140 per-slot keys | Shared handshake uses 25G83 status offsets; `recovery_slot_keys=None` preserves firmware blame when keys are unknown | Same runtime behavior | `hw/t6040.rs`, `g17/initdata.rs`, `g17/recovery/attribution.rs`; successful 25G83 fault/restart qualification is still required | +| DART / UAT | Shared mapping ownership and checked page-table borrowing | Shared MMU, with T6040 reserved tables, PoC cleaning, per-page TLBI and native GPU permission quirks | Same runtime behavior | `hw/t6040.rs`, `mmu.rs`, `pgtable.rs`; do not remove coherency or handoff requirements for a benchmark | +| Memory accounting | Creator-attributed committed/resident GEM accounting and capacity admission | Same `memacct::Client`, `File::memory_stats` and shared capacity machinery | Same runtime behavior | `file.rs`, `gem.rs`, `memacct.rs`, `g17/capacity.rs`; verify multi-client high-water and post-close behavior on hardware | +| MCache / few-primitives / ray traversal | Enabled by the 26A428 variant | Disabled by 25G83 variant; source records hangs for few-primitives and stalled ray traversal, while MCache is unqualified | Same runtime behavior | `g17/fw/variant.rs::V25G83`; enabling these requires independent ABI evidence and hardware tests | +| M5 Pro comparison | No M5 Pro hardware configuration exists in this public baseline | No T605x/G17S KMD path to compare | No claim of M5 parity | Pinned public tree contains T6040 and T8140; a licensed public M5 Pro reference is required | +| CTS and Linux/macOS performance ratio | Hardware results belong to their exact kernel/Mesa pair | No result for this candidate | Pending hardware agent | Run gpubench with matching suite, configuration and conditions; review CTS failures and kernel faults before claiming parity | + +## Source and validation boundaries + +The implementation references are the licensed public Aurora/Asahi kernel and +loader history. Existing Gravity Linux-derived layouts and attribution remain +unchanged. The public Gravity Linux `gravity-m4` reference inspected at +`4fc326f6c25ccceb7515e2635c22e01fde5d3bac` was checked in `g16.rs`, +`g16_runtime.rs` and `g16_power.rs`. Those files do not supply validated +T6040 recovery slot keys; that G16 runtime explicitly leaves compute +non-preemptible. No private implementation, firmware bytes, voltage table or +lab record is included here. + +The build handoff must identify the exact tested source and loader, verify boot +payload checksums, retain r6g rollback, and distinguish CPU/QEMU tests from GPU +execution. Missing J616s measurements are not passing results. From 3477ad38626619e213cdd9ad968e84a963c7cbdc Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 22:31:11 +1100 Subject: [PATCH 197/388] tools/asahi: match the module parameter value API in preemption tests The parameter accessor now returns a copied integer. Update the host adapter so the production priority selection compiles with the same API. This retains the existing firmware/version and explicit-policy controls. Assisted-by: LLM Signed-off-by: Ryan Murray --- tools/asahi/compute-preempt-tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/asahi/compute-preempt-tests.py b/tools/asahi/compute-preempt-tests.py index a13f103341b95..fdab1e9e7406b 100755 --- a/tools/asahi/compute-preempt-tests.py +++ b/tools/asahi/compute-preempt-tests.py @@ -48,7 +48,7 @@ def policy(text): pub static compute_preempt: Parameter = Parameter; pub static mut PARAM: i32 = -1; pub struct Parameter; - impl Parameter { pub fn value(&self) -> &i32 { unsafe { &*std::ptr::addr_of!(PARAM) } } } + impl Parameter { pub fn value(&self) -> i32 { unsafe { *std::ptr::addr_of!(PARAM) } } } } #[derive(Clone, Copy, Debug, PartialEq)] struct U64(u64); #[derive(Clone, Copy, Debug, PartialEq)] enum PipeType { Vertex, Fragment, Compute } From 2b2204fac3d5adac19c4a350e8f52a6254112aeb Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 22:33:34 +1100 Subject: [PATCH 198/388] pmdomain: apple: describe the PMP report test module Add the missing module description so the shared Fedora build no longer reports a modpost warning for pmp-report-test. Test behavior is unchanged. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/pmdomain/apple/pmp-report-test.c | 1 + 1 file changed, 1 insertion(+) diff --git a/drivers/pmdomain/apple/pmp-report-test.c b/drivers/pmdomain/apple/pmp-report-test.c index e04a91d339f23..d99ece1ac4b6f 100644 --- a/drivers/pmdomain/apple/pmp-report-test.c +++ b/drivers/pmdomain/apple/pmp-report-test.c @@ -276,4 +276,5 @@ static struct kunit_suite apple_pmp_report_suite = { }; kunit_test_suite(apple_pmp_report_suite); +MODULE_DESCRIPTION("KUnit tests for Apple PMP report validation"); MODULE_LICENSE("Dual MIT/GPL"); From ab9f25d29c0a542f8180a4df09af804a692998b8 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 22:44:57 +1100 Subject: [PATCH 199/388] drm/asahi: match G17 test helpers to built-in KUnit The Rust kunit_tests macro emits its test modules only for KUNIT=y. With the shared Fedora config's KUNIT=m, the helper constructors and compute-floor constant were still compiled and failed WERROR as dead code. Use the macro's exact condition for these four test-only helpers. Keep both the shipping config and warning policy unchanged. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/asahi/g17/kick.rs | 2 +- drivers/gpu/drm/asahi/g17/preparation.rs | 2 +- drivers/gpu/drm/asahi/g17/queue/admission.rs | 2 +- drivers/gpu/drm/asahi/g17/runtime.rs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/kick.rs b/drivers/gpu/drm/asahi/g17/kick.rs index 0f13c4e148563..f694dad86bbf0 100644 --- a/drivers/gpu/drm/asahi/g17/kick.rs +++ b/drivers/gpu/drm/asahi/g17/kick.rs @@ -43,7 +43,7 @@ const RENDER_HEADROOM: usize = 24; /// The render reserve with every execution context open. const FULL_RENDER_RESERVE: usize = render_reserve(crate::mmu::MAX_EXECUTION_CONTEXTS as usize); /// Compute channels that stay reachable with every execution context open. -#[cfg(CONFIG_KUNIT)] +#[cfg(CONFIG_KUNIT = "y")] pub(super) const COMPUTE_FLOOR: usize = QID_COUNT - FULL_RENDER_RESERVE; /// Upper bound on distinct physical compute channels named by one kick. pub(super) const COMPUTE_CHANNELS: usize = QID_COUNT; diff --git a/drivers/gpu/drm/asahi/g17/preparation.rs b/drivers/gpu/drm/asahi/g17/preparation.rs index eb2366fa3451b..2aa3213115076 100644 --- a/drivers/gpu/drm/asahi/g17/preparation.rs +++ b/drivers/gpu/drm/asahi/g17/preparation.rs @@ -60,7 +60,7 @@ pub(super) struct Gate { } impl Gate { - #[cfg(CONFIG_KUNIT)] + #[cfg(CONFIG_KUNIT = "y")] pub(super) fn new() -> Result> { Self::with_waiters(None) } diff --git a/drivers/gpu/drm/asahi/g17/queue/admission.rs b/drivers/gpu/drm/asahi/g17/queue/admission.rs index e1e7552c243f2..5d770ca1f64b0 100644 --- a/drivers/gpu/drm/asahi/g17/queue/admission.rs +++ b/drivers/gpu/drm/asahi/g17/queue/admission.rs @@ -406,7 +406,7 @@ pub(crate) struct Pool { renders: Option>, } impl Pool { - #[cfg(CONFIG_KUNIT)] + #[cfg(CONFIG_KUNIT = "y")] pub(crate) fn new(max_queues: u32) -> Result> { Self::with_waiters(max_queues, None, None) } diff --git a/drivers/gpu/drm/asahi/g17/runtime.rs b/drivers/gpu/drm/asahi/g17/runtime.rs index e04295d576606..9fe17db8a2809 100644 --- a/drivers/gpu/drm/asahi/g17/runtime.rs +++ b/drivers/gpu/drm/asahi/g17/runtime.rs @@ -186,7 +186,7 @@ pub(super) struct Registry { impl Registry { /// Initialize inside the firmware allocation instead of staging the whole /// registry on the probe stack. No additional allocation is required. - #[cfg(CONFIG_KUNIT)] + #[cfg(CONFIG_KUNIT = "y")] pub(super) fn new( clusters: u32, deflake: crate::g17::fw::render::Deflake, From 8987b717797feb652da658d4207dfd8b91cb867a Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 22:45:32 +1100 Subject: [PATCH 200/388] docs: record the shared modular KUnit build gap Record why the shipping KUNIT=m config exposed unused G17 test helpers and why matching Rust's built-in KUnit guard fixes it without changing hardware policy or weakening warning checks. Assisted-by: LLM Signed-off-by: Ryan Murray --- GAPS.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/GAPS.md b/GAPS.md index 1f04b7ea5cde5..b41325bdeaf26 100644 --- a/GAPS.md +++ b/GAPS.md @@ -6,7 +6,7 @@ r5 display series (`905dff774561`), camera #462 (`2114ed0c6245`), and the two r6 display patches. The reconstructed r6 branch is `wip/j616s-display-r6` at `af0a664046fb`. -This series repairs regression coverage. It does not change clock, voltage, +This series repairs regression coverage and a shared KUNIT=m WERROR build defect. It does not change clock, voltage, firmware policy, feature advertisement, or the submission algorithm. The major T6040 calibration and image-admission changes requested by this audit are already in the baseline. Software parity does not establish benchmark or CTS @@ -31,6 +31,7 @@ parity. There are no J616s results for this series yet. | G13/G14 `submit.empty` regressions | Neo uses the G17 runtime | G16S uses that same G17 runtime, not the maintained G13/G14 workqueue | Same runtime behavior | `driver.rs`, `file.rs`, `g17/queue/mod.rs`; user-space empty submits may never reach this ioctl; trace and benchmark the paired Mesa stack | | G13/G14 idle wake regressions | Firmware-specific cost plus shared host overhead | G16S shares common ioctl/GEM/sync infrastructure, but has different firmware, power handling and queue backend | Same runtime behavior | `driver.rs`, `g17/power.rs`; G13/G14 benchmark ratios cannot be transferred to G16S | | QID capacity | Firmware floor 0; reserve protects dedicated render pairs | 25G83 reserves QIDs 0..31; public queue limit is reduced accordingly | KUnit checks floor 32 in dynamic growth and full logical queue budget tests; host resource runner follows current reserve helper | `g17/kick.rs`, `g17/runtime.rs`, `tools/asahi/g17-resource-tests.py` | +| KUnit / WERROR config matrix | Shared test modules require KUNIT=y | Four G17 test helpers used the broader CONFIG_KUNIT guard and failed WERROR with the shipping KUNIT=m config | Helpers now use CONFIG_KUNIT = "y", matching the Rust macro; config and warning policy unchanged | `rust/macros/kunit.rs`, `g17/kick.rs`, `g17/preparation.rs`, `g17/queue/admission.rs`, `g17/runtime.rs`; verify both KUNIT=m release and KUNIT=y QEMU builds | | Compute ownership / retained mappings | Recovery and retirement witnesses protect mapping lifetime | Same production queue ownership, source loans, growth rollback and deferred release | Restored host fixtures and negative controls; source loans explicitly prevent premature reuse/sharing/rearming | `tools/asahi/g17-capacity-fault-tests.py`, constructor/growth/pool/release-order fixtures | | Restart and fault classification | Shared recovery handshake plus validated T8140 per-slot keys | Shared handshake uses 25G83 status offsets; `recovery_slot_keys=None` preserves firmware blame when keys are unknown | Same runtime behavior | `hw/t6040.rs`, `g17/initdata.rs`, `g17/recovery/attribution.rs`; successful 25G83 fault/restart qualification is still required | | DART / UAT | Shared mapping ownership and checked page-table borrowing | Shared MMU, with T6040 reserved tables, PoC cleaning, per-page TLBI and native GPU permission quirks | Same runtime behavior | `hw/t6040.rs`, `mmu.rs`, `pgtable.rs`; do not remove coherency or handoff requirements for a benchmark | From e5b0147408cfc6243b7ca2d7d0fe8e07efff55ab Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 22:50:09 +1100 Subject: [PATCH 201/388] docs: distinguish T6040 firmware registration and pointer layouts Record the startup, lead-command, pointer-block and topology differences alongside the shared submission path. MMIO pair registration on 25G83 and other fused core counts remain unqualified rather than inferred from Neo's different hardware and firmware interface. Assisted-by: LLM Signed-off-by: Ryan Murray --- GAPS.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/GAPS.md b/GAPS.md index b41325bdeaf26..2e0fd834ce28a 100644 --- a/GAPS.md +++ b/GAPS.md @@ -26,6 +26,9 @@ parity. There are no J616s results for this series yet. | High-priority queues | HIGH / MEDIUM / LOW firmware scheduling profiles | Same scheduling-policy selector and priority work rings | Same runtime behavior | `g17/mod.rs::scheduling_policy`, `g17/fw/channels.rs`, `g17/qos.rs` | | Timestamp publication | Retains one CPU mapping for each firmware timestamp buffer | Same shared timestamp buffer and completion publication | Host timestamp suite again compiles and executes the production retained-mapping writer | `timestamp.rs`, `mmu.rs::TimestampVmap`, `tools/testing/selftests/drivers/gpu/asahi-m4/test-timestamps.py` | | Statistics | Drains firmware statistics; no `agx_stats` snapshot | Same; `stats_snapshot()` returns `None` on both | Same runtime behavior | `g17/channel.rs`, `g17/runtime/events.rs`, `g17/mod.rs`; statistics decoding/export remains a shared limitation | +| Firmware startup / registration | Two instances, message acknowledgement, POWER_ON_DONE and MMIO render-pair registration; no lead commands | One merged 25G83 graph, status acknowledgement, no POWER_ON_DONE, no MMIO pair registration; lead commands required | Same runtime behavior | `g17/fw/variant.rs`, `g17/initdata/v25g83.rs`; startup differences are ABI requirements, while MMIO pair registration on 25G83 remains unqualified | +| Queue pointer / restart layout | Producer at +0x40, recovery target at +0x50 | Producer at +0x50, no recovery-target word | Same runtime behavior | `g17/fw/variant.rs::PointerLayout`, `g17/fw/queue.rs`; do not copy another firmware build's offsets | +| Core topology / mapping ownership | One six-core cluster; two firmware roots and fabric bridge | Qualified two-cluster, 20-core layout; reserved firmware tables, one root and no fabric bridge | Same runtime behavior | `hw/t6040.rs`, `hw/t8140.rs`, `g17/mod.rs`; other fused core counts are still refused, and this audit does not expand that admission | | Alternate loaded image | T8140 uses fixed firmware selection | Both exact T6040 normalized TEXT digests are admitted in one `Build.images` slice | Same runtime behavior | #453 (`26f5eb68dbce`), #463 (`804bba86ca55`), `hw/t6040.rs`, `g17/fw/variant.rs`, `fwid.rs`; unknown images remain refused | | Submission/completion improvements | #423 shared G17 runtime | #423 is already an ancestor of the baseline; direct compute/render publication, admission domains and completion collection are shared | Same runtime behavior | #423; `g17/runtime.rs`, `g17/runtime/render/direct.rs`, `g17/port.rs`, `g17/completion.rs` | | G13/G14 `submit.empty` regressions | Neo uses the G17 runtime | G16S uses that same G17 runtime, not the maintained G13/G14 workqueue | Same runtime behavior | `driver.rs`, `file.rs`, `g17/queue/mod.rs`; user-space empty submits may never reach this ioctl; trace and benchmark the paired Mesa stack | From 0d63308820ab84fb122a470bd482d714e531f253 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 23:08:18 +1100 Subject: [PATCH 202/388] soc: apple: clpc: account for GPU demand on T6040 Apply DjDeveloperr's GPU-demand mechanism from d69f14f4e4fb to T6040. The CPU-only policy can leave the fabric low during GPU-only work. Enable its graphics demand input and describe the existing graphics PMGR state in DT, as T8140 and T8132 already do. Use T6040's existing DCS 5 / fabric 2 table, not T8140's state numbers. CPU demand keeps its three-sample sustain rule; graphics demand uses the shared hold and idle release. No voltage, ceiling, parameter or new platform option is introduced. Confirm powered-state tracking and idle floor release on J616s before qualifying the performance result. Link: https://github.com/aurora-silicon/linux/pull/440 Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/boot/dts/apple/t6040.dtsi | 1 + drivers/soc/apple/clpc.c | 3 +++ 2 files changed, 4 insertions(+) diff --git a/arch/arm64/boot/dts/apple/t6040.dtsi b/arch/arm64/boot/dts/apple/t6040.dtsi index ad87eaf38ac31..2ec10e49cc1fb 100644 --- a/arch/arm64/boot/dts/apple/t6040.dtsi +++ b/arch/arm64/boot/dts/apple/t6040.dtsi @@ -1023,6 +1023,7 @@ clpc: performance-controller@1 { compatible = "apple,t6041-clpc"; reg = <1>; + apple,gpu-power-state = <&ps_gfx>; }; }; diff --git a/drivers/soc/apple/clpc.c b/drivers/soc/apple/clpc.c index cc7a620c1c772..d51501db045cb 100644 --- a/drivers/soc/apple/clpc.c +++ b/drivers/soc/apple/clpc.c @@ -508,6 +508,8 @@ static const struct apple_clpc_hw apple_clpc_hw_t8140 = { * A cluster requests only after three busy samples in a row (12 ms). At idle * a single busy sample raised the floor about 60 % of the time, from short * bursts of background work; three in a row bring that to about 11 %. + * GPU demand bypasses the CPU streak and uses the same qualified DCS 5 / + * fabric 2 states until the graphics state powers off and HOLD_MS expires. */ static const struct apple_clpc_hw apple_clpc_hw_t6040 = { .rail = { [APPLE_CLPC_DCS] = 0, [APPLE_CLPC_SOC] = 1 }, @@ -516,6 +518,7 @@ static const struct apple_clpc_hw apple_clpc_hw_t6040 = { [APPLE_CLPC_SOC] = { 0, 1, 2, 2 }, }, .sustain = 3, + .gpu_demand = true, }; static const struct of_device_id apple_clpc_of_match[] = { From c38dd831800a7529e3326316a547b9a990509b51 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 23:14:23 +1100 Subject: [PATCH 203/388] tools/asahi: exercise CLPC GPU demand and idle release Compile the production rail tables, sample, hold and floor encoder with controlled PMGR reads and time. Check T6040 DCS 5/fabric 2, unchanged T8140/T8132 floors, release after 32 ms, partial power state, read failure, CPU demand composition and vote errors. Use the real PMC rail encoding. Negative controls remove GPU demand, release too early or retain the floor at idle. These are CPU controls; J616s PMGR tracking, idle power, PMP cooperation and performance still need hardware qualification. Assisted-by: LLM Signed-off-by: Ryan Murray --- tools/asahi/clpc-gpu-demand-tests.py | 131 +++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 tools/asahi/clpc-gpu-demand-tests.py diff --git a/tools/asahi/clpc-gpu-demand-tests.py b/tools/asahi/clpc-gpu-demand-tests.py new file mode 100644 index 0000000000000..b39b0848ed74a --- /dev/null +++ b/tools/asahi/clpc-gpu-demand-tests.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Run production CLPC demand/hold logic with controlled PMGR reads and time.""" +from pathlib import Path +import os +import re +import subprocess +import tempfile + +ROOT = Path(__file__).resolve().parents[2] + + +def item(text, signature): + start = text.index(signature) + begin = text.index('{', start) + depth, end = 1, begin + 1 + while depth: + depth += (text[end] == '{') - (text[end] == '}') + end += 1 + return text[start:end] + + +def main(): + source = (ROOT / 'drivers/soc/apple/clpc.c').read_text() + header = (ROOT / 'include/linux/soc/apple/pmc.h').read_text() + rail_shift = re.search(r'^#define APPLE_PMC_RAIL_SHIFT\(rail\).*$', header, re.M).group(0) + types = '\n'.join(item(source, s) + ';' for s in [ + 'struct apple_clpc_hw {', 'struct apple_clpc_vote {']) + tables = '\n'.join(item(source, 'static const struct apple_clpc_hw ' + n) + ';' + for n in ['apple_clpc_hw_t8132', 'apple_clpc_hw_t8140', 'apple_clpc_hw_t6040']) + methods = '\n'.join(item(source, s) for s in [ + 'static unsigned int apple_clpc_vote(', 'static u32 apple_clpc_floor(', + 'static void apple_clpc_sample(']) + code = r''' +#include +#include +#include +#include +#include +typedef uint8_t u8; +typedef uint32_t u32; +#define APPLE_CLPC_LEVELS 4 +#define APPLE_CLPC_HOLD_MS 32 +#define HZ 1000 +#define max(a,b) ((a) > (b) ? (a) : (b)) +#define GENMASK(h,l) (((1u << ((h)-(l)+1))-1) << (l)) +#define APPLE_PMC_RAIL_SHIFT(r) ((r)*8) +#define time_after_eq(a,b) ((long)((a)-(b)) >= 0) +#define msecs_to_jiffies(ms) (ms) +enum apple_clpc_rail { APPLE_CLPC_DCS, APPLE_CLPC_SOC, APPLE_CLPC_RAILS }; +''' + types + r''' +struct apple_clpc_cluster { unsigned int request[APPLE_CLPC_RAILS]; }; +struct apple_clpc { + const struct apple_clpc_hw *hw; + struct apple_clpc_cluster clusters[1]; + struct apple_clpc_vote vote[APPLE_CLPC_RAILS]; + unsigned int nr_clusters, agent; + unsigned long samples, next_scan, update_errors; + void *covered, *pmc, *gpu_pmgr; + u32 gpu_offset, mask; +}; +static unsigned long jiffies; +static void *cpu_possible_mask; +static u32 pmgr_status, published; +static int read_error, vote_error; +static bool queued_idle; +static bool cpumask_equal(void *a, void *b) { return a == b; } +static void apple_clpc_find_clusters(struct apple_clpc *c) { (void)c; } +static void apple_clpc_sample_cluster(struct apple_clpc *c, struct apple_clpc_cluster *cl) { (void)c; (void)cl; } +static int regmap_read(void *map, u32 offset, u32 *status) { + assert(map); (void)offset; *status=pmgr_status; return read_error; +} +static int apple_pmc_floor_update(void *pmc, unsigned int agent, u32 mask, u32 floor) { + (void)pmc; assert(agent==1 && mask==3); published=floor; return vote_error; +} +static void apple_clpc_queue(struct apple_clpc *c, bool idle) { (void)c; queued_idle=idle; } +''' + tables + '\n' + methods + r''' +static void check(const struct apple_clpc_hw *hw, u32 top) { + struct apple_clpc c={.hw=hw,.agent=1,.mask=3,.gpu_pmgr=hw->gpu_demand ? (void *)1 : NULL}; + jiffies=0; read_error=vote_error=0; pmgr_status=0; published=~0u; + apple_clpc_sample(&c); assert(published==0 && queued_idle); + jiffies=100; pmgr_status=0xf0; + apple_clpc_sample(&c); assert(published==top && !queued_idle); + pmgr_status=0; + jiffies=131; apple_clpc_sample(&c); assert(published==top && !queued_idle); + jiffies=132; apple_clpc_sample(&c); assert(published==0 && queued_idle); + // A partially powered state is not a powered graphics consumer. + jiffies=200; pmgr_status=0xe0; + apple_clpc_sample(&c); assert(published==0 && queued_idle); + // Preserve the qualified floor when a live demand input cannot be sampled. + read_error=-EIO; apple_clpc_sample(&c); assert(published==top); + read_error=0; jiffies=232; + apple_clpc_sample(&c); assert(published==0); + // GPU activity must not erase independent CPU demand at release. + c.nr_clusters=1; c.clusters[0].request[0]=2; c.clusters[0].request[1]=1; + jiffies=300; pmgr_status=0xf0; apple_clpc_sample(&c); assert(published==top); + jiffies=332; pmgr_status=0; apple_clpc_sample(&c); assert(published==0x12); + vote_error=-EIO; apple_clpc_sample(&c); assert(c.update_errors==1); +} +int main(void) { + check(&apple_clpc_hw_t6040,0x25); + check(&apple_clpc_hw_t8140,0x34); + check(&apple_clpc_hw_t8132,0x35); + puts("PASS: T6040 5/2, T8140 4/3 and T8132 5/3 GPU floors; 32 ms idle release; partial state, read failure, CPU demand and vote-error controls"); +} +''' + code = code.replace('#define APPLE_PMC_RAIL_SHIFT(r) ((r)*8)', rail_shift) + with tempfile.TemporaryDirectory(prefix='apple-clpc-gpu-') as tmp: + root = Path(tmp) + for name, text in [ + ('fixed', code), + ('omit-gpu-demand', code.replace('if (clpc->gpu_pmgr) {', 'if (false) {')), + ('release-too-early', code.replace('msecs_to_jiffies(APPLE_CLPC_HOLD_MS)', '0')), + ('retain-at-idle', code.replace('vote->state = 0;', '')), + ]: + path = root / (name + '.c') + path.write_text(text) + binary = path.with_suffix('') + subprocess.run([os.environ.get('CC', 'cc'), '-std=gnu11', '-Wall', '-Wextra', + '-Werror', str(path), '-o', str(binary)], check=True) + run = subprocess.run([str(binary)], capture_output=True, text=True) + if name == 'fixed': + assert run.returncode == 0, run.stderr + print(run.stdout.strip()) + else: + assert run.returncode != 0, name + print('PASS negative control:', name) + + +if __name__ == '__main__': + main() From 772a9d3104ff31d332a8e083cb0f90082369cc60 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 23:28:49 +1100 Subject: [PATCH 204/388] arm64: dts: apple: enable the PMP's T6040 memory votes after its owner binds The T6040 PMC description still excludes PMP agent 0 from the earlier bring-up, when the firmware did not run and iBoot's top-state floor would have stayed in force. PMP startup is now supported and the GPU's report entry follows its power lifetime, but the PMC cannot honour that demand while the agent remains disabled. Use the existing apple,pmp supplier dependency before enabling static PMP, CLPC and AP voters, as in the Neo voter integration. The T6040 match data continues to restrict them to the established DCS/SOC rail mask; no display rail, voltage, power ceiling or new control is introduced. Confirm live PMP votes, idle release and sustained GPU/CPU thermal behaviour on J616s before qualifying this candidate. Link: https://github.com/aurora-silicon/linux/pull/219 Link: https://github.com/aurora-silicon/linux/pull/402 Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/boot/dts/apple/t6040.dtsi | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/arch/arm64/boot/dts/apple/t6040.dtsi b/arch/arm64/boot/dts/apple/t6040.dtsi index 2ec10e49cc1fb..1cbc2a97a51c1 100644 --- a/arch/arm64/boot/dts/apple/t6040.dtsi +++ b/arch/arm64/boot/dts/apple/t6040.dtsi @@ -1005,9 +1005,10 @@ * only hands to its firmware, so the two do not conflict; "rails" * comes first so that this node's unit address differs from * pmgr_dcp's. - * Static voters are CLPC (1) and AP (3). The PMP's agent (0) stays - * off while the PMP does not run: iBoot leaves its floor at the top - * DCS state. + * Bind after the PMP firmware owner. The PMP (0), CLPC (1) and AP (3) + * then vote through the PMC's T6040 DCS/SOC rail mask. Without the + * supplier dependency, the PMP's iBoot floor could be honoured before + * its firmware replaces it with live demand. */ pmc: power-management@5048aa000 { compatible = "apple,t6041-pmc"; @@ -1017,7 +1018,8 @@ nonposted-mmio; #address-cells = <1>; #size-cells = <0>; - apple,static-voters = <1 3>; + apple,pmp = <&pmp>; + apple,static-voters = <0 1 3>; status = "disabled"; clpc: performance-controller@1 { From fb63f95afe46ad66c35cf75a968ff201cd5de008 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 23:30:09 +1100 Subject: [PATCH 205/388] dt-bindings: spi: apple: Add the T6041 SPI controller The M4 Pro (T6040/T6041) SPI controllers are register compatible with T8103, like the T6020 and T8140 ones. Assisted-by: LLM Signed-off-by: Ryan Murray --- Documentation/devicetree/bindings/spi/apple,spi.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/Documentation/devicetree/bindings/spi/apple,spi.yaml b/Documentation/devicetree/bindings/spi/apple,spi.yaml index 4b30265686fa1..be47c5f9ff075 100644 --- a/Documentation/devicetree/bindings/spi/apple,spi.yaml +++ b/Documentation/devicetree/bindings/spi/apple,spi.yaml @@ -18,6 +18,7 @@ properties: - items: - enum: - apple,t6020-spi + - apple,t6041-spi - apple,t8140-spi - const: apple,t8103-spi - items: From ceca3fd9960240011d2609198a864d3a549d42cc Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 23:30:09 +1100 Subject: [PATCH 206/388] dt-bindings: input: apple,mesa-fingerprint: Name the signed root key Sensors whose enclave session needs a signed root key (the J700 and J616s part, 0x3356) take it as per-device factory data, like the calibration. Let firmware-name carry it as an optional second entry so the board description names it. Assisted-by: LLM Signed-off-by: Ryan Murray --- .../bindings/input/apple,mesa-fingerprint.yaml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Documentation/devicetree/bindings/input/apple,mesa-fingerprint.yaml b/Documentation/devicetree/bindings/input/apple,mesa-fingerprint.yaml index 53b304bcec7ff..631ba47a5a3ca 100644 --- a/Documentation/devicetree/bindings/input/apple,mesa-fingerprint.yaml +++ b/Documentation/devicetree/bindings/input/apple,mesa-fingerprint.yaml @@ -57,11 +57,17 @@ properties: description: Rail selector words, in power-on order. firmware-name: - maxItems: 1 + minItems: 1 + items: + - description: + Calibration blob for this sensor. + - description: + Signed root-key container, for sensors whose enclave session needs + one. description: - Calibration blob for this sensor. It is per-device factory data written - at manufacture, so it is neither redistributable nor derivable from the - compatible; the board description has to name it. + Per-device factory data written at manufacture, so neither + redistributable nor derivable from the compatible; the board + description has to name it. spi-cpha: true From 1bcccae53e5bfa901859a4e29f85cdbd9278a47c Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 23:30:10 +1100 Subject: [PATCH 207/388] soc: apple: sep: Take the sensor root key name from the device tree The signed root-key container is per-device data, but its name was fixed to J700's. Read it from the sensor node's second firmware-name entry and keep J700's name when the board gives only the calibration, so the J700 device tree and its installed file keep working unchanged. Name the file in the error when it cannot be loaded. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/soc/apple/sbio.rs | 9 +++++---- drivers/soc/apple/sensor.rs | 15 +++++++++++++-- drivers/soc/apple/sensor_shim.c | 24 ++++++++++++++---------- drivers/soc/apple/sep.rs | 3 +++ drivers/soc/apple/shim.h | 2 +- 5 files changed, 36 insertions(+), 17 deletions(-) diff --git a/drivers/soc/apple/sbio.rs b/drivers/soc/apple/sbio.rs index 88be672817b1c..5a81a370dda38 100644 --- a/drivers/soc/apple/sbio.rs +++ b/drivers/soc/apple/sbio.rs @@ -2183,14 +2183,15 @@ impl SepData { self.sensor_trace(c"SENSOR_NONCE"); if !self.sensor_key_loaded.load(Relaxed) { - let firmware = match kernel::firmware::Firmware::request( - c"apple/msrk-j700.bin", &self.dev, - ) { + // Per-device data, so the board names it; J700 predates that. + let name = sensor::root_key_firmware_name().unwrap_or(ROOT_KEY_FIRMWARE); + let firmware = match kernel::firmware::Firmware::request(name, &self.dev) { Ok(firmware) => firmware, Err(error) => { dev_err!( self.dev, - "sensor: signed root-key container unavailable: {:?}\n", + "sensor: signed root-key container {} unavailable: {:?}\n", + name, error ); return false; diff --git a/drivers/soc/apple/sensor.rs b/drivers/soc/apple/sensor.rs index 63a4ae28fbc6d..13bb470c89b3b 100644 --- a/drivers/soc/apple/sensor.rs +++ b/drivers/soc/apple/sensor.rs @@ -25,15 +25,26 @@ extern "C" { fn sep_sensor_irq_available() -> c_int; fn sep_sensor_irq_arm(); fn sep_sensor_irq_wait(timeout_ms: c_uint) -> c_int; - fn sep_sensor_firmware_name() -> *const c_char; + fn sep_sensor_firmware_name(index: c_uint) -> *const c_char; } /// The calibration blob's name from the board description, or `None` when the /// sensor node has no `firmware-name` property (the caller then uses a default). pub(crate) fn firmware_name() -> Option<&'static CStr> { + firmware_name_at(0) +} + +/// The signed root-key container's name from the board description: the +/// second `firmware-name` entry, or `None` when the board gives only the +/// calibration (the caller then uses a default). +pub(crate) fn root_key_firmware_name() -> Option<&'static CStr> { + firmware_name_at(1) +} + +fn firmware_name_at(index: c_uint) -> Option<&'static CStr> { // SAFETY: the shim returns NULL or a pointer to a NUL-terminated string // owned by the device property and valid for the device's lifetime. - let ptr = unsafe { sep_sensor_firmware_name() }; + let ptr = unsafe { sep_sensor_firmware_name(index) }; if ptr.is_null() { return None; } diff --git a/drivers/soc/apple/sensor_shim.c b/drivers/soc/apple/sensor_shim.c index 68405e0a74507..bed228b4d1ca3 100644 --- a/drivers/soc/apple/sensor_shim.c +++ b/drivers/soc/apple/sensor_shim.c @@ -568,19 +568,23 @@ static int sep_sensor_power_line_locked(void) } /* - * The per-device calibration blob name, as the board description gives it in - * the sensor node's "firmware-name" property. NULL when absent, so the caller - * can fall back to a default. + * A per-device blob name, as the board description gives it in the sensor + * node's "firmware-name" list: index 0 is the calibration, index 1 the signed + * root key. NULL when that entry is absent, so the caller can fall back to a + * default. */ -static const char *sep_sensor_firmware_name_locked(void) +static const char *sep_sensor_firmware_name_locked(unsigned int index) { - const char *name = NULL; + const char *names[2] = { NULL, NULL }; + int count; - if (!sep_spi) + if (!sep_spi || index >= ARRAY_SIZE(names)) return NULL; - if (device_property_read_string(&sep_spi->dev, "firmware-name", &name)) + count = device_property_read_string_array(&sep_spi->dev, "firmware-name", + names, ARRAY_SIZE(names)); + if (count <= (int)index) return NULL; - return name; + return names[index]; } /* Powers the sensor on/off, holding the hardware settling delay. */ @@ -871,14 +875,14 @@ int sep_sensor_power_line(void) return result; } -const char *sep_sensor_firmware_name(void) +const char *sep_sensor_firmware_name(unsigned int index) { struct sep_sensor_lifetime *sensor = sep_lock_sensor(); const char *result; if (!sensor) return NULL; - result = sep_sensor_firmware_name_locked(); + result = sep_sensor_firmware_name_locked(index); mutex_unlock(&sensor->lock); return result; } diff --git a/drivers/soc/apple/sep.rs b/drivers/soc/apple/sep.rs index df71496a564fe..7d9f4f2af1869 100644 --- a/drivers/soc/apple/sep.rs +++ b/drivers/soc/apple/sep.rs @@ -449,6 +449,9 @@ const PATCH_POLL_MS: u32 = 20; const PATCH_POLL_ATTEMPTS: u32 = 250; const CALIBRATION_FIRMWARE: &CStr = c"apple/mesa_calibration.bin"; +/// The signed root-key container on boards whose sensor node names only the +/// calibration (J700). +const ROOT_KEY_FIRMWARE: &CStr = c"apple/msrk-j700.bin"; const SBIO_PROBE_USER_ID: i32 = 1000; static_assert!(SBIO_PROBE_USER_ID >= crate::sks::SKS_DESIGNATE_USER_MIN); diff --git a/drivers/soc/apple/shim.h b/drivers/soc/apple/shim.h index 0425bf2cdaa6e..99d2661e2cb1c 100644 --- a/drivers/soc/apple/shim.h +++ b/drivers/soc/apple/shim.h @@ -132,7 +132,7 @@ int sep_sensor_cs_timing_mode(void); int sep_sensor_power_cycle(void); int sep_sensor_power_source(void); int sep_sensor_power(int on); -const char *sep_sensor_firmware_name(void); +const char *sep_sensor_firmware_name(unsigned int index); int sep_sensor_xfer(const void *tx, void *rx, size_t len); int sep_sensor_xfer_tx(const void *tx, size_t len); int sep_sensor_xfer2(const void *tx, size_t tx_len, void *rx, size_t rx_len); From 718202c86b1ff9f11fcc9aa957f017243c2d18b1 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 23:30:10 +1100 Subject: [PATCH 208/388] soc: apple: sep: Add the T6040 (J616s) platform profile The J616s ADT (26.6.2) describes a SEP that iBoot boots before the AP OS, an iop-sep,ascwrap-v6 whose DART window starts at the BIT(40) alias, and the Mesa sensor J700 carries (0x3356) on spi2 at 0x429108000, 8 MHz, chip select 0. So this is a warm-registration platform with T8140's authenticated sensor session, and the xART guard on from the first boot because macOS shares the enclave. Only J616s is matched; the other T6040 Macs' sensor wiring is unchecked. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/soc/apple/profile.rs | 40 ++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/drivers/soc/apple/profile.rs b/drivers/soc/apple/profile.rs index 0ba0eb510122b..0e632c65da81f 100644 --- a/drivers/soc/apple/profile.rs +++ b/drivers/soc/apple/profile.rs @@ -9,7 +9,7 @@ //! here once. The driver reads addresses from the device tree, never scans for //! them, and never patches properties at runtime. //! -//! Five bring-up targets are modelled: +//! These bring-up targets are modelled: //! //! * `T8103` / J313 (MacBook Air, M1): the host boots the SEP with the boot //! endpoint handshake over a 0x30000 shared-memory window. @@ -24,6 +24,8 @@ //! a powered sensor, so the driver uses the warm registration path. //! * `T8122` / J613, J615 (MacBook Air, M3): the T8112 warm registration with //! the T8140 SEP DART aperture above 4 GiB. +//! * `T6040` / J616s (MacBook Pro 16", M4 Pro): iBoot boots sepOS, so the +//! driver uses the warm registration path; the sensor is the J700 part. // The boot handshake, identity source, sensor and DART fields are consumed by // the boot-endpoint, identity and sensor-transport paths. @@ -426,6 +428,28 @@ pub(crate) const T8122: PlatformProfile = PlatformProfile { ..T8112 }; +/// MacBook Pro 16" M4 Pro (J616s). The J616s ADT (26.6.2, 25G83) records a +/// pre-booted SEP (`sepfw-load-at-boot`), an `iop-sep,ascwrap-v6` SEP whose +/// DART aperture starts at the BIT(40) alias (`vm-base` 0x10000004000), and +/// the Mesa sensor J700 carries (`sensor-id` 0x3356) on spi2 at +/// `0x429108000`, 8 MHz, chip select 0. Its power is NUB GPIO 53 and its +/// data-ready line AP GPIO 116. `/defaults` carries `cpx-encryption-mode = 2`. +/// +/// A warm-registration platform like T8140, with T8140's authenticated +/// sensor session: the enclave serves this sensor only after the nonce and +/// the signed root key. Device-tree nodes remain authoritative for addresses. +pub(crate) const T6040: PlatformProfile = PlatformProfile { + name: "T6040/J616s", + sensor: SensorProfile { + controller_base: 0x4_2910_8000, + ..T8140.sensor + }, + // macOS shares this enclave and Linux has no records here yet: keep + // Linux to its own xART records from the first boot. + xart_guard: 1, + ..T8140 +}; + static_assert!(T8103.shmem_capacity == 0x30000); static_assert!(T6020.shmem_capacity == 0x40000); static_assert!(T8140.shmem_capacity == 0x40000); @@ -441,6 +465,8 @@ static_assert!(T8112.sensor.controller_base == T8103.sensor.controller_base); static_assert!(T8122.shmem_capacity == T8112.shmem_capacity); static_assert!(T8122.sensor.controller_base == 0x2a1108000); static_assert!(T8122.wide_dma_mask); +static_assert!(T6040.sensor.controller_base == 0x429108000); +static_assert!(T6040.sensor.expected_id == T8140.sensor.expected_id); /// Whether the machine root declares `compatible`. /// @@ -587,6 +613,11 @@ pub(crate) fn detect() -> Result<&'static PlatformProfile> { if machine_has(b"apple,t8122") && (machine_has(b"apple,j613") || machine_has(b"apple,j615")) { return Ok(&T8122); } + // Only the board whose ADT was read: J616s. The other T6040 Macs share + // the SoC but their sensor wiring is unchecked. + if machine_has(b"apple,t6040") && machine_has(b"apple,j616s") { + return Ok(&T6040); + } Err(ENODEV) } @@ -637,6 +668,11 @@ mod tests { assert!(T8122.wide_dma_mask); assert!(T8122.shmem_capacity == T8112.shmem_capacity); assert!(T8122.sensor.controller_base == 0x2_a110_8000); + assert!(T6040.nonce_session); + assert!(T6040.wide_dma_mask); + assert!(T6040.bootstrap == Bootstrap::WarmRegister); + assert!(T6040.identity == IdentitySource::Chosen); + assert!(T6040.xart_guard == 1); } } @@ -667,7 +703,7 @@ mod dart_window_tests { #[test] fn the_other_profiles_keep_the_bit40_window() { - for profile in [&T8103, &T6000, &T6020, &T8140, &T6020_SEPOS13, &T8122] { + for profile in [&T8103, &T6000, &T6020, &T8140, &T6020_SEPOS13, &T8122, &T6040] { assert_eq!(offset(profile.dart_window), 1 << 40); assert_eq!(range(profile.dart_window), (0, 1 << 32)); } From ccbc6f976c8ee65c2947cd5f6eb9d85413fffd46 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 23:30:10 +1100 Subject: [PATCH 209/388] arm64: dts: apple: t6040: Add the SEP, its DART and mailbox, and spi2 From the J616s ADT: the SEP at 0x406600000 with its mailbox at +0x8000 (AIC 666-669), its DART at 0x404c80000 (AIC 676) with the window iBoot programmed (vm-base 0x10000004000, vm-size 0x3ff00000), and spi2 at 0x429108000 (AIC 1569) with the Mesa sensor child. Add the sep, spi_p and spi2 power states from the ADT pmgr devices. The SEP and its DART stay disabled; apple_sep enables them when it loads. Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/boot/dts/apple/t6040-pmgr.dtsi | 28 ++++++++++ arch/arm64/boot/dts/apple/t6040.dtsi | 66 +++++++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/arch/arm64/boot/dts/apple/t6040-pmgr.dtsi b/arch/arm64/boot/dts/apple/t6040-pmgr.dtsi index 8c05fb4d4d5d3..bf357745d581d 100644 --- a/arch/arm64/boot/dts/apple/t6040-pmgr.dtsi +++ b/arch/arm64/boot/dts/apple/t6040-pmgr.dtsi @@ -140,6 +140,24 @@ power-domains = <&ps_sio>; }; + ps_spi_p: power-controller@198 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x198 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "spi_p"; + power-domains = <&ps_sio>; + }; + + ps_spi2: power-controller@1c8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1c8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "spi2"; + power-domains = <&ps_spi_p>; + }; + ps_ans: power-controller@370 { compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; reg = <0x370 4>; @@ -178,6 +196,16 @@ power-domains = <&ps_ans>, <&ps_apcie_st1>; apple,always-on; }; + + ps_sep: power-controller@c00 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0xc00 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "sep"; + power-domains = <&ps_fab3_soc>; + apple,always-on; + }; }; /* diff --git a/arch/arm64/boot/dts/apple/t6040.dtsi b/arch/arm64/boot/dts/apple/t6040.dtsi index ad87eaf38ac31..fa677708d7c4f 100644 --- a/arch/arm64/boot/dts/apple/t6040.dtsi +++ b/arch/arm64/boot/dts/apple/t6040.dtsi @@ -701,6 +701,48 @@ status = "disabled"; }; + /* + * iBoot hands over a running SEP. Keep Linux off its DART at boot; + * apple_sep enables it when the driver binds. The SEP only accepts + * IOVAs in the window iBoot programmed (ADT vm-base 0x10000004000, + * vm-size 0x3ff00000), which carries the BIT(40) alias the DART + * translates. + */ + sep_dart: iommu@404c80000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x4 0x04c80000 0x0 0x20000>; + interrupt-parent = <&aic>; + interrupts = ; + #iommu-cells = <1>; + apple,dma-range = /bits/ 64 <0x10000004000 0x3ff00000>; + apple,dma-offset = /bits/ 64 <0x0>; + status = "disabled"; + }; + + sep: sep@406600000 { + compatible = "apple,sep"; + reg = <0x4 0x06600000 0x0 0x88000>; + mboxes = <&sep_mbox>; + mbox-names = "mbox"; + iommus = <&sep_dart 0>; + power-domains = <&ps_sep>; + status = "disabled"; + }; + + sep_mbox: mailbox@406608000 { + compatible = "apple,t6041-asc-mailbox", "apple,asc-mailbox-v4"; + reg = <0x4 0x06608000 0x0 0x4000>; + interrupt-parent = <&aic>; + interrupts = , + , + , + ; + interrupt-names = "send-empty", "send-not-empty", + "recv-empty", "recv-not-empty"; + #mbox-cells = <0>; + status = "disabled"; + }; + ans_mbox: mailbox@409608000 { compatible = "apple,t6041-asc-mailbox", "apple,asc-mailbox-v4"; reg = <0x4 0x09608000 0x0 0x4000>; @@ -826,6 +868,30 @@ status = "disabled"; }; + /* + * Touch ID. iBoot leaves the pads muxed and the controller uses its + * native chip select, so there is no pinctrl here. + */ + spi2: spi@429108000 { + compatible = "apple,t6041-spi", "apple,t8103-spi"; + reg = <0x4 0x29108000 0x0 0x4000>; + interrupt-parent = <&aic>; + interrupts = ; + clocks = <&clkref>; + power-domains = <&ps_spi2>; + #address-cells = <1>; + #size-cells = <0>; + status = "disabled"; + + mesa: fingerprint@0 { + compatible = "apple,mesa-fingerprint"; + reg = <0>; + spi-max-frequency = <8000000>; + spi-cpol; + status = "disabled"; + }; + }; + nvme: nvme@44dcc0000 { compatible = "apple,t6041-nvme-ans2"; reg = <0x4 0x4dcc0000 0x0 0x10000>, From a4b962a20add7dd09ff167f8cfc7cc4bd53ad2e9 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 23:30:10 +1100 Subject: [PATCH 210/388] arm64: dts: apple: t6040-j616s: Enable Touch ID Give the boot loader the sep alias it reserves the SEP firmware through, enable the SEP mailbox and spi2, and describe the sensor: NUB GPIO 53 powers it, AP GPIO 116 is its data-ready line, and its sealed calibration and root key come from this Mac's own iBoot system container. Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/boot/dts/apple/t6040-j616s.dts | 29 +++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/arch/arm64/boot/dts/apple/t6040-j616s.dts b/arch/arm64/boot/dts/apple/t6040-j616s.dts index 7abbd491489d0..a4fcf1a227bd7 100644 --- a/arch/arm64/boot/dts/apple/t6040-j616s.dts +++ b/arch/arm64/boot/dts/apple/t6040-j616s.dts @@ -29,6 +29,8 @@ disp0-piodma = &disp0_piodma; /* the boot loader finds the GPU node through this alias */ gpu = &gpu; + /* the boot loader reserves the SEP firmware through this alias */ + sep = &sep; }; chosen { @@ -470,6 +472,33 @@ status = "okay"; }; +/* apple_sep enables the SEP and its DART when it loads. */ +&sep_mbox { + status = "okay"; +}; + +/* + * Touch ID sensor (Mesa 0x3356). Its power is NUB GPIO 53 and its + * data-ready line is AP GPIO 116, as the J616s ADT describes them. + */ +&spi2 { + status = "okay"; +}; + +&mesa { + status = "okay"; + enable-gpios = <&pinctrl_nub 53 GPIO_ACTIVE_HIGH>; + interrupt-parent = <&pinctrl>; + interrupts = <116 IRQ_TYPE_EDGE_RISING>; + spi-cs-setup-delay-ns = <20>; + spi-cs-hold-delay-ns = <20>; + /* + * Per-device factory data from this Mac's own iBoot system container: + * the sealed calibration and the sealed root key. + */ + firmware-name = "apple/mesacal-j616s.bin", "apple/msrk-j616s.bin"; +}; + /* The Type-C retimers (ADT atcrt at 0x18/0x19/0x1a) sit on this bus; no driver yet. */ &i2c6 { status = "okay"; From fc5a95c0ee8708e5aa0a241ea0fc6cd4cdf85a87 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 00:05:40 +1100 Subject: [PATCH 211/388] docs: audit Neo KMD refinements commit by commit on T6040 List every #423 commit separately and enumerate the related integration and aurora-wip history, including original/rebased aliases and Dj's contributions. Record current reachability, firmware/data gates, superseded policies, source evidence and a J616s measurement for each. Separate public measured Neo results from stack results and estimates. Document the applicable CLPC GPU-demand and PMP-voter ports and the protocol-specific paths that cannot be enabled by copying Neo data. Keep unmerged measured follow-ups outside the pinned-head claims. Assisted-by: LLM Signed-off-by: Ryan Murray --- GAPS.md | 1050 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 1048 insertions(+), 2 deletions(-) diff --git a/GAPS.md b/GAPS.md index 2e0fd834ce28a..cfa2140a890b4 100644 --- a/GAPS.md +++ b/GAPS.md @@ -6,8 +6,10 @@ r5 display series (`905dff774561`), camera #462 (`2114ed0c6245`), and the two r6 display patches. The reconstructed r6 branch is `wip/j616s-display-r6` at `af0a664046fb`. -This series repairs regression coverage and a shared KUNIT=m WERROR build defect. It does not change clock, voltage, -firmware policy, feature advertisement, or the submission algorithm. The major +This series repairs regression coverage and a shared KUNIT=m WERROR build defect, +and ports GPU-driven memory/fabric demand and PMP voter cooperation using +T6040 data. GPU voltages, firmware features and the submission algorithm remain +unchanged from the reconstructed base. The major T6040 calibration and image-admission changes requested by this audit are already in the baseline. Software parity does not establish benchmark or CTS parity. There are no J616s results for this series yet. @@ -20,6 +22,8 @@ parity. There are no J616s results for this series yet. | Raw OPP count versus logical states | Firmware consumes 11 logical states | 16 raw GPU OPPs; 11 selectors in each A/B map; both end at raw OPP 15; unused handoff slots must be zero | KUnit now uses valid complete maps and checks the final state's second voltage lane | `g17/calibration.rs`; a count of 15 in an ADT property is not evidence for expanding this firmware layout | | Full clock eligibility | Platform OPPs feed firmware performance tables | DT supplies both cluster voltages and GPU/CS/AFR frequencies; maps and relative-B are checked against the GPU OPP table | Same runtime behavior | `g17/initdata/v25g83.rs`, `g17/fw/initdata/v25g83.rs`; confirm sustained clocks and limiting reasons on hardware | | DVFS ramp / effort | Full effort on publication, cleared after 16 ms of continuous idle | Same `Firmware::note_submission`, direct-port accounting and race-checked idle clear | Same runtime behavior | `g17/mod.rs`, `g17/port.rs`, `g17/power.rs`; opaque firmware power words are not a documented ramp control | +| GPU-driven fabric/memory floor | #440 lifts DCS 4/fabric 3 for GPU demand | T6040 CPU-only demand, gpu_demand=false and no power-state phandle | Port 0d63308820ab enables the shared mechanism with existing T6040 DCS 5/fabric 2 data | clpc.c and T6040 DT; g1/g2 P/C/R/H/I measurements required | +| PMP PMC vote cooperation | PMP/CLPC/AP agents enabled | T6040 static voters omit PMP 0 from pre-working-PMP bring-up | Port 772a9d3104ff adds the existing PMP supplier dependency and voter 0 on T6040 DCS/SOC rails | pmc.c and T6040 DT; live votes, idle release and thermal qualification required | | Idle / off policy | Shared host hold and firmware device-control path | Same host implementation; source documents a 25G83 effective 2 ms off delay and approximately 1 ms wake cost | Same runtime behavior; default hold unchanged | `g17/power.rs`; measure actual idle/wake latency before altering policy | | Fast-die / PMP | Platform-specific firmware power graph | 25G83 AON table and separate power-state target/release (101/96 C) are already populated; DT enables the GFX suppliers | Same runtime behavior | `g17/fw/initdata/v25g83.rs::POWER_STATE_WORDS`, `g17/initdata/v25g83.rs`, J616s DT; verify clocks with PMP running | | Periodic preemption | Context-switch timer mask is zero | Same zero mask; two-cluster operand-state allocation differs by hardware layout | Same runtime behavior | `g17/fw/initdata.rs::CONTEXT_SWITCH_TIMER_MASK`, `g17/fw/compute.rs::OperandState`; no evidence supports changing the mask | @@ -57,3 +61,1045 @@ lab record is included here. The build handoff must identify the exact tested source and loader, verify boot payload checksums, retain r6g rollback, and distinguish CPU/QEMU tests from GPU execution. Missing J616s measurements are not passing results. + +## Neo KMD improvements on T6040 + +This ledger audits refinements and handling, not just feature parity. It uses +`integration/aurora` at `9cbdd0734d9e186c5f5f5949a21b909d10c2571b` and +`aurora-wip` at `927a4a4480cf90ce8e3b4ab1d1ce58d84bf32fd4`. The conservative +history window starts 2026-09-26 (Neo platform prerequisites), before the public +T8140 firmware interface of 2026-10-03 and hardware bring-up of 2026-10-06. +Committer dates retain rebased older-author changes. The inventory also includes +the complete 105-commit public #171 history, all 65 commits of #423, and the +related PR versions used to identify original contributions and later ports. + +The path log covers `drivers/gpu/drm/asahi`, Apple PMP/PMGR/PMC/CLPC/MCC, +RTKit/mailbox, CPU-frequency/thermal and DART code, related AIC and DRM +scheduler/fence support, Rust DRM interfaces, and Neo DT dependencies. Broad +path/PR neighbors and imported baseline changes are explicitly marked N/A; +they are not represented as measured Neo optimizations. Old/rebased hashes are +listed as encountered aliases of the same subject/class, not asserted to be +byte-identical. The coverage inventory lists 1,551 physical commits (including +imported neighbors and PR-original versions), with 851 distinct subjects; all +65 #423 commits have separate rows. **Active** describes the surviving mechanism on T6040, including +its 7.3 port/refinement, rather than claiming that every historical SHA is an +ancestor. Multi-purpose firmware-introduction records retain their shared +parts while explicitly identifying dormant MCache/native-feature portions. A commit link is its exact diff; the evidence key states the current +reachability/gate. No per-commit speedup is invented from a stack measurement. + +### Applicable ports found by the commit audit + +| Neo mechanism | T6040 in g1 | g2 port and per-chip data | Required acceptance | +| --- | --- | --- | --- | +| #440 GPU-driven CLPC memory/fabric floor | CPU demand only; `gpu_demand=false`, no GPU power-state phandle | `0d63308820ab`: existing GPU-demand mechanism and DT phandle, T6040 DCS 5/fabric 2 and existing 32 ms release; CPU sustain=3 retained | C/R/H/I/P; actual PMGR tracking, floor release, video/browsing and sustained-load power | +| #219 PMP voter cooperation | Static voters 1/3 omit PMP 0 from pre-working-PMP bring-up | `772a9d3104ff`: existing `apple,pmp` supplier dependency, voters 0/1/3, existing T6040 DCS/SOC rail mask; no display rail or voltage change | P/B; PMP bound/ready, live request/honoured floors, idle and thermal behavior | + +Other audited gated paths are listed explicitly below. The T8140 lazy CPU +verification cost is absent on T6040's already-fast-switch profile. MMIO pair +registration, range-only TLB encoding, native RT/few-primitives and optional DCS +cooling cannot be enabled by copying a different firmware/hardware protocol. +The recovery slot-key attribution gate still needs validated 25G83 identity +facts; shared recovery, deadlines and retirement handling remain enabled. + +### Reported measurements and their scope + +These are public PR results, not new J616s measurements. Private qualification +reports were checked for scope and regressions; raw lab records are not copied +here. The older GPU profiling report's cost estimates are not measurements. + +| Evidence | Relevant commits/mechanism | Reported Neo result | J616s confirmation | +| --- | --- | --- | --- | +| [#171](https://github.com/aurora-silicon/linux/pull/171) | Retention, off-lock cleanup, reclaim quiet window and shared lifetime stack | Cold first-submit median 224 to 194–198 us, p99 338 to 295–305 us; menu-launch stalls below 100 ms versus 260–280 ms before quiet-window work. Stack results, not isolated gains. | B/M/C/R, repeated fresh contexts and foreground launch under bounded pressure | +| [#171](https://github.com/aurora-silicon/linux/pull/171) | Completion/recovery isolation | Earlier mixed render/compute lost its device within 2 s; later fixture ran over 568k verified checks in 60 s. Historical 1 ms stamp fallback was subsequently refined to 50 ms. | F/H/T; exact data and fences, peer liveness and no premature mapping release | +| [#423](https://github.com/aurora-silicon/linux/pull/423) | Cumulative scopes and submission stack | Same-boot submit API elapsed roughly 14 to 10 us; throughput stayed near 20 us. Combined candidate C-d64 19.44 us, R-d32 41.48 us, hop 42.12 us; workload/Mesa dependent. | C/R/H; preserve wall-time versus thread-CPU distinction | +| [#423](https://github.com/aurora-silicon/linux/pull/423) | `9bf3039873a1`, completion identity index | Scanner self samples 4.87% to 2.24% at depth 64, 3.16% to 1.93% on hops. CPU profile result. | O/C/H | +| [#423](https://github.com/aurora-silicon/linux/pull/423) | `26446182da3a`, 128-interval validation working set | C-d64 API elapsed 10.06 to 7.46 us; R-d32 12.29 to 8.16 us. Not a thread-CPU clock measurement. | C/R/D with rotating 64 buffers and remap/permission invalidation | +| [#423](https://github.com/aurora-silicon/linux/pull/423) | `b6248acf900c`, private-fence fast status | Submit API median 7.52 to 5.79 us; frontier samples 1.83% to 0.25%; throughput remained near 19.5 us. | C/H/O; errors, foreign inputs and delayed host publication | +| [#440](https://github.com/aurora-silicon/linux/pull/440) | `d69f14f4e4fb`, GPU fabric floor | C-d1 158.9 to 108.2 us; C-d64 19.3 to 14.7 us; R-d32 42.7 to 29.7 us; hop 42.2 to 31.9 us. Same driver/Mesa. | P/C/R/H, compare g1/g2 and keep heavy/idle checks | +| [#219](https://github.com/aurora-silicon/linux/pull/219) | GFX report/PMP vote and PMC enables | Short manual-bit glmark2 build experiment: 1453 with GFX report off to 2199 with it on; 2218 with all interface enables. Single-run 7–10 s experiment, not an isolated final-kernel A/B. | P/R; distinguish reported request, enabled agent and honoured floor | +| [#443](https://github.com/aurora-silicon/linux/pull/443) | Lazy verification and fast switch | C-d2 58.8–59.7 to 46.4–47.3 us; C-d1 107.8–108.4 to 102.6–102.7 us. T8140-specific spin removed. | C/I observation only: T6040 already lacks that spin | +| [#413](https://github.com/aurora-silicon/linux/pull/413) | Existing optional idle hold | The roughly 1.1 ms wake and 1.2–1.4 ms tiny-render-after-gap observations are **M4 Pro**, not Neo measurements. Default 2 ms adds no extended hold. | I at defaults; optional hold is a separate policy experiment | + +The provided current Neo Linux/macOS report has no completed paired Linux +record, so an overall Neo margin is still undefined. Historical PR ratios do +not establish the requested current all-workload margin. Use matched suite, +Mesa, clock/power conditions and repeats. Run only workloads admitted by the +controller; queue depth is not permission for extra process workers. + +### Reachability and gate evidence keys + +| Key | Current source/gate and interpretation | +| --- | --- | +| SHARED — Shared G17 execution | driver.rs selects g17::Gpu for T6040. g17/queue/mod.rs, runtime.rs, port.rs, runtime/render/direct.rs and runtime/render/submit.rs have no T8140-only gate on these mechanisms. The variant supplies QID floor 32, producer +0x50, two-cluster operands and 25G83 lead commands. | +| DRM — Shared DRM/scheduler/fences | G17 queues use job.rs, fence.rs and the common DRM scheduler/syncobj/GPUVM/shmem interfaces. CPU_FREQ and UCLAMP_TASK are enabled in the release config. Private cumulative scopes are advertised on both G17-backend targets at feature bit 29; foreign fences retain generic handling. | +| MEM — Common memory and lifetime | file.rs::memory_stats, gem.rs, memacct.rs, cleanup.rs, pgtable.rs and mmu.rs serve the T6040 G17 backend. Creator accounting, pins, revocable backing, canonical bindings and deferred destruction are not gated to T8140. T6040 keeps its explicit UAT coherency policy. | +| GMEM — Shared retained GPU resources | g17/freelist.rs, context.rs, object.rs, queue/compute.rs and runtime/render/pools.rs retain and reclaim the same resource owners. Allocations use T6040 core-layout sizes and its reduced QID budget; no Neo-only admission branch disables them. | +| REC — Shared recovery and deadlines | g17/recovery.rs, recovery/safety.rs, timeout.rs and runtime/events.rs serve both firmware variants. T6040 uses its status-B base and one firmware role. Exact retirement, interrupted-work failure, withheld-doorbell replay, progress-aware budgets and sticky VM reporting remain active. Current missing-stamp fallback is 50 ms, not the historical 1 ms. | +| POWER — Shared feed/effort/idle code | g17/feed.rs is instantiated for T6040 and derives CPU floors from each discovered policy maximum. g17/mod.rs::note_submission/idle_check and power.rs share effort 1.0 while work streams and a 16 ms idle clear. The existing optional idle hold is available on T6040 but its default 2 ms adds no extended hold. | +| TIMESTAMP — Shared timestamp publication | timestamp.rs::FirmwareBuffer retains a CPU vmap for the buffer lifetime; g17 completion publishes through it before signalling. T6040 uses this shared firmware-buffer path and its 24 MHz timestamp conversion, not a per-command vmap. | +| PRIORITY — Shared firmware priority profiles | g17/mod.rs::scheduling_policy selects HIGH/MEDIUM/LOW profiles for both targets; file.rs checks CAP_SYS_NICE or DRM master for HIGH and refuses REALTIME. Priority work-ring selection and QoS ownership are shared. | +| FW — Per-chip firmware adaptation | hw/t6040.rs selects V25G83. g17/initdata/v25g83.rs supplies one merged root and status acknowledgement, per-chip OPP/calibration input, separate views and no POWER_ON_DONE. Pointer and operand layouts are selected from Config/Variant rather than copied from the two-instance 26A428 graph. MCache is explicitly gated by V25G83.mcache=false; its firmware table/protocol is not part of admitted T6040 support. | +| PMGR — Shared PMGR state handling | pmdomain/apple/pmgr-pwrstate.c binds T6041 states through the t8103-compatible state protocol. BUSY masking, timeout unwind and firmware-owned/no-auto-PM properties apply through DT; no T8140-only register address is copied. | +| PMP — PMP/report per-chip data | soc/apple/pmp.rs and pmdomain/apple/pmp-report.c already contain T6041 match data: 42-bit DMA, version-5 bootargs checks, CoreAnalytics EP7, T6041 report offsets and three fast-die lanes. T6040 DT supplies the bootargs/report entries and CPU effort phandles. T8132/J713-only telemetry policy is excluded separately. | +| PMC — Shared PMC, T6040 rail mask | pmdomain/apple/pmc.c::apple_pmc_probe/agent_enable use T6040 agent stride, DCS maximum 5 and agent_rails=GENMASK(1,0). CPU/CLPC/AP voting already worked. Port 772a9d3104ff adds apple,pmp and static voter 0 after its supplier binds; display rails remain excluded. | +| CLPC — Shared CLPC, T6040 demand data | soc/apple/clpc.c uses T6040 DCS {1,2,5,5}, fabric {0,1,2,2}, sustain=3, 4 ms sampling and 32 ms hold. CPU demand/lifecycle refinements are shared. GPU demand was missing and is enabled by 0d63308820ab with the existing apple,gpu-power-state phandle. | +| GPUFLOOR — Applicable gate ported | Before g2, apple_clpc_hw_t6040.gpu_demand was false and its CLPC DT node had no apple,gpu-power-state. Port 0d63308820ab enables the same mechanism as #440 with T6040 DCS 5/fabric 2, not Neo 4/3. Hardware gain and idle/PMP cooperation remain unqualified. | +| MCC — MCC translated to 16 planes | soc/apple/mcc.c accepts the T6041/T8140-compatible controller; T6040 DT describes 16 planes and its display boot-DMA dependency. The J616s enables this node. Generic partial-enable unwind, lifetime and SLC allocation rules apply with that data. | +| CPU — Shared CPU frequency/limit handling | T6041 CPU nodes bind the t8112 fallback profile. Generic OPP cleanup, fast-switch and PMP effort QoS use its DT frequencies and three effort lanes. No Neo frequency or thermal curve is copied. | +| LAZY — Different CPU verification protocol | The lazy_verify refinements are T8140-only. T6040 binds soc_t8112_info, whose verify_transition is false; policy->fast_switch_possible is already true. It does not pay the T8140 blocking verification loop that these commits remove. Adding lazy verification would not port a missing gain. | +| DART — Related DARTs; native GPU uses UAT | iommu/apple-dart.c common publication/error/resume/stream logic is used by T6041 DARTs where their DT ownership mode selects it. T6040 display mirroring uses its own compatible/data. Native GFX translations use asahi/mmu.rs UAT, so this is related PMP/display robustness, not a claim that Neo DART addresses fit the G16 GPU. | +| RTKIT — Shared RTKit transport/lifetime | G17 Coprocessor uses the Rust RTKit wrapper over soc/apple/rtkit.c. Shared buffer retention, barriers, crash handling, bounded logs and shutdown ownership apply. T6040 supplies its firmware-data crash buffers and one ASC instance; MTP/J713-only adoption is excluded. | +| MBOX — ASCWrap transport, per-chip registers | soc/apple/mailbox.c and rust/kernel/soc/apple/mailbox.rs provide the common queue, IRQ and receiver ownership rules. T6040 binds its ASCWrap-v6 data; its wrapper/VBAR windows and IRQs come from DT. Neo-specific mailbox addresses are not reused. | +| AIC — Shared effective-affinity reporting | irq-apple-aic.c applies AICv2+ reporting to the T6041 AIC3 compatible too. It reports actual effective routing rather than manufacturing CPU migration. Record the IRQ/worker CPU on J616s; no isolated throughput gain is claimed. | +| DISPLAY — Related shared display hardening | Generic apple/dcp.c, parser and AFK lifecycle/bounds fixes are inherited by the reconstructed display stack where those helpers are used. They protect the display consumer of GPU buffers; they are not standalone compute performance claims. | +| OBS — Observability only when selected | Existing G17 tracepoints/diagnostics and first-failure logging are shared. Trace register sampling is conditional on the tracepoint being enabled; disabled tracing is not a new runtime policy or a measured throughput gain. | +| MMIO — Different registration ABI | runtime/render.rs::install_pair returns before registration power holds when V25G83.mmio_pair_registration=false. 25G83 installs from queue configuration and uses lead commands; 26A428 uses MMIO pair registration. Do not add its two control round trips or register writes to the admitted 25G83 protocol. | +| FEW — 25G83 rejects this fast path | V25G83.few_primitives=false; command.rs and feature reporting enforce it. The supported-source comment records a fragment hang with the flag. It is not an applicable refinement of the admitted 25G83 render ABI without new validated facts. | +| RT — RT specialization not admitted | V25G83.ray_traversal=false; command.rs rejects native scratch/intersection programs and get_params omits the feature. Generic watchdog/recovery infrastructure remains shared, but native RT pools/kill guards/RT-specific long-job use are dormant on the admitted 25G83 path. | +| KEY — Missing validated recovery identities | hw/t6040.rs has recovery_slot_keys=None. recovery/attribution.rs falls back to firmware blame when slot identity evidence is incomplete. Neo slot-key addresses must not be invented for 25G83; this specific attribution refinement remains gated pending validated T6040 facts. | +| UAT — Qualified alternate invalidation policy | hw/t6040.rs keeps reserved_tables, clean_tables and page_tlbi=true. mem.rs::tlbi_range takes the page/ASID alternative before its range encoder. The range-only field fix is dormant; per-SoC coherency/ownership support is active. Removing these working firmware-table requirements is not a justified performance port. | +| DCS — Optional cooling actor, no T6040 curve | PMC cooling/IPA paths require #cooling-cells and power-model DT data. J616s does not select them, and the Neo DCS thermal zone was subsequently removed because it throttled unrelated GPU/CPU heat. No T6040 power curve is invented to re-enable that policy. | +| DCSOFF — Already equivalent inactive zone | Neo removed its oscillating DCS thermal zone. J616s has no such zone to remove. Its PMP CPU/GPU limits remain; this does not justify disabling thermal protection. | +| IRQENTRY — DT-selected report context | IRQ-safe report entries are selected by apple,irq-safe for PMGR-parent use. GFX report consumption does not require copying the external-display parent topology; use the actual T6040 entry/consumer hierarchy. | +| STAT — Not the Neo backend | StatsChannel/GpuManager agx_stats decoding belongs to the maintained backend. g17::Gpu::stats_snapshot returns None on both Neo and T6040. It is not a Neo optimization waiting to be ported to G16S. | +| G15 — Different backend/hardware | T6030/T6031/T8122/G15 and M3-only runtime commits do not execute in T6040 g17::Gpu. Their firmware records, generation-specific power policy and native-G15 admission cannot be copied into G16S/25G83. | +| G16 — Different G16 backend/owner ABI | T8132/J713 uses the separate g16 backend and retained-owner PMP/TVM protocol. T6040 is G16S but dispatches to the g17 backend with V25G83, not that G16G runtime. Match/board gates and firmware ownership differ. | +| LEGACY — Maintained G13/G14 path | The workqueue/manager/versioned AGX firmware path is not T6040's G17 backend. G13/G14 automatic preemption, timestamp-copy and old InitData changes are not missing G16S mechanisms. | +| OTHERCHIP — Other CPU/guest hardware | T8142/T8152/T6050-specific transition, ownership or guest-timer data is not selected by T6041 CPU/GPU match data. No unsupported register/profile change is ported. | +| TUNNEL — Other peripheral topology | M1 tunneled PCIe/Thunderbolt recovery is not a Neo native-GPU improvement. T6040 has its own topology; these records are retained for coverage, not presented as a gated G16S optimization. | +| PERIPH — Not a GPU refinement | This scoped-history/PR neighbor concerns NVMe, HID, audio, SEP, USB, RTC or another peripheral, not a Neo native-GPU performance/handling mechanism. No port is implied. | +| BOARD — Neo-specific board route | The literal J700 topology/display source/domain-cell fix is not T6040 board data. Its independent J616s display/PMP/GPU composition is retained; copying a Neo route or physical address would be wrong. | +| RAWDIAG — Superseded raw diagnostics | The raw T8140 PMP DVFS debugfs dump was later removed. Current reporting uses validated per-SoC fields; no new raw interface or platform knob is added. | +| OLDTHERMAL — Superseded thermal policy | The interim J700 SMC CPU policy was removed/replaced by thermal-framework and PMP handling. It is not the policy selected for T6040 and no Neo temperature/power curve is copied. | +| SUPERSEDED — Superseded inside the series | The unconditional-doorbell/unbound-inline policies were refined by later #423 commits; the early disabled-J616s admission was replaced by calibrated #463 enablement. Current code preserves low-pressure probes and defers/coalesces eligible doorbells with explicit release/stall/recovery handling; do not reapply the obsolete intermediate policy. | +| VERIFY — Build/test/documentation only | This record changes tests, descriptions, bindings, layout/build checks or source style rather than a runtime performance mechanism. Its contract is retained where applicable; there is no isolated hardware speedup or new platform behavior to port. | +| MERGE — Integration record | This merge is provenance/composition, not an isolated performance mechanism. Its parent changes are enumerated below, including variant preservation and subsequent fixes; no separate speedup is assigned to a merge. | + +### Hardware measurements referenced by each commit + +All hardware actions require the controller's explicit J616s identity, policy +approval and ownership/session/resource gates. These are a measurement plan, +not permission to bypass them. Pair g1/g2 with the same loader and Mesa and +preserve failed runs, logs and r6g rollback. + +| Code | Measurement | +| --- | --- | +| C | compute.light d1/d2/d64; submit API wall and thread-CPU time, scheduler hops, faults and CPU-frequency/IRQ context; matching Mesa/suite. | +| R | render.light d1/d2/d32 and 8 queues, first render, heavy fill and tiler growth; API p50/p99, throughput and memory high-water. | +| H | same/cross-queue binary and timeline hops; p50/p95/p99, doorbells per command and 100k order/data checks; include eligible and fallback dependencies. | +| I | idle.wake after 2/20/100 ms, default policy, first-frame latency and clocks; 60/120 Hz frame pacing and idle power. Keep optional hold experiments separate. | +| F | controller-approved bounded stall/fault/long-job stress with innocent peers; no wrong data or false successful fence, recovery phase/time, VM errors and post-close reuse. | +| M | multi-client create/close/rearm and approved bounded pressure; committed/resident bytes, retained pools/QIDs, restoration time and leak slope. | +| P | PMP readiness, PMC per-agent requests/honoured floors and CLPC readbacks during GPU-only, mixed CPU/GPU, 90 s heavy load, idle, video and browsing; verify idle release and no sustained thermal regression. | +| T | timestamp/query correctness, 24 MHz conversion, aligned/unaligned publication where supported, buffer-map reuse and query CPU cost. | +| Q | privileged/unprivileged priority admission and concurrent LOW/MEDIUM/HIGH progress, global-priority transition cases and timeout attribution. | +| D | approved bounded VM_BIND/unmap/remap and DART ownership/resume boundaries; CPU cost, invalidation errors and zero unexpected DART/UAT faults. | +| B | exact boot/kernel/loader/Mesa pins, firmware admission, first queue/install, panel/display regression, supplier readiness and rollback. | +| O | existing tracepoints/counters under approved instrumentation; IRQ and event-worker CPU, pass duration, notifications and failure site. Tracing off remains the reference. | +| V | static/build/contract or capability-refusal check; no isolated hardware improvement claimed. | + +### Every commit in #423 + +All 65 entries are retained, including intermediate policies, tests and the +integration merge. Superseded steps are not mistaken for current behavior. + +| Commit | Author | Improvement / record | T6040 after ports — evidence key and changed source | Measurement | +| --- | --- | --- | --- | --- | +| [ad691cd5e9f7](https://github.com/aurora-silicon/linux/commit/ad691cd5e9f76aa9e486d93b560c58d5c8ff88c4) | DjDeveloperr | drm/asahi: publish bound G17 compute work without the admission passes [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — SHARED; `drivers/gpu/drm/asahi/g17/completion.rs` | C,H | +| [1121ae36b8d4](https://github.com/aurora-silicon/linux/commit/1121ae36b8d426be24c41ae360281b5f0709a69d) | DjDeveloperr | drm/asahi: wake G17 render publication when its pair is returned [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — SHARED; `drivers/gpu/drm/asahi/g17/mod.rs` | R,H | +| [abd85935bd4d](https://github.com/aurora-silicon/linux/commit/abd85935bd4db7db6be23b8194b1173c9a8988c5) | DjDeveloperr | drm/asahi: clear each G17 compute descriptor only once [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — SHARED; `drivers/gpu/drm/asahi/g17/fw/compute.rs` | C,H | +| [3ef3720890f8](https://github.com/aurora-silicon/linux/commit/3ef3720890f8c085f18e4f5e35db6853075b121c) | DjDeveloperr | drm/asahi: retain compute pool backing through pending channel admission [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — GMEM; `drivers/gpu/drm/asahi/g17/capacity.rs` | M,C,R | +| [f35d7ea1b373](https://github.com/aurora-silicon/linux/commit/f35d7ea1b37395a9bc1241d0fafcb698e0c12d46) | DjDeveloperr | dma-buf: stop polling an array after its first pending member [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — DRM; `drivers/dma-buf/dma-fence-array.c` | C,H,M,F | +| [319805fed41b](https://github.com/aurora-silicon/linux/commit/319805fed41bb181db8e90378beaf905f38f6297) | DjDeveloperr | drm/asahi: avoid unbound compute inline attempts on compute-only queues [#423](https://github.com/aurora-silicon/linux/pull/423) | Not applicable — SUPERSEDED; `drivers/gpu/drm/asahi/g17/context.rs` | C,H | +| [5a4822c6fb3d](https://github.com/aurora-silicon/linux/commit/5a4822c6fb3d1fc0a83d9b754b477bf1db35d782) | DjDeveloperr | drm/asahi: preserve low-pressure compute inline probes [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — SHARED; `drivers/gpu/drm/asahi/g17/runtime.rs` | C,H | +| [026065c43526](https://github.com/aurora-silicon/linux/commit/026065c435260844e39d05a182e99b128c5f9b2d) | DjDeveloperr | drm/asahi: submit same-VM compute input waits through firmware [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — SHARED; `drivers/gpu/drm/asahi/g17/capacity.rs` | C,H | +| [89c8873651b7](https://github.com/aurora-silicon/linux/commit/89c8873651b729fdd4e981e87b9d0df0d0ad0017) | DjDeveloperr | drm/asahi: treat a firmware source as complete once its retirement is queued [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — SHARED; `drivers/gpu/drm/asahi/g17/completion.rs` | C,R,H | +| [3ff4f4baa05a](https://github.com/aurora-silicon/linux/commit/3ff4f4baa05a88d5f99d2fee76089cf72b5617a4) | DjDeveloperr | drm/asahi: publish G17 commands from the ioctl at any queue depth [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — SHARED; `drivers/gpu/drm/asahi/g17/queue/mod.rs` | C,R,H | +| [a0dfc3c0b5a4](https://github.com/aurora-silicon/linux/commit/a0dfc3c0b5a4fced66119fded07ed141990a9025) | DjDeveloperr | drm/asahi: map G17 compute descriptors cached for the firmware [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — SHARED; `drivers/gpu/drm/asahi/g17/queue/compute.rs` | C,H | +| [a36e352d8564](https://github.com/aurora-silicon/linux/commit/a36e352d8564947071fef7e11dba709f399cd106) | DjDeveloperr | drm/asahi: size the G17 compute QID budget by open logical queues [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — SHARED; `drivers/gpu/drm/asahi/g17/kick.rs` | C,H | +| [b141ca9b16c3](https://github.com/aurora-silicon/linux/commit/b141ca9b16c33abb26238782cd7c0b751b5f52fe) | DjDeveloperr | drm/asahi: order same-VM render attachment overlaps in firmware [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — SHARED; `drivers/gpu/drm/asahi/g17/completion.rs` | R,H | +| [a475f4f9f1f7](https://github.com/aurora-silicon/linux/commit/a475f4f9f1f721b6636a4147dacbcf9f6224763b) | DjDeveloperr | drm/asahi: map G17 compute command records cached for the firmware [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — SHARED; `drivers/gpu/drm/asahi/g17/queue/compute.rs` | C,H | +| [ca71e4449b4a](https://github.com/aurora-silicon/linux/commit/ca71e4449b4a23d5f4e0149db76cf5198bfa7e5f) | DjDeveloperr | drm/asahi: stop the G17 compute completion scan once every record is found [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — SHARED; `drivers/gpu/drm/asahi/g17/initdata.rs` | C,H | +| [1cdac9ba0ddd](https://github.com/aurora-silicon/linux/commit/1cdac9ba0dddec96f59eb02a5067ee343aae43f4) | DjDeveloperr | drm/asahi: publish G17 compute into a bound queue without the device mutex [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — SHARED; `drivers/gpu/drm/asahi/g17/context.rs` | C,H | +| [7242bf87efd2](https://github.com/aurora-silicon/linux/commit/7242bf87efd2b3f4f8bf2a31fadb79b3e687f195) | DjDeveloperr | drm/asahi: send a G17 compute queue configuration only when it changes [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — SHARED; `drivers/gpu/drm/asahi/g17/queue/compute.rs` | C,H | +| [014605a36795](https://github.com/aurora-silicon/linux/commit/014605a3679581351913c3ab137653297f7a0cf7) | DjDeveloperr | drm/asahi: add G17 resource wait domains and use them for queue creation [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — SHARED; `drivers/gpu/drm/asahi/g17/availability.rs` | C,R,H | +| [64f717dcfd1e](https://github.com/aurora-silicon/linux/commit/64f717dcfd1e4aa5856882181f5c2ac040c5ce4e) | DjDeveloperr | drm/asahi: wait for G17 compute capacity on the release that frees it [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — SHARED; `drivers/gpu/drm/asahi/g17/availability.rs` | C,H | +| [327e50fc45b5](https://github.com/aurora-silicon/linux/commit/327e50fc45b57acf1954e8c2d78977198286feb0) | DjDeveloperr | drm/asahi: wait for a G17 compute source only through its completion record [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — SHARED; `drivers/gpu/drm/asahi/g17/runtime.rs` | C,H | +| [6f4d3bbb47c1](https://github.com/aurora-silicon/linux/commit/6f4d3bbb47c1a7b19abef118b41b131440a8f414) | DjDeveloperr | drm/asahi: add G17 command lifecycle tracepoints [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — OBS; `drivers/gpu/drm/asahi/g17/mod.rs` | O,F | +| [e24e3bad9e7d](https://github.com/aurora-silicon/linux/commit/e24e3bad9e7d3c6cf7079d683c9364d619cd3a55) | DjDeveloperr | drm/asahi: ring the G17 compute pipe doorbell for every publication [#423](https://github.com/aurora-silicon/linux/pull/423) | Not applicable — SUPERSEDED; `drivers/gpu/drm/asahi/g17/queue/compute.rs` | C,H | +| [94c66ecf24e6](https://github.com/aurora-silicon/linux/commit/94c66ecf24e6d931a434914d6a513ee36ff6ffd7) | DjDeveloperr | drm/asahi: map the G17 compute queue graph cached for the firmware [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — SHARED; `drivers/gpu/drm/asahi/g17/queue/compute.rs` | C,H | +| [db365ecc299f](https://github.com/aurora-silicon/linux/commit/db365ecc299f0663db8a629e70dfdb4d117eb95e) | DjDeveloperr | drm/asahi: wait for G17 render capacity on the release that frees it [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — SHARED; `drivers/gpu/drm/asahi/g17/context.rs` | R,H | +| [06bc290c266a](https://github.com/aurora-silicon/linux/commit/06bc290c266abcb8b0d45f75c3369e231ae8c4a7) | DjDeveloperr | drm/asahi: give the G17 render scheduler the pair's depth as credits [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — SHARED; `drivers/gpu/drm/asahi/g17/job.rs` | R,H | +| [3d1f2047766c](https://github.com/aurora-silicon/linux/commit/3d1f2047766c2d6e8d165a4af2cfe4adc0edd366) | DjDeveloperr | drm/asahi: keep a G17 render free list registered while the firmware is busy [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — SHARED; `drivers/gpu/drm/asahi/g17/freelist.rs` | R,H | +| [b7fa0d482eea](https://github.com/aurora-silicon/linux/commit/b7fa0d482eea670f9004422eaf57703a7ec8d4e5) | DjDeveloperr | drm/asahi: take the G17 device mutex less often to publish a render [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — SHARED; `drivers/gpu/drm/asahi/g17/queue/admission.rs` | R,H | +| [d272a7060d20](https://github.com/aurora-silicon/linux/commit/d272a7060d20a0beb635ca8426c8822b8e823a43) | DjDeveloperr | drm/asahi: publish a G17 render into its pair without the device mutex [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — SHARED; `drivers/gpu/drm/asahi/g17/initdata.rs` | R,H | +| [6428f7fd42fe](https://github.com/aurora-silicon/linux/commit/6428f7fd42fe075981984da569b0fa076f59a127) | DjDeveloperr | drm/asahi: lend a G17 render pair without the identity check first [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — SHARED; `drivers/gpu/drm/asahi/g17/runtime.rs` | R,H | +| [8cf02f589bce](https://github.com/aurora-silicon/linux/commit/8cf02f589bce34b13002cbdf0f3b0361c4a8e66b) | DjDeveloperr | drm/asahi: wait for G17 render admission before run_job [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — SHARED; `drivers/gpu/drm/asahi/g17/queue/admission.rs` | R,H | +| [550fb355e9a3](https://github.com/aurora-silicon/linux/commit/550fb355e9a30c0db9c59fe35a931f1db9c6b5c4) | DjDeveloperr | drm/asahi: trace the G17 core power state at each work doorbell [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — OBS; `drivers/gpu/drm/asahi/g17/mod.rs` | O,F | +| [f381ac9b71eb](https://github.com/aurora-silicon/linux/commit/f381ac9b71ebcb70eb72497ec4115553856a31fc) | DjDeveloperr | drm/asahi: trace G17 host-to-firmware messages [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — OBS; `drivers/gpu/drm/asahi/g17/channel.rs` | O,F | +| [adcd56e02036](https://github.com/aurora-silicon/linux/commit/adcd56e020362664c7bc3af288e1ad812df7b4c7) | DjDeveloperr | drm/asahi: maintain cumulative completion scopes without repeated fence merges [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — DRM; `drivers/gpu/drm/asahi/g17/fence.rs` | C,H,M,F | +| [bcdfdafd80cf](https://github.com/aurora-silicon/linux/commit/bcdfdafd80cf93fadc79df688dd0109b9c4ae8d2) | DjDeveloperr | drm/asahi: test G17 queue budgets with firmware-reserved identities [#423](https://github.com/aurora-silicon/linux/pull/423) | Not applicable — VERIFY; `drivers/gpu/drm/asahi/g17/kick.rs` | V | +| [971d4316d91c](https://github.com/aurora-silicon/linux/commit/971d4316d91cce97b92db72c4f29e5155b76d626) | DjDeveloperr | drm/asahi: size cached compute records for every native wait [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — SHARED; `drivers/gpu/drm/asahi/g17/queue/compute.rs` | C,H | +| [9bf3039873a1](https://github.com/aurora-silicon/linux/commit/9bf3039873a1e241991b5999aceff5610c660ade) | DjDeveloperr | drm/asahi: index pending compute completion identities before reading payloads [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — SHARED; `drivers/gpu/drm/asahi/g17/queue/compute/retirement.rs` | C,H | +| [26446182da3a](https://github.com/aurora-silicon/linux/commit/26446182da3aa8d98ce799478a025117b021eaba) | DjDeveloperr | drm/asahi: retain a deep working set of validated GPU page ranges [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — SHARED; `drivers/gpu/drm/asahi/g17/validation.rs` | C,R,D | +| [b6248acf900c](https://github.com/aurora-silicon/linux/commit/b6248acf900c1cf30deac52791c2c50d10055d7b) | DjDeveloperr | drm/asahi: avoid locking pending private completion fences [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — SHARED; `drivers/gpu/drm/asahi/g17/job.rs` | C,R,H | +| [b08c81722d89](https://github.com/aurora-silicon/linux/commit/b08c81722d896eee8da9771e99e38e6986a2a806) | DjDeveloperr | drm/asahi: give G17 firmware restarts time to drain long work [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — REC; `drivers/gpu/drm/asahi/g17/recovery.rs` | F,Q | +| [f1db16a83586](https://github.com/aurora-silicon/linux/commit/f1db16a83586e4bec596a71e12e0d8b87dd3b16c) | DjDeveloperr | drm/asahi: fail commands a GPU restart interrupted instead of completing them [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — REC; `drivers/gpu/drm/asahi/g17/completion.rs` | F,Q | +| [30f85560fd28](https://github.com/aurora-silicon/linux/commit/30f85560fd28ce05151c16eab7a58433d125a81e) | DjDeveloperr | drm/asahi: stop the G17 device on a watchdog storm only without progress [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — REC; `drivers/gpu/drm/asahi/g17/queue/compute.rs` | F,Q | +| [2c0abe2d090a](https://github.com/aurora-silicon/linux/commit/2c0abe2d090a9701b13b22d5ac56fb24d87cfe12) | DjDeveloperr | drm/asahi: scan only the G17 compute completion records written since the last pass [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — SHARED; `drivers/gpu/drm/asahi/g17/initdata.rs` | C,H | +| [39bdd5b8e2d5](https://github.com/aurora-silicon/linux/commit/39bdd5b8e2d584fff0d342a71a8945e64c8a9673) | DjDeveloperr | drm/asahi: release pruned G17 frontier entries after the ioctl publishes [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — SHARED; `drivers/gpu/drm/asahi/g17/queue/mod.rs` | C,R,H | +| [b41ecb56c077](https://github.com/aurora-silicon/linux/commit/b41ecb56c0775a43b747adaa37661b353651dbff) | DjDeveloperr | drm/asahi: publish both G17 render QoS owners in one pass [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — SHARED; `drivers/gpu/drm/asahi/g17/port.rs` | R,H | +| [b86c445ba282](https://github.com/aurora-silicon/linux/commit/b86c445ba2829f644075ca663217abe65844e8c0) | DjDeveloperr | drm/asahi: stop copying G17 fragment registers to the unused client page [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — SHARED; `drivers/gpu/drm/asahi/g17/queue/render/memory.rs` | C,R,H | +| [5a55068193e2](https://github.com/aurora-silicon/linux/commit/5a55068193e213d7115a63f2222ffc304a4ced98) | DjDeveloperr | drm/asahi: read the G17 render work rings once per direct publication [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — SHARED; `drivers/gpu/drm/asahi/g17/port.rs` | R,H | +| [b67018ed5974](https://github.com/aurora-silicon/linux/commit/b67018ed5974069c13c13249834dbc3b84e806ed) | DjDeveloperr | drm/asahi: request the G17 tiling descriptor slot before preparing a render [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — SHARED; `drivers/gpu/drm/asahi/g17/queue/render.rs` | R,H | +| [4d0362d0c18d](https://github.com/aurora-silicon/linux/commit/4d0362d0c18d288d3bd520fdfb69c2e7624cf6b0) | DjDeveloperr | drm/asahi: store G17 register arrays without an intermediate copy [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — SHARED; `drivers/gpu/drm/asahi/g17/fw/kick.rs` | C,R,H | +| [861b39d948f6](https://github.com/aurora-silicon/linux/commit/861b39d948f6c0588b3646e1246e660b52230e5b) | DjDeveloperr | drm/asahi: also request the G17 queue configuration records early [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — SHARED; `drivers/gpu/drm/asahi/g17/queue/render/memory.rs` | C,R,H | +| [66ef051d8bd4](https://github.com/aurora-silicon/linux/commit/66ef051d8bd4e66bdc145eb7fb7a920235a8e668) | DjDeveloperr | drm/asahi: scan only render pool slots for G17 free-list leases [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — GMEM; `drivers/gpu/drm/asahi/g17/runtime/render/pools.rs` | M,C,R | +| [1aec5be232da](https://github.com/aurora-silicon/linux/commit/1aec5be232da7bacbbe38b7f9b22c27906eac8c6) | DjDeveloperr | drm/asahi: omit G17 render queue configurations that only reload the held completion [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — SHARED; `drivers/gpu/drm/asahi/g17/queue/render.rs` | R,H | +| [d0df244b8f02](https://github.com/aurora-silicon/linux/commit/d0df244b8f023037bb0f9cc9793f1d0f285cfc87) | DjDeveloperr | drm/asahi: keep one CPU mapping per G17 timestamp buffer [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — TIMESTAMP; `drivers/gpu/drm/asahi/g17/completion.rs` | T,C | +| [3ff8c49c5f12](https://github.com/aurora-silicon/linux/commit/3ff8c49c5f12b908a72e6c076ff0d7336ffddcb8) | DjDeveloperr | drm/asahi: withhold G17 compute doorbells behind two unretired commands [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — SHARED; `drivers/gpu/drm/asahi/g17/queue/compute.rs` | C,H | +| [b1f5dc6df0ba](https://github.com/aurora-silicon/linux/commit/b1f5dc6df0ba5650c8424027a025db233c6544c4) | DjDeveloperr | drm/asahi: drop G17 compute doorbells that another doorbell announced [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — SHARED; `drivers/gpu/drm/asahi/g17/port.rs` | C,H | +| [d38cce2d8be5](https://github.com/aurora-silicon/linux/commit/d38cce2d8be54bdbc01e90e1a92ef3dd65ac0640) | DjDeveloperr | drm/asahi: announce withheld G17 compute doorbells whenever retirement stalls [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — SHARED; `drivers/gpu/drm/asahi/g17/queue/compute.rs` | C,H | +| [2d8286101c86](https://github.com/aurora-silicon/linux/commit/2d8286101c8631939cb13ce1b7b14c9041856ca6) | DjDeveloperr | drm/asahi: build the G17 driver without warnings [#423](https://github.com/aurora-silicon/linux/pull/423) | Not applicable — VERIFY; `drivers/gpu/drm/asahi/g17/queue/compute.rs` | V | +| [9a0ed807e4d3](https://github.com/aurora-silicon/linux/commit/9a0ed807e4d3691a5ea46fce2a2424295f8af6ea) | DjDeveloperr | drm/asahi: renew G17 job deadlines while the GPU keeps retiring work [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — REC; `drivers/gpu/drm/asahi/g17/job.rs` | F,Q | +| [afbfbc0b89ca](https://github.com/aurora-silicon/linux/commit/afbfbc0b89ca4779b13cefff5bed0b4036751b2c) | DjDeveloperr | drm/asahi: kill closed G17 contexts only while no other work runs [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — REC; `drivers/gpu/drm/asahi/g17/runtime/compute_exit.rs` | F,Q | +| [31279abf0648](https://github.com/aurora-silicon/linux/commit/31279abf0648c00e454b181f01d65b277a32fb90) | DjDeveloperr | drm/asahi: fail G17 render groups a recovery dropped out of order at once [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — REC; `drivers/gpu/drm/asahi/g17/queue/render.rs` | F,Q | +| [7012a59ed129](https://github.com/aurora-silicon/linux/commit/7012a59ed129baf969d83a3aa4d90f2a6cb410f2) | DjDeveloperr | drm/asahi: announce withheld G17 compute doorbells after a recovery [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — REC; `drivers/gpu/drm/asahi/g17/recovery.rs` | F,Q | +| [8e257af33a6c](https://github.com/aurora-silicon/linux/commit/8e257af33a6c4036ee56bc8a628e733dba2bd525) | DjDeveloperr | drm/asahi: move the G17 cumulative-signals feature to bit 29 [#423](https://github.com/aurora-silicon/linux/pull/423) | Active — SHARED; `drivers/gpu/drm/asahi/g17/sync_output.rs` | C,R,H | +| [76d4e880149e](https://github.com/aurora-silicon/linux/commit/76d4e880149e5592396001f728f7803efb616377) | DjDeveloperr | drm/asahi: build the G17 compute QID budget test against the variant constructor [#423](https://github.com/aurora-silicon/linux/pull/423) | Not applicable — VERIFY; `drivers/gpu/drm/asahi/g17/runtime.rs` | V | +| [d0da7e79dc9c](https://github.com/aurora-silicon/linux/commit/d0da7e79dc9c99020d109e95d18256cd5b3d6bc2) | DjDeveloperr | drm/asahi: check the G17 QID floor test against the open-queue compute budget [#423](https://github.com/aurora-silicon/linux/pull/423) | Not applicable — VERIFY; `drivers/gpu/drm/asahi/g17/runtime.rs` | V | +| [18b408b7368b](https://github.com/aurora-silicon/linux/commit/18b408b7368b9470c22d92c9afe1991d0fe608ba) | DjDeveloperr | drm/asahi: check the G17 admission churn test against its deadline, not the clock [#423](https://github.com/aurora-silicon/linux/pull/423) | Not applicable — VERIFY; `drivers/gpu/drm/asahi/g17/preparation.rs` | V | +| [de1be1b4b5b1](https://github.com/aurora-silicon/linux/commit/de1be1b4b5b110ae60877f77aa1637a920333579) | DjDeveloperr | Merge integration/aurora into fix/g17-recovery-dropped-groups-20261025 [#423](https://github.com/aurora-silicon/linux/pull/423) | Not applicable — MERGE | V | + +### Other Neo and shared-support commits + +Each listed alias has the stated current mechanism/gate. Device-specific data +uses the T6040 counterpart, not literal Neo register/DT values. + +| Commits / encountered aliases | Author | Improvement / record | T6040 after ports — evidence key and changed source | Measurement | +| --- | --- | --- | --- | --- | +| [6a5d96e61fa5](https://github.com/aurora-silicon/linux/commit/6a5d96e61fa52810e709a0b8bb5e9988df78a7d7) | Sasha Finkelstein | cpufreq: apple-soc: Calculate frequency as a 64-bit value | Active — CPU; `drivers/cpufreq/apple-soc-cpufreq.c` | C,I,P | +| [a861779b92c2](https://github.com/aurora-silicon/linux/commit/a861779b92c259c0a5ba7ec3fd512ad7bda1b8d4) | Asahi Lina | rust: soc: apple: rtkit: Add Apple RTKit abstraction | Active — RTKIT; `drivers/soc/apple/Kconfig` | B,F | +| [4b42187aef53](https://github.com/aurora-silicon/linux/commit/4b42187aef53c266b472e731e4f8e0a1fe0de3a7) | Janne Grunau | rust: soc: apple: rtkit: Add ASC CPU control register | Active — RTKIT; `rust/kernel/soc/apple/rtkit.rs` | B,F | +| [6a2c57b5a719](https://github.com/aurora-silicon/linux/commit/6a2c57b5a719f7c71fe6252865e4f036f6d27a64) | Sasha Finkelstein | soc: apple: rtkit: Add apple_rtkit_has_endpoint() | Active — RTKIT; `drivers/soc/apple/rtkit.c` | B,F | +| [b7fbb2fde4ea](https://github.com/aurora-silicon/linux/commit/b7fbb2fde4eaa0ac53e46dcb76a835fdc9cd9477) | Sasha Finkelstein | rust: soc: apple: rtkit: Add apple_rtkit_has_endpoint | Active — RTKIT; `rust/kernel/soc/apple/rtkit.rs` | B,F | +| [bba00880a59b](https://github.com/aurora-silicon/linux/commit/bba00880a59b144155716928066b12cc5751dd52) | Sasha Finkelstein | rust: soc: apple: Add Apple mailbox abstractions | Active — MBOX; `drivers/soc/apple/Kconfig` | B,O | +| [883657fe64b3](https://github.com/aurora-silicon/linux/commit/883657fe64b3a62fa36c90666b0a00a4e5093166) | Sasha Finkelstein | soc: apple: Add support for the PMP co-processor | Active — PMP; `drivers/soc/apple/Kconfig` | P,B | +| [fccd1dbe8ba8](https://github.com/aurora-silicon/linux/commit/fccd1dbe8ba820d4d9f5d1a624987e2820035204), [04841b120bbf](https://github.com/aurora-silicon/linux/commit/04841b120bbf7a2fb454cbfa984647e867b64848), [da7aa98b1aae](https://github.com/aurora-silicon/linux/commit/da7aa98b1aae834db5f7f4a8396afc39099d1b2f) | Janne Grunau | fixup! soc: apple: Add support for the PMP co-processor | Active — PMP; `drivers/soc/apple/pmp.rs` | P,B | +| [c81971a6c4c7](https://github.com/aurora-silicon/linux/commit/c81971a6c4c706b2f5783653a2df8d79782d4efe) | Asahi Lina | drm/scheduler: Fix UAF in drm_sched_fence_get_timeline_name | Active — DRM; `drivers/gpu/drm/scheduler/sched_entity.c` | C,H,M,F | +| [94bfb56baeeb](https://github.com/aurora-silicon/linux/commit/94bfb56baeeb02bc9c9ce8616aef0bd72a5de3f2) | Asahi Lina | rust: drm: sched: Add GPU scheduler abstraction | Active — DRM; `rust/kernel/drm/mod.rs` | B,M | +| [1340031e447c](https://github.com/aurora-silicon/linux/commit/1340031e447c56ec9d0f8ea7a68436a623fafc91) | Asahi Lina | drm/asahi: Add the Asahi driver for Apple AGX GPUs | Active — MEM; `drivers/gpu/drm/Kconfig` | M,D | +| [af74e2af96bc](https://github.com/aurora-silicon/linux/commit/af74e2af96bc483417f5015f98f4aa54e355851d) | Janne Grunau | drm/asahi: v6.19 probe change | Active — MEM; `drivers/gpu/drm/asahi/driver.rs` | M,D | +| [8c048a0ab849](https://github.com/aurora-silicon/linux/commit/8c048a0ab84971117a5f641ba2185b51e0b12059) | Janne Grunau | drm/asahi: Move unsafe data initialization to driver code | Active — MEM; `drivers/gpu/drm/asahi/driver.rs` | M,D | +| [a95f0620d24c](https://github.com/aurora-silicon/linux/commit/a95f0620d24c456c74d11261e329df054bf70e7b) | Janne Grunau | drm/asahi: RiiR page tables | Active — MEM; `drivers/gpu/drm/asahi/alloc.rs` | M,D | +| [19e9dc7ccb94](https://github.com/aurora-silicon/linux/commit/19e9dc7ccb9499ac3c92e7764edc2312656dc9d5) | Janne Grunau | drm/asahi: Implement ASAHI_BIND_SINGLE_PAGE (mmu/pgtbl) | Active — MEM; `drivers/gpu/drm/asahi/mmu.rs` | M,D | +| [96ce16e432a2](https://github.com/aurora-silicon/linux/commit/96ce16e432a213f7ae185870826697900c9acc60) | Asahi Lina | drm/asahi: pgtable: Add dumper | Active — MEM; `drivers/gpu/drm/asahi/pgtable.rs` | M,D | +| [ad2fb060c5dc](https://github.com/aurora-silicon/linux/commit/ad2fb060c5dcce503bc099062ff54c237b6866d0) | Asahi Lina | drm/asahi: pgtable: Add helpers for decoding PTE perms | Active — MEM; `drivers/gpu/drm/asahi/pgtable.rs` | M,D | +| [7b55670c3a59](https://github.com/aurora-silicon/linux/commit/7b55670c3a59219e37192a1f5771a86217008808) | Janne Grunau | drm/asahi: Copy tyr's mmu/vm/range.rs | Active — MEM; `drivers/gpu/drm/asahi/asahi.rs` | M,D | +| [9d16f582e208](https://github.com/aurora-silicon/linux/commit/9d16f582e2085935f893f99326fbcc0346c998bb) | Janne Grunau | drm/asahi: Switch gpuvm to DRM_GPUVM_IMMEDIATE_MODE | Active — MEM; `drivers/gpu/drm/asahi/file.rs` | M,D | +| [829551e64f1a](https://github.com/aurora-silicon/linux/commit/829551e64f1a350adddb71751be16a0243e1b666) | Janne Grunau | drm/asahi: MemFlag change in v6.19 | Active — MEM; `drivers/gpu/drm/asahi/gpu.rs` | M,D | +| [f5b02e71cd67](https://github.com/aurora-silicon/linux/commit/f5b02e71cd671ac4e3ceff54bbd1b25e7f20ae9d) | Janne Grunau | drm/asahi: rtkit pin adaptation | Active — MEM; `drivers/gpu/drm/asahi/gpu.rs` | M,D | +| [4220285e8415](https://github.com/aurora-silicon/linux/commit/4220285e8415cc772736b57f0dface3dcdbbee4f) | Janne Grunau | drm/asahi: select APPLE_PMP_REPORT | Active — PMP; `drivers/gpu/drm/asahi/Kconfig` | P,B | +| [30d957799282](https://github.com/aurora-silicon/linux/commit/30d9577992828e8d2f8345cc4b34a19098ca068c) | Janne Grunau | drm/asahi: Improve VM bind performance for large BOs | Active — MEM; `drivers/gpu/drm/asahi/mmu.rs` | M,D | +| [22e9a0b69598](https://github.com/aurora-silicon/linux/commit/22e9a0b69598bc23e66d72d1e51bd40396966dc1) | Janne Grunau | drm/asahi: `use` formatting annd rename changes | Active — MEM; `drivers/gpu/drm/asahi/driver.rs` | M,D | +| [f014fcba9d71](https://github.com/aurora-silicon/linux/commit/f014fcba9d71972198f28c3d5315605669073bea) | Janne Grunau | drm/asahi: v7.1 Mmio relaxed changes | Active — MEM; `drivers/gpu/drm/asahi/regs.rs` | M,D | +| [20dec7b1eaa9](https://github.com/aurora-silicon/linux/commit/20dec7b1eaa9a31dc67a8216d338bb10a452659d) | DesktopECHO | drm/asahi: Clean up deferred BOs when dropping a VM | Active — MEM; `drivers/gpu/drm/asahi/file.rs` | M | +| [e07ff81ee812](https://github.com/aurora-silicon/linux/commit/e07ff81ee812fd84603a498beaa1bb25eb629caa) | Janne Grunau | fixup! rust: soc: apple: rtkit: Add Apple RTKit abstraction | Active — RTKIT; `rust/kernel/soc/apple/rtkit.rs` | B,F | +| [44a01c258b11](https://github.com/aurora-silicon/linux/commit/44a01c258b111368f3c75848553ae1a658f46013) | Janne Grunau | drm/asahi: Adjustment for v7.2 + partial drm-rust v7.3 | Active — MEM; `drivers/gpu/drm/asahi/alloc.rs` | M,D | +| [23b564dd6c09](https://github.com/aurora-silicon/linux/commit/23b564dd6c09982106e2b5cc5ce80c25010c354a) | Janne Grunau | drm/asahi: pgtable: Drop more dev coredump related code | Active — MEM; `drivers/gpu/drm/asahi/pgtable.rs` | M,D | +| [b7f0faf4da36](https://github.com/aurora-silicon/linux/commit/b7f0faf4da367be4a29146f5eb82b795dc2f270d), [afffea42ac29](https://github.com/aurora-silicon/linux/commit/afffea42ac29a1f6f8b3ef3819691c1d7b0a259a) | Hojun Cho | pmdomain: apple: Add missing spin_lock_init | Active — PMP; `drivers/pmdomain/apple/pmp-report.c` | P,B | +| [0de0a3740a9a](https://github.com/aurora-silicon/linux/commit/0de0a3740a9a26973e7e49ad1cd87981914c457a), [919b934ff719](https://github.com/aurora-silicon/linux/commit/919b934ff7191fa72fab73ecf4ac785dd570c3c3), [ae512e4edf8b](https://github.com/aurora-silicon/linux/commit/ae512e4edf8b96d5eacd79b11fb190837d311b1b) | Ryan Murray | arm64: dts: apple: t8140-pmgr: Add ANS, SPMI, PMP and SMC I2C domains | Active — PMP | P,B | +| [2740578bc68c](https://github.com/aurora-silicon/linux/commit/2740578bc68c410528172a54f9e2e19d35c255d1), [c96a7ffae393](https://github.com/aurora-silicon/linux/commit/c96a7ffae39306507c405d49a3a19927eab289fe), [5578a4e73d10](https://github.com/aurora-silicon/linux/commit/5578a4e73d10d63615ed1eaaf5a7215310df19b8) | Ryan Murray | arm64: dts: apple: t8140: Add CPU frequency scaling | Active translated data — FW | B,P | +| [e7e641d03183](https://github.com/aurora-silicon/linux/commit/e7e641d03183aa76578a1504a4758da6b71c968f), [b58c399f44d7](https://github.com/aurora-silicon/linux/commit/b58c399f44d7499ed62c907efda7b0017f421baf), [f9ec3820d63d](https://github.com/aurora-silicon/linux/commit/f9ec3820d63d57ddb9790ac168a5642efc11f21c) | Ryan Murray | arm64: dts: apple: t8140: Use the Tahiti CPU compatibles and L1 cache sizes | Active translated data — FW | B,P | +| [c702ccc166bb](https://github.com/aurora-silicon/linux/commit/c702ccc166bbe593718a8b4d8a51063e6c5bc102), [f104bb09ce2a](https://github.com/aurora-silicon/linux/commit/f104bb09ce2abf48baa4cf5028b9224141cfdd00), [118ffba5b7b0](https://github.com/aurora-silicon/linux/commit/118ffba5b7b06b9786e24dfab819caa1fb1a2333) | Ryan Murray | arm64: dts: apple: t8140-j700: Enable ANS, SMC and CPU frequency scaling | Active translated data — FW | B,P | +| [5e5d5289f17e](https://github.com/aurora-silicon/linux/commit/5e5d5289f17ede272d495e7e4db117d47c6523a3), [dd366e9d3a8e](https://github.com/aurora-silicon/linux/commit/dd366e9d3a8e05ab209c7ebe1b28413820f018d4) | Marcelo Alcantara | iommu: apple-dart: raise MAX_DARTS_PER_DEVICE to 8 | Active — DART; `drivers/iommu/apple-dart.c` | D,B | +| [e1817d4152f5](https://github.com/aurora-silicon/linux/commit/e1817d4152f57baca92148c8bbc48fe9b0a8e9f9), [97cdb08431f3](https://github.com/aurora-silicon/linux/commit/97cdb08431f3eef20c2e24c41166bc5dee9e9976), [162b64627bbc](https://github.com/aurora-silicon/linux/commit/162b64627bbcb2fa0ae83cbb6cbf86825ced3e33), [70dfd8f93d3c](https://github.com/aurora-silicon/linux/commit/70dfd8f93d3cac6d6ac7ad26909ca6378fd6a2b4) | Ryan Murray | iommu/apple-dart: Check devm_memremap() for an error pointer | Active — DART; `drivers/iommu/apple-dart.c` | D,B | +| [f57d90040433](https://github.com/aurora-silicon/linux/commit/f57d9004043387ed6c437ddf0afcfcc1bb49f2fd), [6fa300933bb1](https://github.com/aurora-silicon/linux/commit/6fa300933bb1f05c9a49bddc05f8ea3b42c47d35), [79379798f12f](https://github.com/aurora-silicon/linux/commit/79379798f12fdb32ba79f74b0524cfc4f0170471) | Ryan Murray | iommu/apple-dart: Support T8140 DARTs and firmware-owned stream roots | Active — DART; `drivers/iommu/apple-dart.c` | D,B | +| [6ec845b2f381](https://github.com/aurora-silicon/linux/commit/6ec845b2f3812090ed39893f8e26999c757950c9), [3bd878053ca7](https://github.com/aurora-silicon/linux/commit/3bd878053ca7aed1e4af56bc14c339b8bddc6d9c), [6898f34ac7bd](https://github.com/aurora-silicon/linux/commit/6898f34ac7bd349ce29b60fb9785168950c5ed69), [fb7b8edf4ee1](https://github.com/aurora-silicon/linux/commit/fb7b8edf4ee19a35c07c1a2c6aa6b8e65c71777b) | DjDeveloperr | arm64: dts: apple: t8140-pmgr: Add the GPU power domains [#171](https://github.com/aurora-silicon/linux/pull/171) | Active translated data — FW | B,P | +| [cc2d03580b0a](https://github.com/aurora-silicon/linux/commit/cc2d03580b0a68ca0f9779adebd646645813d673), [ea4abd222b2f](https://github.com/aurora-silicon/linux/commit/ea4abd222b2fa2473dba79ca4a91a220e8cb20ba), [52fac306b907](https://github.com/aurora-silicon/linux/commit/52fac306b90783087af17af7ba3e2fc6570122e5), [e77d74a6be76](https://github.com/aurora-silicon/linux/commit/e77d74a6be7645e64b9b50bb51713e72a08c9520) | DjDeveloperr | arm64: dts: apple: t8140: Add the GPU [#171](https://github.com/aurora-silicon/linux/pull/171) | Active translated data — FW | B,P | +| [8284bd4d3914](https://github.com/aurora-silicon/linux/commit/8284bd4d3914ce296a83c38753a69b07e1c10a0e), [29b0b3a54664](https://github.com/aurora-silicon/linux/commit/29b0b3a546641438b14da95054ae136540465de8), [54124e3efacb](https://github.com/aurora-silicon/linux/commit/54124e3efacba7ba530e742cb2f297bbdeb719fa), [f4a46cd97b2a](https://github.com/aurora-silicon/linux/commit/f4a46cd97b2a3e70875d76e3cf2b82f3959fd0f5) | DjDeveloperr | arm64: dts: apple: t8140-j700: Enable the GPU [#171](https://github.com/aurora-silicon/linux/pull/171) | Active translated data — FW | B,P | +| [31ee8e23124d](https://github.com/aurora-silicon/linux/commit/31ee8e23124d77700e47cabc0e8645404187fd74), [5b85b301d4ee](https://github.com/aurora-silicon/linux/commit/5b85b301d4ee38256c757299a0c58c4a79a531a7), [727284277d33](https://github.com/aurora-silicon/linux/commit/727284277d335215edd48b5f74a6f4d4e949ff3a), [9c10d7198c8e](https://github.com/aurora-silicon/linux/commit/9c10d7198c8e56b16da35914ffde0eecb6bfb525) | DjDeveloperr | arm64: dts: apple: t8140: describe the GPU mailboxes as ASCWrap v6 [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — MBOX | B | +| [dbc9f011ec91](https://github.com/aurora-silicon/linux/commit/dbc9f011ec91fb72089a45487891bfd7c5a8939d), [86fba21bf7c9](https://github.com/aurora-silicon/linux/commit/86fba21bf7c926033306cb2dc5b6d5bcfbbe30ce), [018ab982644a](https://github.com/aurora-silicon/linux/commit/018ab982644a6eff94a6020d8910e9c8c187d6a2) | Ryan Murray | arm64: dts: apple: t8140: Add CPU energy model and thermal sensor cells | Active translated data — FW | B,P | +| [b15c2faba082](https://github.com/aurora-silicon/linux/commit/b15c2faba08274f02bf9a11e0e15eef470017dbd), [d693f725d774](https://github.com/aurora-silicon/linux/commit/d693f725d77422774ae683701732562a84789439), [d69c31212d2b](https://github.com/aurora-silicon/linux/commit/d69c31212d2b93b6396775c7890d5734f962c486) | Ryan Murray | arm64: dts: apple: t8140-j700: Add CPU thermal zones | Active translated data — FW | B,P | +| [25b99e9b6a75](https://github.com/aurora-silicon/linux/commit/25b99e9b6a75a9e783d7b5f04ee60b4f97fe24ed), [1ffe37e7af24](https://github.com/aurora-silicon/linux/commit/1ffe37e7af241fb91ecd7a8d17c946fd0a5d8ecc), [64eff8a46206](https://github.com/aurora-silicon/linux/commit/64eff8a46206613ff354cbeaff49d1354e4c1f52) | Ryan Murray | arm64: dts: apple: t8140: Drop the J700 CPU thermal policy property | Active translated data — FW | B,P | +| [db87f30fa4e7](https://github.com/aurora-silicon/linux/commit/db87f30fa4e7a322d655989316aef41a00fc46e5), [cf7dc9d0474c](https://github.com/aurora-silicon/linux/commit/cf7dc9d0474cc53e3716948365b72bacc848d5e9), [8ba3c01c426a](https://github.com/aurora-silicon/linux/commit/8ba3c01c426a8efd643289689493835c51b8937d) | Ryan Murray | arm64: dts: apple: t8140: Add measured CPU capacity values | Active translated data — FW | B,P | +| [6bb7669702ed](https://github.com/aurora-silicon/linux/commit/6bb7669702edf26c05f2be84f303d0fd612487b2), [2623391b533d](https://github.com/aurora-silicon/linux/commit/2623391b533de7f038408e57e3084a75f765b56c), [3dd2554de0c5](https://github.com/aurora-silicon/linux/commit/3dd2554de0c5b6e9d16a3809186872847987de6a) | Ryan Murray | arm64: dts: apple: enable the PMP fast-die limiter on J700 [#179](https://github.com/aurora-silicon/linux/pull/179) | Active — PMP | P,B | +| [47ed624515f4](https://github.com/aurora-silicon/linux/commit/47ed624515f40f37b39230c3b1b20bb6678c788a), [66988937c0d6](https://github.com/aurora-silicon/linux/commit/66988937c0d62aeb1ed828379cb83bbc68581afc), [39a036ead934](https://github.com/aurora-silicon/linux/commit/39a036ead934c701af76e05eebd72b102810861c) | Ryan Murray | arm64: dts: apple: t8140-j700: enable the PMP nodes once | Active — PMP | P,B | +| [a539d62f765f](https://github.com/aurora-silicon/linux/commit/a539d62f765fb0e8153f2be0fa8fb33058f6df9a), [c1af50661b44](https://github.com/aurora-silicon/linux/commit/c1af50661b44940df0c5747ae7dfca25c871bbcb), [d30b0dfcf358](https://github.com/aurora-silicon/linux/commit/d30b0dfcf35813ce52d935e24e7e6d46349ec3cd) | Ryan Murray | arm64: dts: apple: t8140-j700: Keep the idle E cluster out of its lowest OPP | Active translated data — FW | B,P | +| [fb68b124b886](https://github.com/aurora-silicon/linux/commit/fb68b124b8865e39b72b2387e7f739856f72cc26), [76dd6fb9e643](https://github.com/aurora-silicon/linux/commit/76dd6fb9e643747eb61c3b409c93b1ce2975184e), [2d51c6aada40](https://github.com/aurora-silicon/linux/commit/2d51c6aada40a6c97ae81464eea6935ebf70ba95) | Ryan Murray | arm64: dts: apple: t8140: Describe the memory cache controller | Active — MCC | M,P | +| [5811648de37d](https://github.com/aurora-silicon/linux/commit/5811648de37d1bcd66a5294f44191da8880947b2), [840606faf824](https://github.com/aurora-silicon/linux/commit/840606faf824b4f6ea862f8475e7edaff9af4150), [b5b71dfc50b2](https://github.com/aurora-silicon/linux/commit/b5b71dfc50b2bfbf5800a76c540f878924a678cb), [ed31b37fb032](https://github.com/aurora-silicon/linux/commit/ed31b37fb0327ee7c20961fce2785ed3d4ddb154) | DjDeveloperr | arm64: dts: apple: t8140: describe the PMC and report GFX to the PMP [#212](https://github.com/aurora-silicon/linux/pull/212) [#219](https://github.com/aurora-silicon/linux/pull/219) | Active after port — PMC | P,C,R | +| [15f21cbc0d4b](https://github.com/aurora-silicon/linux/commit/15f21cbc0d4bbbc901454d1e69f53084e1c5dc18), [6cab4decfb66](https://github.com/aurora-silicon/linux/commit/6cab4decfb664524936da59558bc39407b633b9a), [0272b63e30f0](https://github.com/aurora-silicon/linux/commit/0272b63e30f07b8c1a496cb01f5aa74e2cc1ff93) | Ryan Murray | arm64: dts: apple: t8140: keep the GPU on its PMGR state [#219](https://github.com/aurora-silicon/linux/pull/219) | Active translated data — FW | B,P | +| [0a44c956dde2](https://github.com/aurora-silicon/linux/commit/0a44c956dde28aad0bd37b66c6ff5d8096e5676d), [8fa836c5de81](https://github.com/aurora-silicon/linux/commit/8fa836c5de8111ab90d8f2a746526d2cd487afb7), [ae8e780a5b7d](https://github.com/aurora-silicon/linux/commit/ae8e780a5b7dedcb4a31adbd106fb9f9dbff8be0) | Ryan Murray | arm64: dts: apple: t8140: describe the PMC's DCS limit [#219](https://github.com/aurora-silicon/linux/pull/219) | Gated — DCS | P | +| [84eb6b53da34](https://github.com/aurora-silicon/linux/commit/84eb6b53da3453d7c2838bd2a41779e0a9bce181), [f01e1cde7d9f](https://github.com/aurora-silicon/linux/commit/f01e1cde7d9f270e156ee77c6819466757b938f6), [a037985ac960](https://github.com/aurora-silicon/linux/commit/a037985ac9605eb76fe51a05b172b022ac1f0005) | Ryan Murray | arm64: dts: apple: t8140-j700: limit the DCS vote when the die runs hot [#219](https://github.com/aurora-silicon/linux/pull/219) | Gated — DCS | P | +| [4f6bac29d934](https://github.com/aurora-silicon/linux/commit/4f6bac29d9349ced88686239f259ab2747207708), [e10ab48a4590](https://github.com/aurora-silicon/linux/commit/e10ab48a4590c23c664546f524650310222b3a35), [c1bae63595e8](https://github.com/aurora-silicon/linux/commit/c1bae63595e8fc7b18e79664ee6aa6b4536f5d95) | Ryan Murray | arm64: dts: apple: t8140: Keep the GPU power state on across sleep | Active translated data — FW | B,P | +| [01d1be596840](https://github.com/aurora-silicon/linux/commit/01d1be596840358a7258d87fc91d3201a3287313), [8b6371b5500d](https://github.com/aurora-silicon/linux/commit/8b6371b5500d062c00f71d46a0bccbcbed111edd) | Ryan Murray | iommu: apple-dart: Program T8140 and T8142 streams individually | Active — DART; `drivers/iommu/apple-dart.c` | D,B | +| [2398a03f445e](https://github.com/aurora-silicon/linux/commit/2398a03f445e8f9f65fa9f86748a3deeab3e1b1f), [69c39f85461e](https://github.com/aurora-silicon/linux/commit/69c39f85461e8944dff387eab703c58ea6c6ed49), [1d2904fd3301](https://github.com/aurora-silicon/linux/commit/1d2904fd3301c63620f07c81ae79f2486a81a9a6) | Dj | iommu/apple-dart: defer locked root publication until attach | Active — DART; `drivers/iommu/apple-dart.c` | D,B | +| [ae5849165749](https://github.com/aurora-silicon/linux/commit/ae58491657494b0f56ec0113a3aa43612648e242), [44c7f32baad0](https://github.com/aurora-silicon/linux/commit/44c7f32baad0511e6f3ae5c2ffdd863eec8e5d23), [dc9d13edb0e5](https://github.com/aurora-silicon/linux/commit/dc9d13edb0e5b18c61d25240ed4ba532afb8d11f) | Acelogic | iommu/apple-dart: accept T8142 firmware slots that mirror Linux mappings | Active — DART; `drivers/iommu/apple-dart.c` | D,B | +| [1917cbce81cb](https://github.com/aurora-silicon/linux/commit/1917cbce81cb806498b0a5a3f5e636c851255408), [61dfeb8f659d](https://github.com/aurora-silicon/linux/commit/61dfeb8f659d36149d3dc6162d304136b108c5b9), [c361702757bf](https://github.com/aurora-silicon/linux/commit/c361702757bfccb9183ed79be0c6200fb3722e6f) | Ryan Murray | iommu/apple-dart: defer publication until locked roots are known | Active — DART; `drivers/iommu/apple-dart.c` | D,B | +| [c74dc14619ec](https://github.com/aurora-silicon/linux/commit/c74dc14619ec5467789855d89c0ef224a3655093), [35f59a797e91](https://github.com/aurora-silicon/linux/commit/35f59a797e914308f4ae289e87551ad97997a497), [d957b0bd713b](https://github.com/aurora-silicon/linux/commit/d957b0bd713bbf4617f54f5ed0d274964e9af6c9) | Ryan Murray | iommu/apple-dart: validate software mappings against inherited slots | Active — DART; `drivers/iommu/apple-dart.c` | D,B | +| [f89a3c873999](https://github.com/aurora-silicon/linux/commit/f89a3c87399911e0faa4f66091b2e7dcc1145825), [64013b9557ab](https://github.com/aurora-silicon/linux/commit/64013b9557abadab17883a7e697f32f32e0c4160), [707f28a4c399](https://github.com/aurora-silicon/linux/commit/707f28a4c399092ba01bd281a3f0b6623f7bb223) | Ryan Murray | iommu/apple-dart: import firmware translations on T8140 | Active — DART; `drivers/iommu/apple-dart.c` | D,B | +| [99633bd5454e](https://github.com/aurora-silicon/linux/commit/99633bd5454eac0e455812358be63b4f08d78a98), [c29b618f6340](https://github.com/aurora-silicon/linux/commit/c29b618f6340b4ede94747a266f86a844344e769), [ebcff4ba64dd](https://github.com/aurora-silicon/linux/commit/ebcff4ba64dd90983cc8150b0f46fda82b5fb9f0) | Ryan Murray | iommu: apple-dart: retain inherited translations before reservation setup | Active — DART; `drivers/iommu/apple-dart.c` | D,B | +| [8d2d7790d8d5](https://github.com/aurora-silicon/linux/commit/8d2d7790d8d552f01ce2bf10f922aaccbf7918aa), [dbd410951406](https://github.com/aurora-silicon/linux/commit/dbd4109514067a2ed8140d8138bee68453272f02), [8478ae014201](https://github.com/aurora-silicon/linux/commit/8478ae0142014d170a62df6dd94cc66bd989627b) | Chris Kearney | iommu/apple-dart: restore legacy locked display domain handoff | Active — DART; `drivers/iommu/Kconfig` | D,B | +| [0297adcecc72](https://github.com/aurora-silicon/linux/commit/0297adcecc72a563b5cfb3ca3e483f71422fb77c), [b71446526889](https://github.com/aurora-silicon/linux/commit/b71446526889b27c60461685c3c320b7e123155d), [2bad3dba632f](https://github.com/aurora-silicon/linux/commit/2bad3dba632ff85537392b79255fc527baf01746) | Ryan Murray | iommu: apple-dart: keep inherited firmware translations visible before attach | Active — DART; `drivers/iommu/apple-dart.c` | D,B | +| [80ce713f5251](https://github.com/aurora-silicon/linux/commit/80ce713f52511abe4d35b35bfac0f10f9072d401), [8ece17f482f1](https://github.com/aurora-silicon/linux/commit/8ece17f482f11da6e81915b98fc8b93ed796504a), [a64a4771b276](https://github.com/aurora-silicon/linux/commit/a64a4771b27602bf5a72b413b6bc9a868892992a) | Ryan Murray | iommu: apple-dart: invalidate T8110 ranges by device DVA [#221](https://github.com/aurora-silicon/linux/pull/221) | Active — DART; `drivers/iommu/apple-dart.c` | D,B | +| [cef9a23aa45e](https://github.com/aurora-silicon/linux/commit/cef9a23aa45e3ac6b352e6c2bd839935374ae351), [253ba9bebf47](https://github.com/aurora-silicon/linux/commit/253ba9bebf470c0d78845103c93b937f2d9cdfb3), [dd5ac6fdffb0](https://github.com/aurora-silicon/linux/commit/dd5ac6fdffb03d048df510198af6e8d53dceb17d) | Ryan Murray | iommu: apple-dart: leave live firmware translations out of invalidates [#221](https://github.com/aurora-silicon/linux/pull/221) | Active — DART; `drivers/iommu/apple-dart.c` | D,B | +| [0a73b0299574](https://github.com/aurora-silicon/linux/commit/0a73b0299574da4c42659024b4a0e8390c36d2d8), [bc6682c215ad](https://github.com/aurora-silicon/linux/commit/bc6682c215adc6febfb86c9fa2ed0fb69657dcc2), [9c512b25f55e](https://github.com/aurora-silicon/linux/commit/9c512b25f55eaf6332b590e5a205ca7d55de12c8) | Ryan Murray | iommu: apple-dart: invalidate client-ranged T8140 streams again [#232](https://github.com/aurora-silicon/linux/pull/232) | Active — DART; `drivers/iommu/apple-dart.c` | D,B | +| [ae812d47ea17](https://github.com/aurora-silicon/linux/commit/ae812d47ea176c9bb0b4ddccc17349fee5afbf1a), [fe15676f6e03](https://github.com/aurora-silicon/linux/commit/fe15676f6e03395f4947cbffb2d49fbee17ea5f9), [4ca4ba380552](https://github.com/aurora-silicon/linux/commit/4ca4ba3805528f5370bbe6bab3d8dbb8f7f9226c) | Ryan Murray | iommu: apple-dart: invalidate four-level T8140 streams by DVA range [#237](https://github.com/aurora-silicon/linux/pull/237) | Active — DART; `drivers/iommu/apple-dart.c` | D,B | +| [a17634424697](https://github.com/aurora-silicon/linux/commit/a17634424697b082221d141ef61d9689460eebf8), [d1a5c65245b8](https://github.com/aurora-silicon/linux/commit/d1a5c65245b87c457a8fc4b4190d440e9bc38b8d) | Sasha Finkelstein | soc: apple: rtkit: Add tracekit endpoint. | Active — RTKIT; `drivers/soc/apple/rtkit.c` | B,F | +| [ff7d81511780](https://github.com/aurora-silicon/linux/commit/ff7d81511780770e944ebe6e66ed56a06b497a31) | Ryan Murray | drm/asahi: Adapt to the Linux 7.3 Rust APIs [#375](https://github.com/aurora-silicon/linux/pull/375) | Active — MEM; `drivers/gpu/drm/asahi/debug.rs` | M,D | +| [588c231eaa54](https://github.com/aurora-silicon/linux/commit/588c231eaa54f4a34872519517fbbcc079b41175) | Ryan Murray | soc: apple: Adapt the Rust SEP, PMP and AOP drivers to Linux 7.3 | Active — PMP; `drivers/soc/apple/aop.rs` | P,B | +| [c025e6cd0615](https://github.com/aurora-silicon/linux/commit/c025e6cd0615e416e0a06ba997e8d9f417a3e185), [af0890cf33cf](https://github.com/aurora-silicon/linux/commit/af0890cf33cf899cf0005daf6138da1dfe573c19) | Ryan Murray | soc: apple: rtkit: Harden syslog buffer handling | Active — RTKIT; `drivers/soc/apple/rtkit.c` | B,F | +| [3a8760c28c33](https://github.com/aurora-silicon/linux/commit/3a8760c28c3341380e105c75bb48e76c895d3f3d), [c01af1b31c52](https://github.com/aurora-silicon/linux/commit/c01af1b31c523b09106c4bed43f8bb7cba65cbfc) | Ryan Murray | soc: apple: rtkit: Keep a live shared buffer on a repeated request | Active — RTKIT; `drivers/soc/apple/rtkit.c` | B,F | +| [495b2f037c28](https://github.com/aurora-silicon/linux/commit/495b2f037c284f4b7b10f54c90a7d34fbf5e99fa), [6b7e37786882](https://github.com/aurora-silicon/linux/commit/6b7e37786882db803e9de1352a2af623044f9237), [1fc9ea993905](https://github.com/aurora-silicon/linux/commit/1fc9ea9939058bc1307565f036506da30dd62908) | Ryan Murray | soc: apple: rtkit: Serialize receiver ownership through teardown | Active — MBOX; `drivers/soc/apple/mailbox.c` | B,O | +| [335eaeb9b9f1](https://github.com/aurora-silicon/linux/commit/335eaeb9b9f147230f464a140287e820b1d78518), [0593ad398a6e](https://github.com/aurora-silicon/linux/commit/0593ad398a6eb83e2e2ce9dd25bf1fd0768e5bcd) | Ryan Murray | soc: apple: rtkit: Add apple_rtkit_free_retaining_buffers() | Active — RTKIT; `drivers/soc/apple/rtkit.c` | B,F | +| [e796f5fbaf06](https://github.com/aurora-silicon/linux/commit/e796f5fbaf0698b478c43ba6eb15e046c9cc6244), [8527354650e8](https://github.com/aurora-silicon/linux/commit/8527354650e810479ceb0a58417e683b2dee852c) | Ryan Murray | soc: apple: rtkit: Allow streaming-mapped shared buffers | Active — RTKIT; `drivers/soc/apple/rtkit.c` | B,F | +| [9191296f1898](https://github.com/aurora-silicon/linux/commit/9191296f1898d1acf2d0cd500230ae80c9e3e0ca), [88e3c5c07943](https://github.com/aurora-silicon/linux/commit/88e3c5c07943fedf73e45f88b270f8550d7691ae) | Ryan Murray | soc: apple: rtkit: Support adopting a running co-processor | Active — RTKIT; `drivers/soc/apple/rtkit-internal.h` | B,F | +| [1932e1e1b184](https://github.com/aurora-silicon/linux/commit/1932e1e1b18437408e77dcc1d23d6d966b0fdb98), [f232f22bef54](https://github.com/aurora-silicon/linux/commit/f232f22bef54eb6fbd848a1374e49fe91035b397) | Ryan Murray | soc: apple: rtkit: Report crashes of an adopted co-processor | Active — RTKIT; `drivers/soc/apple/rtkit-internal.h` | B,F | +| [b38944203ab0](https://github.com/aurora-silicon/linux/commit/b38944203ab0ad8ec01c26bd67df67455fcc98c3), [4f0416f2c43b](https://github.com/aurora-silicon/linux/commit/4f0416f2c43ba895b3c34a263cf5d0e070095a55), [2768025e0f1d](https://github.com/aurora-silicon/linux/commit/2768025e0f1d9792f84e9fbb99f6c0b155c0a22a), [35b2acbf474d](https://github.com/aurora-silicon/linux/commit/35b2acbf474d8505c3edc1a2749a8d6fbfaf7a09) | Ryan Murray | pmdomain: apple: Print the read-back state on a power state timeout | Active — OBS; `drivers/pmdomain/apple/pmgr-pwrstate.c` | O,F | +| [768a665d0ca1](https://github.com/aurora-silicon/linux/commit/768a665d0ca138d0f2c80029fd1d272c11468e2e), [9c5b8fe6d6d6](https://github.com/aurora-silicon/linux/commit/9c5b8fe6d6d6a93f7c4bbbd14026fd93d5f6aa73), [afe19efeff9c](https://github.com/aurora-silicon/linux/commit/afe19efeff9c466486fb723f7066f86b9a3e71d5), [9dc79fb25c92](https://github.com/aurora-silicon/linux/commit/9dc79fb25c9294bd7a8ecb00acc9bcea379c3d1a) | Ryan Murray | pmdomain: apple: Mask BUSY in power state writes and ratelimit errors | Active — PMGR; `drivers/pmdomain/apple/pmgr-pwrstate.c` | B,F | +| [a6aa625a9b35](https://github.com/aurora-silicon/linux/commit/a6aa625a9b35b96ba4ffc0749d55a0f21fe9e426), [03f4792761f4](https://github.com/aurora-silicon/linux/commit/03f4792761f42c3a4581be4b585a479cfc65bfc3), [f82e88f4049e](https://github.com/aurora-silicon/linux/commit/f82e88f4049ed6ea278717be6d922f8e347aab13), [928641d26be3](https://github.com/aurora-silicon/linux/commit/928641d26be3083dc06c0de072ce99537fa059b7) | DjDeveloperr | soc: apple: rtkit: Support co-processors that place their own buffers [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — RTKIT; `drivers/soc/apple/rtkit.c` | B,F | +| [e52b589758bd](https://github.com/aurora-silicon/linux/commit/e52b589758bd9c72db728ef41947aadc2d1eec3f), [a8e468104169](https://github.com/aurora-silicon/linux/commit/a8e46810416977c034c45d805c0b504cfdab9a6a), [c07d044a0311](https://github.com/aurora-silicon/linux/commit/c07d044a03117705721c78500bc36489ea34e6c4) | Ryan Murray | soc: apple: rtkit: recognise an initial crash log in adopted sessions | Active — RTKIT; `drivers/soc/apple/rtkit-crashlog.c` | B,F | +| [8420f4272083](https://github.com/aurora-silicon/linux/commit/8420f42720830b9e5bad37647b6eead5303bc566), [352b68249894](https://github.com/aurora-silicon/linux/commit/352b682498947d0062635916a9d6902165ab38b5), [2f434459eb09](https://github.com/aurora-silicon/linux/commit/2f434459eb094edc046cd2ff71f1901302d7ef3d) | Ryan Murray | soc: apple: rtkit: reject an already owned mailbox receiver | Active — RTKIT; `drivers/soc/apple/rtkit.c` | B,F | +| [791f8c37bad3](https://github.com/aurora-silicon/linux/commit/791f8c37bad39ada4186e7db9efb74472ef1670b), [44a668c87df1](https://github.com/aurora-silicon/linux/commit/44a668c87df18147ca0c9ace22e897e4c65b7620), [41e7892e40eb](https://github.com/aurora-silicon/linux/commit/41e7892e40eb21ddb08b2391a7940ed6499daf8d) | Ryan Murray | soc: apple: pmp: use a 42-bit DMA mask on T8140 [#179](https://github.com/aurora-silicon/linux/pull/179) | Active — PMP; `drivers/soc/apple/pmp.rs` | P,B | +| [7a9913ea25e2](https://github.com/aurora-silicon/linux/commit/7a9913ea25e2d95d3ebe8f0392fec92979d08728), [493080d07b91](https://github.com/aurora-silicon/linux/commit/493080d07b918a2025fb79ff987d0dc9aeb294fe), [f69f1b76fcb0](https://github.com/aurora-silicon/linux/commit/f69f1b76fcb04e6bd7d7d066654e612be9761af0) | Ryan Murray | pmdomain: apple: pmp-report: expose T8140 fast-die effort [#179](https://github.com/aurora-silicon/linux/pull/179) | Active — PMP; `drivers/pmdomain/apple/pmp-report.c` | P,B | +| [1a665f53f540](https://github.com/aurora-silicon/linux/commit/1a665f53f540a523d8df2c11d11a824f933b0d14), [8410c0eac2d9](https://github.com/aurora-silicon/linux/commit/8410c0eac2d9419a33c6a8d4a00bd14059f94610), [4f3e0ed14a9c](https://github.com/aurora-silicon/linux/commit/4f3e0ed14a9c280aafb3642619e7335413592e06) | Ryan Murray | pmdomain: apple: drain PMP report users before teardown | Active — PMP; `drivers/pmdomain/apple/pmp-report.c` | P,B | +| [50b59e9b8f43](https://github.com/aurora-silicon/linux/commit/50b59e9b8f43e49c14a8a5e426f7891ac5f5e8c7), [061cf8fae73f](https://github.com/aurora-silicon/linux/commit/061cf8fae73f12de8d6720602ccac2aec250a20f), [f4ec00ff19db](https://github.com/aurora-silicon/linux/commit/f4ec00ff19dbfdc6d9bdbf03e8d7ec2798502f1a) | Ryan Murray | soc: apple: Enable T8140 system level cache allocation | Active — MCC; `drivers/soc/apple/Kconfig` | M,P | +| [628dbbe941d2](https://github.com/aurora-silicon/linux/commit/628dbbe941d25e5153dc62cb04104f846f3ffc48), [af915d02f5f6](https://github.com/aurora-silicon/linux/commit/af915d02f5f6022114a46e302642e915f9b22a01), [8b7d7f837ff6](https://github.com/aurora-silicon/linux/commit/8b7d7f837ff68f7fa37f6aaae55114bbd15efb43), [132cee4161dc](https://github.com/aurora-silicon/linux/commit/132cee4161dcff4307c55314f2a6189a00e7b0df) | DjDeveloperr | pmdomain: apple: add PMC voter interface driver [#212](https://github.com/aurora-silicon/linux/pull/212) [#219](https://github.com/aurora-silicon/linux/pull/219) | Active after port — PMC; `drivers/pmdomain/apple/Kconfig` | P,C,R | +| [698f429f4256](https://github.com/aurora-silicon/linux/commit/698f429f42566c059e7788e4efd44978a5db33a9), [9e840cf32336](https://github.com/aurora-silicon/linux/commit/9e840cf323360eabdb61223cf73198f4f4f3179d), [6454e02db2a5](https://github.com/aurora-silicon/linux/commit/6454e02db2a562d82adfbc4447ffe093f43dae0a), [5bd867f5c339](https://github.com/aurora-silicon/linux/commit/5bd867f5c33919ed65c81ead1e31595bb6bb91c5) | DjDeveloperr | soc: apple: add host performance controller floors [#212](https://github.com/aurora-silicon/linux/pull/212) [#219](https://github.com/aurora-silicon/linux/pull/219) | Active — CLPC; `drivers/soc/apple/Kconfig` | P,C,R,I | +| [faff1f7acf75](https://github.com/aurora-silicon/linux/commit/faff1f7acf752284972af5d469f91186c1e28552), [1c69f57e2c3e](https://github.com/aurora-silicon/linux/commit/1c69f57e2c3e5ba7cea2db4708242b7b538b15b2), [e736b2a049bf](https://github.com/aurora-silicon/linux/commit/e736b2a049bf708512f845093f712c431c20da3b) | Ryan Murray | pmdomain: apple: pmc: report the honoured floor of each agent [#219](https://github.com/aurora-silicon/linux/pull/219) | Active — PMC; `drivers/pmdomain/apple/pmc.c` | P,C,R | +| [9afc8195e8e2](https://github.com/aurora-silicon/linux/commit/9afc8195e8e2b3b4da25805f0b0c53baac16f1de), [e3f7747b88a3](https://github.com/aurora-silicon/linux/commit/e3f7747b88a38a8f2db51a63b323bb6fe3a25157), [cf8edfc522ce](https://github.com/aurora-silicon/linux/commit/cf8edfc522ce82871acc421119e324b56ab2813f) | Ryan Murray | pmdomain: apple: pmc: limit the DCS rail as a cooling device [#219](https://github.com/aurora-silicon/linux/pull/219) | Gated — DCS; `drivers/pmdomain/apple/pmc.c` | P | +| [c809017f2926](https://github.com/aurora-silicon/linux/commit/c809017f2926613cc9b683189c5e37aa6236c647), [bbbc7f0f2526](https://github.com/aurora-silicon/linux/commit/bbbc7f0f2526501df99e7d40c7a14a5bcb3c168d), [99bf4fc4c39c](https://github.com/aurora-silicon/linux/commit/99bf4fc4c39cbe59493531b7682f0a7eb60ac2e0) | Ryan Murray | pmdomain: apple: build the PMC driver by default [#219](https://github.com/aurora-silicon/linux/pull/219) | Active — PMC; `drivers/pmdomain/apple/Kconfig` | P,C,R | +| [413512c8c6c7](https://github.com/aurora-silicon/linux/commit/413512c8c6c7503e21b2d845b7072f3e67985e80), [afbf48da68ef](https://github.com/aurora-silicon/linux/commit/afbf48da68ef244414db4e174b630cf95ed33bd2), [d0f012d7b92f](https://github.com/aurora-silicon/linux/commit/d0f012d7b92f9967d42c35a70bc09c2b87274c0b) | Ryan Murray | soc: apple: clpc: do not count offline CPUs as busy [#219](https://github.com/aurora-silicon/linux/pull/219) | Active — CLPC; `drivers/soc/apple/clpc.c` | P,C,R,I | +| [a1517450f0d6](https://github.com/aurora-silicon/linux/commit/a1517450f0d6afa99ed5a5f2acbbc46350fdccd5), [a0301cd2e0fb](https://github.com/aurora-silicon/linux/commit/a0301cd2e0fb788836d8ef4d09b397a478891a72), [ddc216cd42cb](https://github.com/aurora-silicon/linux/commit/ddc216cd42cb7c7ebc831d3fafca17df244b280e) | Ryan Murray | soc: apple: clpc: stop waking idle CPUs and release the floor in sleep [#219](https://github.com/aurora-silicon/linux/pull/219) | Active — CLPC; `drivers/soc/apple/clpc.c` | P,C,R,I | +| [17c6097b09dd](https://github.com/aurora-silicon/linux/commit/17c6097b09ddd81a31a618e1d2447c3fdfed87ce), [ebd97e7441ff](https://github.com/aurora-silicon/linux/commit/ebd97e7441ff7ca5f7169f6e2bf550e7fb1438e0), [4f13ab717cb3](https://github.com/aurora-silicon/linux/commit/4f13ab717cb3a5ab19e5c097084ff2c676457f37) | Ryan Murray | pmdomain: apple: pmp-report: keep entry domains bound [#223](https://github.com/aurora-silicon/linux/pull/223) | Active — PMP; `drivers/pmdomain/apple/pmp-report.c` | P,B | +| [bd2a439af1fe](https://github.com/aurora-silicon/linux/commit/bd2a439af1feefbb19cec4af61a28ae81a7fe479), [6733b19981f1](https://github.com/aurora-silicon/linux/commit/6733b19981f1d70f51264f609582025889e083db), [59540c7f72b3](https://github.com/aurora-silicon/linux/commit/59540c7f72b3721d293fb36cc5fd5bf79e1f47bc) | Ryan Murray | pmdomain: apple: Undo a power state transition that times out | Active — PMGR; `drivers/pmdomain/apple/pmgr-pwrstate.c` | B,F | +| [afa52a4aacd2](https://github.com/aurora-silicon/linux/commit/afa52a4aacd24be61e6963bcfa96a673fcd0f7fd), [b1dadbe56c1a](https://github.com/aurora-silicon/linux/commit/b1dadbe56c1ac98c1687aacd6f9a4ffd45d44802) | Acelogic | soc: apple: rtkit: Allow AP readiness before the IOP power acknowledgement | Active — RTKIT; `drivers/soc/apple/rtkit-helper.c` | B,F | +| [80223777e1c0](https://github.com/aurora-silicon/linux/commit/80223777e1c0fcdd18cdf7dbb693389f18bffe66), [7de139272456](https://github.com/aurora-silicon/linux/commit/7de1392724567cdab869ed7102a23642d6e64320), [b5c2960b4b44](https://github.com/aurora-silicon/linux/commit/b5c2960b4b441fa51b7f0ccdbaa17eae8a9de4bc) | Ryan Murray | soc: apple: rtkit: identify failed boot handshake stages | Active — RTKIT; `drivers/soc/apple/rtkit.c` | B,F | +| [e02a7f13d77d](https://github.com/aurora-silicon/linux/commit/e02a7f13d77d0c450e6e663da89160b8e2e6a9de), [3a165a5f418d](https://github.com/aurora-silicon/linux/commit/3a165a5f418df4ca2445c14657bdc6dbe39bb223), [50a711417171](https://github.com/aurora-silicon/linux/commit/50a711417171e48a07340999bf43bb8c281b14d7) | Eryk Wieliczko | pmdomain: apple: leave firmware-owned power domains untouched | Active — PMGR; `drivers/pmdomain/apple/pmgr-pwrstate.c` | B,F | +| [15a254a94f16](https://github.com/aurora-silicon/linux/commit/15a254a94f1685a5550aec6c99a3270bfb389951), [e271de959b7d](https://github.com/aurora-silicon/linux/commit/e271de959b7de2e436f15415329d175e2e158517) | Ryan Murray | soc: apple: rtkit: Recognise opaque OSLog events 3 to 5 | Active — RTKIT; `drivers/soc/apple/rtkit.c` | B,F | +| [faadb28854ca](https://github.com/aurora-silicon/linux/commit/faadb28854caaf534df60f6d629eca87c963344f), [aa486de92319](https://github.com/aurora-silicon/linux/commit/aa486de923198c933849bb2172f5ddad4f4710b6), [6559da7d50ab](https://github.com/aurora-silicon/linux/commit/6559da7d50ab10da722b3719f447836b12427159), [7810f444ae5a](https://github.com/aurora-silicon/linux/commit/7810f444ae5a403773baf8d4f460e29cb2375cf5) | Ryan Murray | cpufreq: apple-soc: Use related_cpus when removing OPP tables at exit | Active — CPU; `drivers/cpufreq/apple-soc-cpufreq.c` | C,I,P | +| [f630f4e6a724](https://github.com/aurora-silicon/linux/commit/f630f4e6a724b62182875eeb75e3559f6304b286), [4df394b4c36b](https://github.com/aurora-silicon/linux/commit/4df394b4c36be04ca63fabae2c630bfdda20188a), [31ce0bec7173](https://github.com/aurora-silicon/linux/commit/31ce0bec7173eb3fbc1067e6dba46c2ca2f4dc8e) | Ryan Murray | cpufreq: apple-soc: Add T8140 support | Active — CPU; `drivers/cpufreq/apple-soc-cpufreq.c` | C,I,P | +| [116ec26bc074](https://github.com/aurora-silicon/linux/commit/116ec26bc074456b670823002c81ccffc5fbbecc), [2e97d372534e](https://github.com/aurora-silicon/linux/commit/2e97d372534e73b72b9b36b9debaa3caa9ae5ab6), [b0a02dcbfded](https://github.com/aurora-silicon/linux/commit/b0a02dcbfded7f1485db7f37694712892025a4d9) | Ryan Murray | cpufreq: apple-soc: enforce PMP fast-die limits through QoS [#179](https://github.com/aurora-silicon/linux/pull/179) | Active — PMP; `drivers/cpufreq/apple-soc-cpufreq.c` | P,B | +| [3a1ff1c99f6a](https://github.com/aurora-silicon/linux/commit/3a1ff1c99f6a35163c77f9c2cc5f6b19d4fe6242), [9d74a86460df](https://github.com/aurora-silicon/linux/commit/9d74a86460df995f3522f3267c3f3fa34d79a47e) | Ryan Murray | cpufreq: apple-soc: keep required fallback limits with an older DT | Active — CPU; `drivers/cpufreq/apple-soc-cpufreq.c` | C,I,P | +| [1248cb920813](https://github.com/aurora-silicon/linux/commit/1248cb9208135326d9724eb1a83e3c67d25c3664), [a40bdafc42e5](https://github.com/aurora-silicon/linux/commit/a40bdafc42e5dcf8c0c4bc728f883c7e3d48bffe), [a61bca6382a7](https://github.com/aurora-silicon/linux/commit/a61bca6382a765f5b74134dea3b094039f2a15bd) | Ryan Murray | cpufreq: apple-soc: tidy includes and the ->exit() callback [#317](https://github.com/aurora-silicon/linux/pull/317) | Active — CPU; `drivers/cpufreq/apple-soc-cpufreq.c` | C,P | +| [c52554b353b4](https://github.com/aurora-silicon/linux/commit/c52554b353b4fcecf684ea648e4b1ab5f896b677), [143dc7bec441](https://github.com/aurora-silicon/linux/commit/143dc7bec44131ac467fa21049f2d63c52783d52), [e7d3d687d207](https://github.com/aurora-silicon/linux/commit/e7d3d687d20745259fcb83af83c9ed377ff6a1f3) | Ryan Murray | cpufreq: apple-soc: describe the unmanaged P-state ceiling as data [#317](https://github.com/aurora-silicon/linux/pull/317) | Active — CPU; `drivers/cpufreq/apple-soc-cpufreq.c` | C,P | +| [e71cc1ccab4e](https://github.com/aurora-silicon/linux/commit/e71cc1ccab4eaefdf63d66104107c9604ab04121), [cd8875cfd1bf](https://github.com/aurora-silicon/linux/commit/cd8875cfd1bfaa80b1b1dbd7e27ea234d129471b), [cfe897d4abac](https://github.com/aurora-silicon/linux/commit/cfe897d4abac70a3d9703ea7f4c082ed89da7a96) | Ryan Murray | rust: soc: apple: rtkit: Add shutdown() and a buffer-retaining drop | Active — RTKIT; `rust/kernel/soc/apple/rtkit.rs` | B,F | +| [10dd5c21ef72](https://github.com/aurora-silicon/linux/commit/10dd5c21ef7211836beaa47f6f3fd3e250d36866), [8779ecb38c89](https://github.com/aurora-silicon/linux/commit/8779ecb38c89c103617c4ea24207fef96472ebc4), [06914e1d4995](https://github.com/aurora-silicon/linux/commit/06914e1d499555a96e0beccccc0b5485051108cd) | Ryan Murray | soc: apple: rtkit: Handle oslog notifications quietly | Active — RTKIT; `drivers/soc/apple/rtkit.c` | B,F | +| [9a4cf171ba9e](https://github.com/aurora-silicon/linux/commit/9a4cf171ba9edabdd0f0f715468d42689791adb7), [aa18e91ac6fc](https://github.com/aurora-silicon/linux/commit/aa18e91ac6fce29dcc8e69aac619fab3fda7ee85), [904945425463](https://github.com/aurora-silicon/linux/commit/904945425463c0450cb19582afeb4b935175ae51), [201afd84ae50](https://github.com/aurora-silicon/linux/commit/201afd84ae50008095068d028af22ecf4c1356c5) | DjDeveloperr | drm/asahi: use const float bit conversion supported by Rust minimum [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — MEM; `drivers/gpu/drm/asahi/float.rs` | M,D | +| [c9be90533ae0](https://github.com/aurora-silicon/linux/commit/c9be90533ae06ac64dfe5e35b121cf30a1cc37f0), [6b610ee95063](https://github.com/aurora-silicon/linux/commit/6b610ee95063a8d047c1ee49c65ee3599ee24a92), [94b86f96bd9e](https://github.com/aurora-silicon/linux/commit/94b86f96bd9ef2275c99ddf5238374e615b2f510), [12f0a07a6166](https://github.com/aurora-silicon/linux/commit/12f0a07a61666305a1efe1a1f4c4c88a013abafb) | DjDeveloperr | rust: drm: sched: Fix job accounting when cancelling jobs [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — DRM; `rust/kernel/drm/sched.rs` | C,H,M,F | +| [7ba04b60d2ca](https://github.com/aurora-silicon/linux/commit/7ba04b60d2caf298bb9119b359629feedfec38c5), [43d53c0d4f56](https://github.com/aurora-silicon/linux/commit/43d53c0d4f5660bc6d300e6626d1f9a05d832c79), [15a7eae7964f](https://github.com/aurora-silicon/linux/commit/15a7eae7964f94a16fd273fc3b25a22ad2c27bab), [f8102be460b6](https://github.com/aurora-silicon/linux/commit/f8102be460b66b12af865d6250e73ffc6f99c73a) | DjDeveloperr | rust: drm: sched: Keep the scheduler alive while jobs reference it [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — DRM; `rust/kernel/drm/sched.rs` | C,H,M,F | +| [d1ebc542fe8e](https://github.com/aurora-silicon/linux/commit/d1ebc542fe8e1082a1c6e311b72d4d6c609c940d), [e4c63b3dbab0](https://github.com/aurora-silicon/linux/commit/e4c63b3dbab04bda2a798b913dd5e46b8799290a), [7d650853ea8e](https://github.com/aurora-silicon/linux/commit/7d650853ea8e5de3f512a36b69f8653014de83b4), [f1f985d57681](https://github.com/aurora-silicon/linux/commit/f1f985d576810c7183ea9bcd1b701cb284927ac6) | DjDeveloperr | rust: drm: sched: Do not stop the scheduler before tearing it down [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — DRM; `rust/kernel/drm/sched.rs` | C,H,M,F | +| [45ade507d593](https://github.com/aurora-silicon/linux/commit/45ade507d593cb69929a4daf5d1d50159d729126), [1bc200c44668](https://github.com/aurora-silicon/linux/commit/1bc200c446687bc2eb2b6abc19f1d9b5a3ad59ee), [9171e855e6d5](https://github.com/aurora-silicon/linux/commit/9171e855e6d58a12059e265a7e3907f44ffa70a9), [14d7b3c379da](https://github.com/aurora-silicon/linux/commit/14d7b3c379da4c764581d4c34aff495d7346af72) | DjDeveloperr | rust: drm: sched: let drivers handle timeouts without a reset [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — DRM; `rust/kernel/drm/sched.rs` | C,H,M,F | +| [996d255ea104](https://github.com/aurora-silicon/linux/commit/996d255ea104c999554b5c5584d2b59c6eed3cd9), [59b1e2a30011](https://github.com/aurora-silicon/linux/commit/59b1e2a300116dec7170b0b4a44e4b94a39c372b), [630c43dac20e](https://github.com/aurora-silicon/linux/commit/630c43dac20e9621d3c2cd687c98ab85d51c8b9e), [e0a5c5ad84cb](https://github.com/aurora-silicon/linux/commit/e0a5c5ad84cbc6e48c0194882b8c1c67d72538a8) | DjDeveloperr | drm/asahi: uapi: Add the G17 additions [#171](https://github.com/aurora-silicon/linux/pull/171) | Active translated data — FW; `drivers/gpu/drm/asahi/file.rs` | B,C,R | +| [2f298100e9d5](https://github.com/aurora-silicon/linux/commit/2f298100e9d59b05f3c71d9e27d8079a63a0b6bc), [4a9cf4453a2d](https://github.com/aurora-silicon/linux/commit/4a9cf4453a2d0349848c89bd2028e8e6757263a7), [e46d8cfd391b](https://github.com/aurora-silicon/linux/commit/e46d8cfd391b00b302d5a9ff4608b82c3714c910), [aae9f7d3525f](https://github.com/aurora-silicon/linux/commit/aae9f7d3525f93a7a5b5ae0529f8e85219735d08) | DjDeveloperr | drm/asahi: separate DRM-facing GPU operations from the firmware manager [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — MEM; `drivers/gpu/drm/asahi/driver.rs` | M,D | +| [3291ed172be2](https://github.com/aurora-silicon/linux/commit/3291ed172be2a63252d34367f8f5d8baebf2e537), [cac96891a49f](https://github.com/aurora-silicon/linux/commit/cac96891a49fd1416cb3bb0f68922cbebb7618c1), [54721b0f0bee](https://github.com/aurora-silicon/linux/commit/54721b0f0bee7be5b3094112ffcfa36e76dec7ed), [7d3ddb6c35e1](https://github.com/aurora-silicon/linux/commit/7d3ddb6c35e1c478f68c78ac8bb8b3ee9d314e58) | DjDeveloperr | drm/asahi: mmu: support per-SoC UAT geometry and user VA range [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — MEM; `drivers/gpu/drm/asahi/file.rs` | M,D | +| [a0ae891ae8a0](https://github.com/aurora-silicon/linux/commit/a0ae891ae8a00e183ddbc753347a7ea2ef26d7dc), [79aa2038d24c](https://github.com/aurora-silicon/linux/commit/79aa2038d24c495775338c6d72b638885e2cd5e1), [49e3d35945c3](https://github.com/aurora-silicon/linux/commit/49e3d35945c397f7af5a6477540e2ee254b3bd76), [5e21536704b5](https://github.com/aurora-silicon/linux/commit/5e21536704b56c38b478516d1f65bdcfef748a17) | DjDeveloperr | drm/asahi: pgtable: track ownership below firmware-shared roots [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — MEM; `drivers/gpu/drm/asahi/mmu.rs` | M,D | +| [524c57538f14](https://github.com/aurora-silicon/linux/commit/524c57538f142ef38c8b081f5148ffb515e96266), [52ef95b9e763](https://github.com/aurora-silicon/linux/commit/52ef95b9e7635d564449ec08b33a62c6ed26dd12), [87cab74a2498](https://github.com/aurora-silicon/linux/commit/87cab74a2498f5df62f7697786a2743e84125b99), [c0e36ff2a57f](https://github.com/aurora-silicon/linux/commit/c0e36ff2a57f2a461a96b80cebb741f5b76de7ad) | DjDeveloperr | drm/asahi: add the T8140 firmware interface [#171](https://github.com/aurora-silicon/linux/pull/171) | Active translated data; MCache gated by V25G83.mcache=false — FW; `drivers/gpu/drm/asahi/g17/buffer.rs` | B,C,R | +| [ed303abfff0f](https://github.com/aurora-silicon/linux/commit/ed303abfff0fd9e29e8094b828cd3054969bcf97), [0386eb2184a2](https://github.com/aurora-silicon/linux/commit/0386eb2184a268ed6178a6a93a12880806a8022f), [d7d77474e459](https://github.com/aurora-silicon/linux/commit/d7d77474e4590fcd9c1d0e30ab16dc9ca0c73346), [bc4c50abf8ed](https://github.com/aurora-silicon/linux/commit/bc4c50abf8ede16fec7f2a79546a310134b208df) | DjDeveloperr | drm/asahi: request temporary CPU capacity for sustained GPU feeds [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — POWER; `drivers/gpu/drm/asahi/g17/completion.rs` | I,C,R,P | +| [4a77604eb003](https://github.com/aurora-silicon/linux/commit/4a77604eb003ed141f28781769b28953fdabfb1f), [9fdd32f35641](https://github.com/aurora-silicon/linux/commit/9fdd32f3564105f1d012ef20e1c64de912cade1d), [15d553b170f1](https://github.com/aurora-silicon/linux/commit/15d553b170f157558da0343d477c39161f9870f1), [e1e0c5c90faf](https://github.com/aurora-silicon/linux/commit/e1e0c5c90faf0307f95374c83efe3009d58f0bdb) | DjDeveloperr | drm/asahi: observe compute completions without a queue mask [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — SHARED; `drivers/gpu/drm/asahi/g17/runtime/events.rs` | C,H | +| [593e44ca35f7](https://github.com/aurora-silicon/linux/commit/593e44ca35f708e48e0d9b9efde35bfe1052ef35), [e32d6efe3891](https://github.com/aurora-silicon/linux/commit/e32d6efe3891821dce2e6c0e27219ed676ea4f2f), [7dcd8d1467e2](https://github.com/aurora-silicon/linux/commit/7dcd8d1467e2bc1e90eeb04489d4ab7b392351e8), [0c69741cdc26](https://github.com/aurora-silicon/linux/commit/0c69741cdc26f2dbb26754e87f1b53db1f4bed38) | DjDeveloperr | drm/asahi: avoid repeated submission bookkeeping [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — SHARED; `drivers/gpu/drm/asahi/g17/queue/mod.rs` | C,R,H | +| [0ee6827563de](https://github.com/aurora-silicon/linux/commit/0ee6827563deb2e2519534dd938334c90606fb33), [ce33535500cc](https://github.com/aurora-silicon/linux/commit/ce33535500cc78ed2f6695050ce21487902806bc), [ffa3234a92d7](https://github.com/aurora-silicon/linux/commit/ffa3234a92d789514f3e6b05213f417454b73639), [e0d2761bd854](https://github.com/aurora-silicon/linux/commit/e0d2761bd854136076d642b614c9b6078699243e) | DjDeveloperr | drm/asahi: settle unpublished jobs discarded by entity teardown [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — SHARED; `drivers/gpu/drm/asahi/g17/job.rs` | C,R,H | +| [7eb278622690](https://github.com/aurora-silicon/linux/commit/7eb27862269055d524de513e380572b59171a390), [aa659e9a3710](https://github.com/aurora-silicon/linux/commit/aa659e9a3710765dc87d06e50227bdd8476cbd72), [0a72b08543fd](https://github.com/aurora-silicon/linux/commit/0a72b08543fdec99aa8117c8b4983eeee9b8c856), [d41b760af0bc](https://github.com/aurora-silicon/linux/commit/d41b760af0bcc10744b4145cfdbc19dab7625e8e) | DjDeveloperr | drm/asahi: reserve completion capacity for global compute replay [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — REC; `drivers/gpu/drm/asahi/g17/queue/compute.rs` | F,Q | +| [06c4e4a3193e](https://github.com/aurora-silicon/linux/commit/06c4e4a3193e5b0630e343d9730cad097471440a), [1a53f784e14f](https://github.com/aurora-silicon/linux/commit/1a53f784e14f219a44f5faa7a33947c3f4dedcb1), [945285b800d3](https://github.com/aurora-silicon/linux/commit/945285b800d30fcc9a60461b70249b96b4c053ad), [868c80c92152](https://github.com/aurora-silicon/linux/commit/868c80c921527154df5e73a271583a4b0b63fdeb) | DjDeveloperr | drm/asahi: filter compute completion identities before payload reads [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — SHARED; `drivers/gpu/drm/asahi/g17/initdata.rs` | C,H | +| [304da56f2b1b](https://github.com/aurora-silicon/linux/commit/304da56f2b1b927ac837671897273835eeccb91b), [dc74efcea722](https://github.com/aurora-silicon/linux/commit/dc74efcea72275420878d60c358b3af182006eef), [513f7568ad7e](https://github.com/aurora-silicon/linux/commit/513f7568ad7eb30b97ac1309f37bbb3a11229409), [f61c86ab2830](https://github.com/aurora-silicon/linux/commit/f61c86ab28302e46cba8fbdbaa8339aaa4e17b40) | DjDeveloperr | drm/asahi: sample opening counters at the polling deadline [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — REC; `drivers/gpu/drm/asahi/g17/mod.rs` | F,Q | +| [d012beaabd09](https://github.com/aurora-silicon/linux/commit/d012beaabd0985acd8fa07a5e605f9d0c5b50590), [ff5769e16bea](https://github.com/aurora-silicon/linux/commit/ff5769e16bea501f52ff724effb309f107da39e9), [26ba1aa06add](https://github.com/aurora-silicon/linux/commit/26ba1aa06add76aa7e97a843d752fe0d08afd617), [99dd8deec12f](https://github.com/aurora-silicon/linux/commit/99dd8deec12fe28abb844854059bce6a87764199) | DjDeveloperr | drm/asahi: commit canonical binding metadata with each user mapping [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — MEM; `drivers/gpu/drm/asahi/file.rs` | M,D | +| [fef7f56d2bb3](https://github.com/aurora-silicon/linux/commit/fef7f56d2bb33e5bb627a6dc8872b271a5c504cd), [6753db1b2896](https://github.com/aurora-silicon/linux/commit/6753db1b289605b5cc007a8dd8912d948897f86b), [906cacc20dfc](https://github.com/aurora-silicon/linux/commit/906cacc20dfcccb95c6ba7809f8d3ccac4624791), [11b50b74597c](https://github.com/aurora-silicon/linux/commit/11b50b74597c2c3e1f5687d5db50ce0eaa103c21) | DjDeveloperr | drm/asahi: order canonical object invalidation with mapping removal [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — MEM; `drivers/gpu/drm/asahi/file.rs` | M,D | +| [31b8abbea796](https://github.com/aurora-silicon/linux/commit/31b8abbea79664748c2e8a17745bda9e82b119d0), [477cea7fca6e](https://github.com/aurora-silicon/linux/commit/477cea7fca6eab5f5460c2c5e2459fa9cf6e1fa4), [eb1d380fc62d](https://github.com/aurora-silicon/linux/commit/eb1d380fc62d3f8febbcf3a19d173eca44e49f19), [f594e2be7f49](https://github.com/aurora-silicon/linux/commit/f594e2be7f492d0cb4fdbdf5197122418f330036) | DjDeveloperr | drm/asahi: advertise G17 queue and input-sync capabilities [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — SHARED; `drivers/gpu/drm/asahi/g17/mod.rs` | C,R,H | +| [ee8060be8a84](https://github.com/aurora-silicon/linux/commit/ee8060be8a84b43d483ac9d4f76d51f1f5a5e35c), [80c1cad754d8](https://github.com/aurora-silicon/linux/commit/80c1cad754d8711b46ece6be4d7e00c6fd27b6df), [4810fcc3be49](https://github.com/aurora-silicon/linux/commit/4810fcc3be491f1eaa0c78a183c800d08fecbc1d), [98783a0fc38d](https://github.com/aurora-silicon/linux/commit/98783a0fc38dbac9eecb4164ed6c090e3adf591d) | DjDeveloperr | drm/asahi: acquire event records after observing their producer [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — SHARED; `drivers/gpu/drm/asahi/g17/channel.rs` | C,R,H | +| [eaebe2b1f691](https://github.com/aurora-silicon/linux/commit/eaebe2b1f691e89c991bafee6013b2e0d0b5070d), [63c2157467ec](https://github.com/aurora-silicon/linux/commit/63c2157467ec2530afd39ac823a75b74c5d210c9), [7703d07ccd6d](https://github.com/aurora-silicon/linux/commit/7703d07ccd6dbb8c50cbd993a325e0d6a61cad33), [267dc9ba9ea8](https://github.com/aurora-silicon/linux/commit/267dc9ba9ea8e068a3d9d0683a58b72a51291ece) | DjDeveloperr | drm/asahi: discard partial G17 mappings when binding fails [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — MEM; `drivers/gpu/drm/asahi/mmu.rs` | M,D | +| [7f498ba60f1a](https://github.com/aurora-silicon/linux/commit/7f498ba60f1a44401e434975d8d9433f57212539), [56d0e6d6c8b3](https://github.com/aurora-silicon/linux/commit/56d0e6d6c8b340a20cab5869ab6c516170614aa4), [bdfbf517e1fb](https://github.com/aurora-silicon/linux/commit/bdfbf517e1fb478c202e1531a8a084dc7557d4c5), [9f204371cd61](https://github.com/aurora-silicon/linux/commit/9f204371cd6118ecaa4635b76a763e6c4d53b728) | DjDeveloperr | drm/asahi: reuse the reserved output fence for submitted work [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — SHARED; `drivers/gpu/drm/asahi/g17/fence.rs` | C,R,H | +| [1c200975b7c1](https://github.com/aurora-silicon/linux/commit/1c200975b7c1ccbe890e6bdeb097fb9c5d59bfac), [7c55851883a6](https://github.com/aurora-silicon/linux/commit/7c55851883a670690b2e354802b0e265c0430930), [c97c4be0ce4e](https://github.com/aurora-silicon/linux/commit/c97c4be0ce4e1a265fa915b9ec4e634a7db2c3af), [fa771fbd7629](https://github.com/aurora-silicon/linux/commit/fa771fbd7629800795268b560e9e2ff1b0cb806c) | DjDeveloperr | drm/asahi: wake render growth when its prerequisites become ready [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — SHARED; `drivers/gpu/drm/asahi/g17/runtime/events.rs` | R,H | +| [3b92fcd8c068](https://github.com/aurora-silicon/linux/commit/3b92fcd8c068445739b69393a29c7ee7de0bc5b4), [7d4176528d08](https://github.com/aurora-silicon/linux/commit/7d4176528d08bba02e84e23f43b006a7fdbda51f), [4fe42ca7a853](https://github.com/aurora-silicon/linux/commit/4fe42ca7a85341ec777ca2f19e5ed0eb8fdafa82), [956a8e2fd2dd](https://github.com/aurora-silicon/linux/commit/956a8e2fd2ddcb7e9dc596796b79f62b4e017c55) | DjDeveloperr | drm/asahi: skip frontier scans without a cross-engine prefix [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — SHARED; `drivers/gpu/drm/asahi/g17/queue/mod.rs` | C,R,H | +| [cc1601720b58](https://github.com/aurora-silicon/linux/commit/cc1601720b58ceedb0a76606922d7a554da13e17), [63a3c2d02dac](https://github.com/aurora-silicon/linux/commit/63a3c2d02dac4acb23d299c6a00d096f04bf5520), [75ea155f8b2e](https://github.com/aurora-silicon/linux/commit/75ea155f8b2ed4da38c39179a9a653fec0eb1c37), [5026a187d811](https://github.com/aurora-silicon/linux/commit/5026a187d811c441374d11c71a3fa0df58e118fd) | DjDeveloperr | drm/asahi: track CPU feed requests through policy lifetime [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — POWER; `drivers/gpu/drm/asahi/g17/feed.rs` | I,C,R,P | +| [848b36596112](https://github.com/aurora-silicon/linux/commit/848b36596112673711945126a51efd79d0cfa625), [547707b268e9](https://github.com/aurora-silicon/linux/commit/547707b268e96ecff8cadbc6f8b78652ab52d0ba), [a23fed471fe1](https://github.com/aurora-silicon/linux/commit/a23fed471fe1d97b7d75c738cb63d045ad434754), [c42fd8c45c3a](https://github.com/aurora-silicon/linux/commit/c42fd8c45c3a20ed2ec94c40bf5fbcf782220e7c) | DjDeveloperr | drm/asahi: bound dependency scans to the ordered command prefix [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — SHARED; `drivers/gpu/drm/asahi/g17/queue/mod.rs` | C,R,H | +| [7a205aa6b8eb](https://github.com/aurora-silicon/linux/commit/7a205aa6b8eb17488b9882f49beece4f24b49030), [b1087fb75ab1](https://github.com/aurora-silicon/linux/commit/b1087fb75ab153f2cb017c3a08922ece0da18ca7), [b42e20b3a8d3](https://github.com/aurora-silicon/linux/commit/b42e20b3a8d3e03bdf184be067a0149ed6fbee79), [e4d41aaa9c0c](https://github.com/aurora-silicon/linux/commit/e4d41aaa9c0cdd145d5d07fe0b3d0eb209bf0879) | DjDeveloperr | drm/asahi: wait for host timestamp writes in compute prefixes [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — TIMESTAMP; `drivers/gpu/drm/asahi/g17/queue/mod.rs` | T,C | +| [23e316ffe7e5](https://github.com/aurora-silicon/linux/commit/23e316ffe7e5a3fa108eb271778222bdc7f21ee0), [2a2aff37cd8e](https://github.com/aurora-silicon/linux/commit/2a2aff37cd8e3771dea46ebdc3e45dc7b4415b4c), [55df4049246b](https://github.com/aurora-silicon/linux/commit/55df4049246b6da9482b15411e173a11cc7f04e8), [81d324022d0f](https://github.com/aurora-silicon/linux/commit/81d324022d0f97346639b06a6abaa8b86d480ead) | DjDeveloperr | drm/asahi: release successful submission work-state ownership [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — SHARED; `drivers/gpu/drm/asahi/g17/completion.rs` | C,R,H | +| [18ef3a813070](https://github.com/aurora-silicon/linux/commit/18ef3a813070620821371e437d1557a1dd1c79cf), [cfe7880a5d16](https://github.com/aurora-silicon/linux/commit/cfe7880a5d167059b871475fbc5abe60248fcf3f), [1660e75c7e3c](https://github.com/aurora-silicon/linux/commit/1660e75c7e3ccb4b906662db275d6c5ccaebd452), [e0f7410fd5b3](https://github.com/aurora-silicon/linux/commit/e0f7410fd5b368d06ab17d802720f9d76e12880c) | DjDeveloperr | drm/asahi: preserve exact historical command prefixes [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — SHARED; `drivers/gpu/drm/asahi/g17/command.rs` | C,R,H | +| [5c02e806d300](https://github.com/aurora-silicon/linux/commit/5c02e806d3004d2bb01706362298d8a6802e7433), [6507ba3ca670](https://github.com/aurora-silicon/linux/commit/6507ba3ca670d34720c6c8bfea44836b8228772b), [c7986b27e066](https://github.com/aurora-silicon/linux/commit/c7986b27e066b3d3b4fd81e95b6e174cd6c21807), [fbfb5d96f25e](https://github.com/aurora-silicon/linux/commit/fbfb5d96f25efbf2b1571ca635b195e0ad4a54be) | DjDeveloperr | drm/asahi: bring the cleanroom driver up on Neo hardware [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — SHARED; `drivers/gpu/drm/asahi/g17/buffer.rs` | C,R,H | +| [d0b66e8a8778](https://github.com/aurora-silicon/linux/commit/d0b66e8a877898097d40a380483c09e6be471532), [5fa0b934070c](https://github.com/aurora-silicon/linux/commit/5fa0b934070c615c3c745f5759d2fb7f90eaee09), [01d0b0bed7a7](https://github.com/aurora-silicon/linux/commit/01d0b0bed7a763fa775654a0a115c01f38b34584), [66af386568f7](https://github.com/aurora-silicon/linux/commit/66af386568f748d2499be3e3587ce1e7ca0ae9cd) | Ryan Murray | drm/asahi: report live GEM object accounting [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — MEM; `drivers/gpu/drm/asahi/asahi.rs` | M,D | +| [722064f1be0d](https://github.com/aurora-silicon/linux/commit/722064f1be0d171bc5d496cca352f2db6285c6c0), [f313fc4bd1a1](https://github.com/aurora-silicon/linux/commit/f313fc4bd1a1c91bad6657f7a136cc7b0f88dbe4), [2c076e0de6fe](https://github.com/aurora-silicon/linux/commit/2c076e0de6fe0a0a38a2041b02d5b9e75347375a), [f7fcc50568b9](https://github.com/aurora-silicon/linux/commit/f7fcc50568b9467f241a425b20b92ff1de10f641) | Ryan Murray | drm/asahi: port j700 queue accounting and compute recovery isolation [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — REC; `drivers/gpu/drm/asahi/g17/context.rs` | F,Q | +| [b1e288fb887d](https://github.com/aurora-silicon/linux/commit/b1e288fb887dc74d1b428d478493c6c568d3b02d), [b3b17a139f25](https://github.com/aurora-silicon/linux/commit/b3b17a139f25e8164dfdfe31d7a0c03fc2a4fa85), [2d84d7470c0f](https://github.com/aurora-silicon/linux/commit/2d84d7470c0f0d60c11c9df9e9665ee6a6a2bf71), [3c65418ee698](https://github.com/aurora-silicon/linux/commit/3c65418ee698735d33f299fe208aa8c22ff310a4) | DjDeveloperr | drm/asahi: account once per compute queue transition [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — SHARED; `drivers/gpu/drm/asahi/g17/queue/compute.rs` | C,H | +| [4273fb6241bb](https://github.com/aurora-silicon/linux/commit/4273fb6241bb6f9083dd8feb263fee0924fffa94), [fa2e76747a7f](https://github.com/aurora-silicon/linux/commit/fa2e76747a7f3e264fc9c8dfaaa616627cf4e70f), [2ebb1eb92796](https://github.com/aurora-silicon/linux/commit/2ebb1eb927960a96456df1369294d3d6e392a591), [982df2dcc66e](https://github.com/aurora-silicon/linux/commit/982df2dcc66e2cb2012df476c4325c8fce016dda) | DjDeveloperr | drm: carry shared GPUVM and scheduler interfaces used by the G17 review [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — DRM; `drivers/gpu/drm/drm_gpuvm.c` | C,H,M,F | +| [c163a8df57f9](https://github.com/aurora-silicon/linux/commit/c163a8df57f986f9cb2f3c3275a1dd2b627d7dba), [751ee0b363e7](https://github.com/aurora-silicon/linux/commit/751ee0b363e7d2733cb452989b6c5fcca182f087), [84f4e1fd0eaf](https://github.com/aurora-silicon/linux/commit/84f4e1fd0eaf13c985e6535d2906b68f1c519947), [ca389fba9b6c](https://github.com/aurora-silicon/linux/commit/ca389fba9b6c7c9354c2f765fe81c5ef5f069f3b) | DjDeveloperr | drm/asahi: correct reviewed lifetime, failure and submission paths [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — RTKIT; `drivers/gpu/drm/asahi/g17/channel.rs` | B,F | +| [3a61b700f19d](https://github.com/aurora-silicon/linux/commit/3a61b700f19d1b2a63017c02359a0eb30360c496), [d297d20ba207](https://github.com/aurora-silicon/linux/commit/d297d20ba207abd5b9c4d7b80aa370dd5a379a4f), [ceb8f96ba823](https://github.com/aurora-silicon/linux/commit/ceb8f96ba8232067893d3956bcc4c335bda7e268), [d9b7b82975e2](https://github.com/aurora-silicon/linux/commit/d9b7b82975e2dad4b9a5941ade2f4debadd61c6c) | DjDeveloperr | drm/asahi: order render timestamp publication independently of GPU retirement [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — TIMESTAMP; `drivers/gpu/drm/asahi/g17/completion.rs` | T,C | +| [0f89327272be](https://github.com/aurora-silicon/linux/commit/0f89327272be80ecf44b643c4198614255024367), [212a7b2ccc2d](https://github.com/aurora-silicon/linux/commit/212a7b2ccc2d7a972478f4feed4c19d77cf7e01d), [3bf578fd8958](https://github.com/aurora-silicon/linux/commit/3bf578fd8958a40af5ab8b3ea892749a1dc5b4bf), [e22376a0ca67](https://github.com/aurora-silicon/linux/commit/e22376a0ca679b5359fdef009b2479616619677b) | DjDeveloperr | drm/sched: separate Rust job timeout state from submission state [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — DRM; `drivers/gpu/drm/asahi/g17/job.rs` | C,H,M,F | +| [ca7ec4ac8767](https://github.com/aurora-silicon/linux/commit/ca7ec4ac8767714b3a4c28a8bc4e3121e5a346bf), [3810bcb5e35c](https://github.com/aurora-silicon/linux/commit/3810bcb5e35cb34abd3e80d5998c94ddc747cb34), [20560fe15a23](https://github.com/aurora-silicon/linux/commit/20560fe15a23c2dd8db188a074e245c20bcf9858), [3e464649d372](https://github.com/aurora-silicon/linux/commit/3e464649d37234b0f47dfe3c22d3f73cbb6ef45b) | DjDeveloperr | drm/asahi: observe completed render work before a deferred VM failure [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — SHARED; `drivers/gpu/drm/asahi/g17/runtime/render/submit.rs` | R,H | +| [6a57525c17e8](https://github.com/aurora-silicon/linux/commit/6a57525c17e8cdf0fcb5965e367c8bca29f6a2f4), [7b30ae9dd4eb](https://github.com/aurora-silicon/linux/commit/7b30ae9dd4eb5a5da852deae9709ef4c10ac09c8), [d20cdd0dba5a](https://github.com/aurora-silicon/linux/commit/d20cdd0dba5a51899ea79cb8ac2df9e14cd3cd8a), [83cf9f5ad0ba](https://github.com/aurora-silicon/linux/commit/83cf9f5ad0baf71001cc0aa9c25c83f27f4a219a) | DjDeveloperr | drm/asahi: publish child page tables before linking them [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — MEM; `drivers/gpu/drm/asahi/pgtable.rs` | M,D | +| [c6754b181576](https://github.com/aurora-silicon/linux/commit/c6754b181576b5fb5467f9735e7abe4451271681), [03f169480b45](https://github.com/aurora-silicon/linux/commit/03f169480b452a052c707ddc07b36c2be478d645), [da35a7e8b950](https://github.com/aurora-silicon/linux/commit/da35a7e8b9502f0afb30feac9aa41b03b0b7d080), [b321f0e70539](https://github.com/aurora-silicon/linux/commit/b321f0e7053949d9144028d0a465a4f03eb6cc83) | DjDeveloperr | drm/asahi: retire deferred mapping cleanup without allocating [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — DRM; `drivers/gpu/drm/asahi/mmu.rs` | C,H,M,F | +| [26b6f8056c9a](https://github.com/aurora-silicon/linux/commit/26b6f8056c9a913656c75753e1d719f5771a2496), [85b9304050b9](https://github.com/aurora-silicon/linux/commit/85b9304050b9331ed8206ab69532c1bb42fe9582), [982da05e5f83](https://github.com/aurora-silicon/linux/commit/982da05e5f831a54800a778df91464ab6d6372b2), [7d57556a8962](https://github.com/aurora-silicon/linux/commit/7d57556a8962dd49aadbb24af772ee5240b8c1c1) | DjDeveloperr | drm/asahi: keep compute timestamps outside shared queue aliases [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — TIMESTAMP; `drivers/gpu/drm/asahi/g17/completion.rs` | T,C | +| [a244ed25307c](https://github.com/aurora-silicon/linux/commit/a244ed25307c3d03df0a57def5fffa80d19209fa), [ee1b5a0211c1](https://github.com/aurora-silicon/linux/commit/ee1b5a0211c14b5a799304948bd91fbfebaed35c), [733634bcb0c4](https://github.com/aurora-silicon/linux/commit/733634bcb0c4de6e9885878dc8b1aad744353c83), [e3cfe6674874](https://github.com/aurora-silicon/linux/commit/e3cfe66748744b6aadf6b863b57818adb0721cdd) | DjDeveloperr | drm/shmem-helper: add revocable CPU mappings and idle backing release [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — DRM; `drivers/gpu/drm/asahi/gem.rs` | C,H,M,F | +| [9afbd440e531](https://github.com/aurora-silicon/linux/commit/9afbd440e5316ca0831289cd5dec955003554737), [97bab816b9d2](https://github.com/aurora-silicon/linux/commit/97bab816b9d20dc0b7433baf2077bc2225d9e36a), [449bb6888c6f](https://github.com/aurora-silicon/linux/commit/449bb6888c6feeb79e6390cfe00dd7d4d58b6261), [e2b08ebbcadc](https://github.com/aurora-silicon/linux/commit/e2b08ebbcadc3a9ebe2733140e1a88c62af87ba1) | DjDeveloperr | drm/asahi: reclaim idle private VM backing under memory pressure [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — MEM; `drivers/gpu/drm/asahi/file.rs` | M,D | +| [18e1bce60e1a](https://github.com/aurora-silicon/linux/commit/18e1bce60e1a82f3872012d13fe6f5b51684eb38), [babe2ba0c5a6](https://github.com/aurora-silicon/linux/commit/babe2ba0c5a68f0d3b81085736f52f57f32cbbfe), [717d6fe9e152](https://github.com/aurora-silicon/linux/commit/717d6fe9e152af9bdda4a73a81151145cb2681ca), [e82b27902f91](https://github.com/aurora-silicon/linux/commit/e82b27902f91d8c833395a966366d0ffc30769c3) | DjDeveloperr | drm/asahi: free a closed context's render pool once its release is consumed [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — GMEM; `drivers/gpu/drm/asahi/g17/context.rs` | M,C,R | +| [177e22791d7c](https://github.com/aurora-silicon/linux/commit/177e22791d7cfecb3b6c6b6315adf674ef1c9b30), [431b18cf9072](https://github.com/aurora-silicon/linux/commit/431b18cf9072a8618cbab77cfcbde3952cdb5623), [20a55bacd2fb](https://github.com/aurora-silicon/linux/commit/20a55bacd2fb91e52b6bb5cbb3e88e5218b2f726), [fa210660c43c](https://github.com/aurora-silicon/linux/commit/fa210660c43c1e8ffc4c0705792febb70c29e779) | DjDeveloperr | drm/asahi: hold idle descent for every first render pair install [#171](https://github.com/aurora-silicon/linux/pull/171) | Gated — MMIO; `drivers/gpu/drm/asahi/g17/runtime/render.rs` | B,I | +| [06175c4cc1bc](https://github.com/aurora-silicon/linux/commit/06175c4cc1bccd44765a75a70278fedb27ed9fe5), [2f10cc462301](https://github.com/aurora-silicon/linux/commit/2f10cc462301b9d1a1e64bd31468268d90a5d96d), [e94491e80ac1](https://github.com/aurora-silicon/linux/commit/e94491e80ac12de2fb9b741739b560ab17ecbb34), [d7715ebbd2d3](https://github.com/aurora-silicon/linux/commit/d7715ebbd2d3397958f08442d096a2476d32cb6a) | DjDeveloperr | drm/asahi: add GEM_MADVISE and purge advised objects under memory pressure [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — DRM; `drivers/gpu/drm/asahi/driver.rs` | C,H,M,F | +| [f338bb052975](https://github.com/aurora-silicon/linux/commit/f338bb05297509f7b87714c5727fb99a8fea373a), [c2e8f1ce84f1](https://github.com/aurora-silicon/linux/commit/c2e8f1ce84f1f72a14e6395fa5246a133211432c), [85271d321506](https://github.com/aurora-silicon/linux/commit/85271d321506ec35cc7e6a82665649e7fd2e4f8c), [c8c442ae6a83](https://github.com/aurora-silicon/linux/commit/c8c442ae6a83502a90fca56d1b8e08e669ce03cb) | DjDeveloperr | drm/asahi: drop the sysfs object and queue-slot counters [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — MEM; `drivers/gpu/drm/asahi/g17/context.rs` | M,D | +| [3bb318ea84a1](https://github.com/aurora-silicon/linux/commit/3bb318ea84a1cd2d7661948b347270c8eaf71931), [8b1bcc708f82](https://github.com/aurora-silicon/linux/commit/8b1bcc708f820baa04c2998acf8513a0146d59cf), [ed235501b653](https://github.com/aurora-silicon/linux/commit/ed235501b6531eb218d3b058bab11a807ae17713), [152092e32a7f](https://github.com/aurora-silicon/linux/commit/152092e32a7fd7a7b3a96b8e45be7db78934b7d8) | DjDeveloperr | drm/asahi: drop the stalled scheduler-release warning [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — SHARED; `drivers/gpu/drm/asahi/g17/runtime/teardown.rs` | C,R,H | +| [110dd19ab7e0](https://github.com/aurora-silicon/linux/commit/110dd19ab7e0263538c8301f903471b1700eac12), [815912e331c3](https://github.com/aurora-silicon/linux/commit/815912e331c32ab6468a108ab34c6b26d60faecb), [9d8357e27590](https://github.com/aurora-silicon/linux/commit/9d8357e275907651adbb7022b812f789ece7d040), [24ce1bcdd8eb](https://github.com/aurora-silicon/linux/commit/24ce1bcdd8ebf0f8664c043a639c0201286f94e5) | DjDeveloperr | drm/asahi: build constant firmware objects from word tables [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — SHARED; `drivers/gpu/drm/asahi/g17/fw/initdata.rs` | C,R,H | +| [b5a72b8cb8a5](https://github.com/aurora-silicon/linux/commit/b5a72b8cb8a581d84a4c35c2b4ecc11cd50fabfb), [cea457b72a30](https://github.com/aurora-silicon/linux/commit/cea457b72a308addcffbef0555408e3c2f021c7b), [8e98d0432927](https://github.com/aurora-silicon/linux/commit/8e98d0432927d780061fcc55516769f3d830291f), [7e317cd1218e](https://github.com/aurora-silicon/linux/commit/7e317cd1218e5461124fbfc8db7dd72e71253f8e) | DjDeveloperr | drm/asahi: grow the scheduler-release table instead of failing a close [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — SHARED; `drivers/gpu/drm/asahi/g17/teardown.rs` | C,R,H | +| [8afbe186d27a](https://github.com/aurora-silicon/linux/commit/8afbe186d27aab81bc71008838387799f1d9d4df), [c4f9aff1f720](https://github.com/aurora-silicon/linux/commit/c4f9aff1f720075ed3b059e6d82a5d8be5663768), [be6a3b5d938f](https://github.com/aurora-silicon/linux/commit/be6a3b5d938f2c51ef5b6d60990dcefd64cb677f), [18dbc2de6f21](https://github.com/aurora-silicon/linux/commit/18dbc2de6f21c92c49ffafce39a38c8d6439a5b5) | DjDeveloperr | drm/asahi: add a render flag for passes with at most four known primitives [#171](https://github.com/aurora-silicon/linux/pull/171) | Gated — FEW; `drivers/gpu/drm/asahi/g17/command.rs` | R | +| [7d34f2f65865](https://github.com/aurora-silicon/linux/commit/7d34f2f658658c1fd829700f2e14211a1b150ccd), [b1ec7881bf69](https://github.com/aurora-silicon/linux/commit/b1ec7881bf69ab6b1678761dec5552b28aa414e1), [3318d3e23f8e](https://github.com/aurora-silicon/linux/commit/3318d3e23f8e49a5721f9e2da0eea674eece1d2e), [c00f3a06ed2d](https://github.com/aurora-silicon/linux/commit/c00f3a06ed2d0400a5ec210c856f3a382971f033) | DjDeveloperr | drm/asahi: add a render flag that orders only the fragment stage on barriers [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — SHARED; `drivers/gpu/drm/asahi/g17/command.rs` | R,H | +| [e2b1485ec599](https://github.com/aurora-silicon/linux/commit/e2b1485ec599011654ac1f6ccc4dc78350b50566), [763771c2f3d0](https://github.com/aurora-silicon/linux/commit/763771c2f3d0613ecacaeb20d1cfd21809ead7d5), [0751d1c543c9](https://github.com/aurora-silicon/linux/commit/0751d1c543c933f65ec2ce2606d5effd25206721), [64820e0cb952](https://github.com/aurora-silicon/linux/commit/64820e0cb952592fff5708712a4b4766a8a12f4c) | DjDeveloperr | drm/asahi: give back the USC backing of idle retained compute queues [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — POWER; `drivers/gpu/drm/asahi/g17/freelist.rs` | I,C,R,P | +| [6825b2e8b6f7](https://github.com/aurora-silicon/linux/commit/6825b2e8b6f7d51be5f21a46821541ae8828c6e5), [9e693050e446](https://github.com/aurora-silicon/linux/commit/9e693050e44633bcf0503ec5b864a292d513b563), [ffbf4b465de9](https://github.com/aurora-silicon/linux/commit/ffbf4b465de966c4a5bf3b2e2881fd5678f98bfa), [3db48c0ac600](https://github.com/aurora-silicon/linux/commit/3db48c0ac600b5e919c4d2713dbd84e9b12b4ec4) | DjDeveloperr | drm/asahi: drop an exited owner's context from idle retained compute queues [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — POWER; `drivers/gpu/drm/asahi/g17/context.rs` | I,C,R,P | +| [dbea31c253f3](https://github.com/aurora-silicon/linux/commit/dbea31c253f3d1845a4998abed2db88177e82d54), [e85d3c35771a](https://github.com/aurora-silicon/linux/commit/e85d3c35771a27fc3721f13d572c2538e898837f), [9964d01e50ad](https://github.com/aurora-silicon/linux/commit/9964d01e50add98a41985adc69ace3bc5dc867f1), [5427403e7135](https://github.com/aurora-silicon/linux/commit/5427403e7135dd886ee52a78d73639bd5a2ccb17) | DjDeveloperr | drm/asahi: keep reclaim away from VMs that are in use or were just restored [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — MEM; `drivers/gpu/drm/asahi/mmu/lifetime.rs` | M,D | +| [34de15489113](https://github.com/aurora-silicon/linux/commit/34de15489113f71dd93695681d735afaddef10db), [c5fdbf93b7c8](https://github.com/aurora-silicon/linux/commit/c5fdbf93b7c8280dd1d266df8b4298b6b5c0645c), [5eb07ab2539b](https://github.com/aurora-silicon/linux/commit/5eb07ab2539bf7628366644c96a4742c934b9a78), [1c62cd0853b6](https://github.com/aurora-silicon/linux/commit/1c62cd0853b670104424bcfddeee7f7d9279304e) | DjDeveloperr | drm/asahi: count bindings and VM creation as reclaim activity [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — MEM; `drivers/gpu/drm/asahi/mmu/lifetime.rs` | M,D | +| [9bca711b6542](https://github.com/aurora-silicon/linux/commit/9bca711b65427b7dc0c384e6de2a783cde6e0160), [84e172f23883](https://github.com/aurora-silicon/linux/commit/84e172f23883c27a8eb0cdc85fa81de9c8879eb1), [3202a8b8912f](https://github.com/aurora-silicon/linux/commit/3202a8b8912f39ff7b7c06acc5fd604d5d0c4769), [dec3bc9eb622](https://github.com/aurora-silicon/linux/commit/dec3bc9eb622df05eca30716834c35506ee31f9f) | DjDeveloperr | drm/asahi: set the idle-VM reclaim quiet window to 2000 ms [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — MEM; `drivers/gpu/drm/asahi/mmu/residency.rs` | M,D | +| [f1018934472b](https://github.com/aurora-silicon/linux/commit/f1018934472b655ba44f1fcbfa2f3be2fdedb1fd), [d2046dcecf61](https://github.com/aurora-silicon/linux/commit/d2046dcecf61aceec6af3d013da811e147e3c410), [4963fa334b24](https://github.com/aurora-silicon/linux/commit/4963fa334b2439a0fc831ee3a5a8db008712a307), [1c1abce16316](https://github.com/aurora-silicon/linux/commit/1c1abce16316282e15f5e471b8ef0b5a33acc038) | DjDeveloperr | drm/asahi: release the VM status mirror object when the VM handle closes [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — MEM; `drivers/gpu/drm/asahi/g17/status.rs` | M,D | +| [cc0e08ecbe61](https://github.com/aurora-silicon/linux/commit/cc0e08ecbe61eae09d5be1619a0e6373888f7d02), [503bc0e39d66](https://github.com/aurora-silicon/linux/commit/503bc0e39d665ac97ac53e0b78b2851ca61d2242), [8057982690f1](https://github.com/aurora-silicon/linux/commit/8057982690f1f1954a83f62382873a8baf03bb8e), [6af79e6e57ef](https://github.com/aurora-silicon/linux/commit/6af79e6e57ef80d2b93d4c428956ec6cdfb11a3a) | DjDeveloperr | drm/asahi: unmap detached compute owners and free vacated pools off-lock [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — GMEM; `drivers/gpu/drm/asahi/g17/freelist.rs` | M,C,R | +| [90e691e8fe49](https://github.com/aurora-silicon/linux/commit/90e691e8fe49fa13756b6a64d22074c6e4e4a6cb), [b00ba3dcbfeb](https://github.com/aurora-silicon/linux/commit/b00ba3dcbfebf63d761a8018109bef6067a2cf8b), [6f6bef1b9fd7](https://github.com/aurora-silicon/linux/commit/6f6bef1b9fd7c46f0885eea0ece01a811e08c7d1), [0b4bd1537cc7](https://github.com/aurora-silicon/linux/commit/0b4bd1537cc71a1796b976c05cdab24e76549303) | DjDeveloperr | drm/asahi: retire a compute command on its record when its stamp never follows [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — SHARED; `drivers/gpu/drm/asahi/g17/queue/compute.rs` | C,H | +| [f227c7671c0f](https://github.com/aurora-silicon/linux/commit/f227c7671c0fa54ab73b1477aaf6bbc482e29b8b), [2ceb94e6a1a7](https://github.com/aurora-silicon/linux/commit/2ceb94e6a1a7939d520401fcbb0f41ce5ce9f47a), [e978503f0047](https://github.com/aurora-silicon/linux/commit/e978503f00475864733aefc433d758f0254f401f), [f647d69cc504](https://github.com/aurora-silicon/linux/commit/f647d69cc5043bf1d3389c8e250e78935e5afa24) | DjDeveloperr | drm/asahi: log the first GPU work failure reported to each G17 VM [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — OBS; `drivers/gpu/drm/asahi/g17/status.rs` | O,F | +| [0685cf3905b7](https://github.com/aurora-silicon/linux/commit/0685cf3905b73081560c7c1adc28f76efda774fa), [e8e17790b150](https://github.com/aurora-silicon/linux/commit/e8e17790b150da70debb5e9a1c31027883ad90d3), [85ab080b193f](https://github.com/aurora-silicon/linux/commit/85ab080b193fa481276ebcb9393a93b25024384f), [c8ea00a9ed62](https://github.com/aurora-silicon/linux/commit/c8ea00a9ed621e2e5c7c84461091fda3a9b7cb7b) | DjDeveloperr | drm/asahi: name the completion path in the first-failure log line [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — OBS; `drivers/gpu/drm/asahi/g17/completion.rs` | O,F | +| [ef4fa278cd1a](https://github.com/aurora-silicon/linux/commit/ef4fa278cd1abc81d229ec5853175c35537a962c), [bbbbb8aba0a3](https://github.com/aurora-silicon/linux/commit/bbbbb8aba0a3a08da0b31f31dd0fa1655089c2b0), [ec743d4d34f8](https://github.com/aurora-silicon/linux/commit/ec743d4d34f89fa2ea639801062c957f807c54aa), [5d2a444327af](https://github.com/aurora-silicon/linux/commit/5d2a444327afc38543beb78a252e23661e4d4ecc) | DjDeveloperr | drm/asahi: use one monotonic clock helper across the G17 runtime [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — SHARED; `drivers/gpu/drm/asahi/g17/mod.rs` | C,R,H | +| [860e86da2994](https://github.com/aurora-silicon/linux/commit/860e86da2994a4584ba5ce013c5ef5d79cbda111), [bca35f80a993](https://github.com/aurora-silicon/linux/commit/bca35f80a99330132651e21c61f4d0e5da583cd9), [388a083cca9a](https://github.com/aurora-silicon/linux/commit/388a083cca9a1ff7b98f14dc0e598a63494e7d89), [6850c91310a9](https://github.com/aurora-silicon/linux/commit/6850c91310a913b5ff9646261250e62b129f4ce8) | DjDeveloperr | drm/asahi: grow the scheduler-release table silently [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — SHARED; `drivers/gpu/drm/asahi/g17/teardown.rs` | C,R,H | +| [3cd28842a502](https://github.com/aurora-silicon/linux/commit/3cd28842a502864d73cbfeb7018d6f6e2e00dcdc), [033ec9a3b717](https://github.com/aurora-silicon/linux/commit/033ec9a3b71756814007305dfaf437f3e26c1195), [0c8194353943](https://github.com/aurora-silicon/linux/commit/0c8194353943469158f8651c5ec51723e130afa3), [e60f49eede0e](https://github.com/aurora-silicon/linux/commit/e60f49eede0ecf38cb206c4f59ff832b2aacc1d6) | DjDeveloperr | drm/asahi: give back the descriptor ring of vacated compute queues [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — GMEM; `drivers/gpu/drm/asahi/g17/queue/compute.rs` | M,C,R | +| [5a98c966960a](https://github.com/aurora-silicon/linux/commit/5a98c966960a4310f741538fea1fc04724acffdb), [c9c1859181ab](https://github.com/aurora-silicon/linux/commit/c9c1859181ab17b15b76c74dc36ef00ad7cac96e), [75535b36cc0e](https://github.com/aurora-silicon/linux/commit/75535b36cc0ea75d394b20e11b993bf22e1efe20), [5d630414e13f](https://github.com/aurora-silicon/linux/commit/5d630414e13f4e017628430820fff73a86e97de9) | DjDeveloperr | drm/asahi: return killed compute queues to service with fresh backing [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — GMEM; `drivers/gpu/drm/asahi/g17/queue/compute.rs` | M,C,R | +| [6cae1d77a10a](https://github.com/aurora-silicon/linux/commit/6cae1d77a10aee3e2cd7f52c92eb62b957f6e7ff), [b138b1c78a0e](https://github.com/aurora-silicon/linux/commit/b138b1c78a0ebbba6cef6b3976fc39bfd8b1e3d4), [4457298833f1](https://github.com/aurora-silicon/linux/commit/4457298833f1001a90c15d9277dee87ad6e11927), [16822976f9d3](https://github.com/aurora-silicon/linux/commit/16822976f9d375ddbdc788d968077a15660be5d7) | DjDeveloperr | drm/asahi: give back the render pool backing of evicted idle clients [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — MEM; `drivers/gpu/drm/asahi/g17/context.rs` | M,D | +| [8d77b4f20652](https://github.com/aurora-silicon/linux/commit/8d77b4f20652f18c7fb4e6ae9184eb88ab7e779e), [39c1fc51692c](https://github.com/aurora-silicon/linux/commit/39c1fc51692c3bfcb37c274ce84dfe812bceaaae), [7e8fe19766fd](https://github.com/aurora-silicon/linux/commit/7e8fe19766fd1b895120a8e7bd5fe78f7aca2a7c), [bff3853f260b](https://github.com/aurora-silicon/linux/commit/bff3853f260b02bdd9605bc6690905cb70aca29f) | DjDeveloperr | drm/asahi: carry a scheduling profile from DRM queue priority to firmware [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — PRIORITY; `drivers/gpu/drm/asahi/g17/context.rs` | Q | +| [a15128aa41ec](https://github.com/aurora-silicon/linux/commit/a15128aa41ecb760c5669ec03d8ffee55213bd6c), [87744d8c30b2](https://github.com/aurora-silicon/linux/commit/87744d8c30b28d86a4cc2c45b2e5009b730ed6b4), [fe676c6f4915](https://github.com/aurora-silicon/linux/commit/fe676c6f4915ec15bb3ea0880101eb5b29cfc255), [009b0fb93ba3](https://github.com/aurora-silicon/linux/commit/009b0fb93ba35380563055aa8bc725f36c505262) | DjDeveloperr | drm/asahi: drop two defensive clippy allowances in the VM binding paths [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — MEM; `drivers/gpu/drm/asahi/mmu.rs` | M,D | +| [1567322c2517](https://github.com/aurora-silicon/linux/commit/1567322c251710c365592380a195ceb22979d781), [408df60b8f35](https://github.com/aurora-silicon/linux/commit/408df60b8f35a1f510750df51f5e46bfb51d51a4), [3d1be9ce9669](https://github.com/aurora-silicon/linux/commit/3d1be9ce966988190d72dcad1d0fa4dd1b9204c9), [410d8f80e9ab](https://github.com/aurora-silicon/linux/commit/410d8f80e9ab6b405d7f918901fcbe883bb2d9b2) | DjDeveloperr | drm/asahi: build the G17 boot objects region by region [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — SHARED; `drivers/gpu/drm/asahi/g17/initdata.rs` | C,R,H | +| [0e3bd3afd897](https://github.com/aurora-silicon/linux/commit/0e3bd3afd897637d7c42d54c8ca9adc25e5a691c), [2fa38fc95677](https://github.com/aurora-silicon/linux/commit/2fa38fc956777c4e357d44db60d1b7aafaac273f), [456e6601e69d](https://github.com/aurora-silicon/linux/commit/456e6601e69d15067bcccde93498fd58e22977a1), [f5bd61786e3b](https://github.com/aurora-silicon/linux/commit/f5bd61786e3b537fd811f0ea27d2ec33874232b0) | DjDeveloperr | drm/asahi: split the G17 render pair install paths out of build_render_pair [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — SHARED; `drivers/gpu/drm/asahi/g17/runtime/render/prepare.rs` | R,H | +| [fe926de1f191](https://github.com/aurora-silicon/linux/commit/fe926de1f191b798b49cbe57e36dfba0e0cc4ac7), [e89e97a5241c](https://github.com/aurora-silicon/linux/commit/e89e97a5241cd22b95cb60304f1e2426698c45af), [ef1251b62428](https://github.com/aurora-silicon/linux/commit/ef1251b62428a647c7ed4311cc6223ca1c8fc80a), [e0fa28b70ca9](https://github.com/aurora-silicon/linux/commit/e0fa28b70ca92d7942809827a7cff7e230d68826) | DjDeveloperr | drm/asahi: split the G17 render publication into preflight, barriers and announce [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — SHARED; `drivers/gpu/drm/asahi/g17/queue/render.rs` | R,H | +| [311b9ea8bf83](https://github.com/aurora-silicon/linux/commit/311b9ea8bf835bc7a53866f34b8520ffdc17ef01), [1235a9ee13fe](https://github.com/aurora-silicon/linux/commit/1235a9ee13fe9ed0bf846970e5f5cf9a3049e290), [f76a38f8511f](https://github.com/aurora-silicon/linux/commit/f76a38f8511fd9df3d53e30b899a780c17353d28), [74bc2f09ab74](https://github.com/aurora-silicon/linux/commit/74bc2f09ab74832be661557cacf374ee5fff11d7) | DjDeveloperr | drm/asahi: split the G17 compute publication preflight and queue configuration [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — SHARED; `drivers/gpu/drm/asahi/g17/queue/compute.rs` | C,H | +| [f22f4cfb5b97](https://github.com/aurora-silicon/linux/commit/f22f4cfb5b97b17cf49e7ca9feb877313156f246), [74e91a7f9ad5](https://github.com/aurora-silicon/linux/commit/74e91a7f9ad5d2f61bd5846713e27838c6f5324a), [49e9337b68a4](https://github.com/aurora-silicon/linux/commit/49e9337b68a41b5f9a3a26d8d72e8b0eecc16844), [5ad7b191f7e6](https://github.com/aurora-silicon/linux/commit/5ad7b191f7e698e3ef140163c6ecf15d3e57ff85) | DjDeveloperr | drm/asahi: size the compute descriptor ring to the kicks that can be in flight [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — GMEM; `drivers/gpu/drm/asahi/g17/queue/compute.rs` | M,C,R | +| [bb5149df6a74](https://github.com/aurora-silicon/linux/commit/bb5149df6a74ead7da49be6953c63f4033229ff3), [9d92af630ad2](https://github.com/aurora-silicon/linux/commit/9d92af630ad2f55fe898d653e29c363c131ee4e3), [6d54c0fd885c](https://github.com/aurora-silicon/linux/commit/6d54c0fd885ceded70867de874f27987fc8f69b4), [5f6490414a68](https://github.com/aurora-silicon/linux/commit/5f6490414a683eef149ba6de38cbc27ba62f9a26) | DjDeveloperr | drm/asahi: pack G17 tiler heap blocks contiguously in their backing [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — GMEM; `drivers/gpu/drm/asahi/g17/buffer.rs` | M,C,R | +| [46c70ecebde4](https://github.com/aurora-silicon/linux/commit/46c70ecebde4be2fbd3e82e8311946a156504bf1), [f024202d33e8](https://github.com/aurora-silicon/linux/commit/f024202d33e8832b01e0cff6006fbb9e2b1178a3), [29ff1f70b488](https://github.com/aurora-silicon/linux/commit/29ff1f70b4880e79a8b980adc4d44d4eacb7c7e1), [d2a2c2fa97b9](https://github.com/aurora-silicon/linux/commit/d2a2c2fa97b994595209843720ad78b99b93f16a) | DjDeveloperr | drm/asahi: allocate only the G17 boot pool blocks that are used [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — GMEM; `drivers/gpu/drm/asahi/g17/initdata.rs` | M,C,R | +| [56b8769a1364](https://github.com/aurora-silicon/linux/commit/56b8769a1364bc6c46ab48463afa203a5aec92bb), [614f11f2eb30](https://github.com/aurora-silicon/linux/commit/614f11f2eb30471e02d42f7eab08e9bf4ec4cd41), [c304acb009f0](https://github.com/aurora-silicon/linux/commit/c304acb009f0264d1485596f651495d66efb0fec), [272955b009fe](https://github.com/aurora-silicon/linux/commit/272955b009fe844d4d06361365f166d75b6b451b) | DjDeveloperr | drm/asahi: give back idle G17 tiler heap growth beyond recent use [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — POWER; `drivers/gpu/drm/asahi/g17/buffer.rs` | I,C,R,P | +| [3434c1aab8a6](https://github.com/aurora-silicon/linux/commit/3434c1aab8a6c5d305198347040b8237c4bdce06), [35fe32db0e80](https://github.com/aurora-silicon/linux/commit/35fe32db0e806643f201fc6871095da0e68cfaf8), [1464c4bb1ade](https://github.com/aurora-silicon/linux/commit/1464c4bb1ade2f3fe0625eb93776b26a7f91db96), [c01af7a9e73c](https://github.com/aurora-silicon/linux/commit/c01af7a9e73c789c6a9f98547585a7048927af30) | DjDeveloperr | drm/asahi: give back warm G17 compute pools after ten idle seconds | Active — POWER; `drivers/gpu/drm/asahi/g17/runtime.rs` | I,C,R,P | +| [43a409830c67](https://github.com/aurora-silicon/linux/commit/43a409830c673f190213d2643d50f7500bbf69ab), [882abd400823](https://github.com/aurora-silicon/linux/commit/882abd400823eedce71e3def3c17a3143cd7318b), [57e6637d9f08](https://github.com/aurora-silicon/linux/commit/57e6637d9f087775ee87dcab22dd6c4bb0d4f685), [2d7de7690cc0](https://github.com/aurora-silicon/linux/commit/2d7de7690cc0f9c8d3a74b08c33999593b720aac) | DjDeveloperr | drm/asahi: keep compaction away from GEM pages held for the GPU | Active — MEM; `drivers/gpu/drm/asahi/gem.rs` | M,D | +| [dd109eec40ac](https://github.com/aurora-silicon/linux/commit/dd109eec40ace8e09c436eed1997e302e2b26842), [8bc791526797](https://github.com/aurora-silicon/linux/commit/8bc791526797872caece1891ed792e69dce48438), [76b1755a0610](https://github.com/aurora-silicon/linux/commit/76b1755a06109a66416af0282ec8310dc8a3292b), [6dd0f73fd3b2](https://github.com/aurora-silicon/linux/commit/6dd0f73fd3b2bade5415b31346aadeb2f38d446e) | DjDeveloperr | drm/asahi: complete page-table stores before GPU TLB invalidation | Active — MEM; `drivers/gpu/drm/asahi/g17/kick.rs` | M,D | +| [bec1201676a6](https://github.com/aurora-silicon/linux/commit/bec1201676a6f3b073e8d93791d8c9c7e6c99d7f), [36b80077e7c4](https://github.com/aurora-silicon/linux/commit/36b80077e7c45c57469f9d8cd6ba3e0aa6007cd6), [91e15ed8e7a6](https://github.com/aurora-silicon/linux/commit/91e15ed8e7a681ca293f94e337f59563cd03879d), [019ad857ddfa](https://github.com/aurora-silicon/linux/commit/019ad857ddfa08ed2473eae34db5b8b8d98b068e) | DjDeveloperr | drm/asahi: remove tiler-shrink diagnostic logging | Active — OBS; `drivers/gpu/drm/asahi/g17/buffer.rs` | O,F | +| [692752fb7bf6](https://github.com/aurora-silicon/linux/commit/692752fb7bf60c9e5f83e80dba56779fa48961c1), [3f6a8c31626f](https://github.com/aurora-silicon/linux/commit/3f6a8c31626fc077dbfcea147cd6e30cb28c5e70), [27178c4e373a](https://github.com/aurora-silicon/linux/commit/27178c4e373a23ba1015647f774a3102265c1b27), [d128b5e48465](https://github.com/aurora-silicon/linux/commit/d128b5e484658066c82504df550f6f13abcbd1cc) | DjDeveloperr | drm/asahi: reuse the render state mapping for auxiliary framebuffers | Active — SHARED; `drivers/gpu/drm/asahi/g17/queue/render/memory.rs` | R,H | +| [a2e5af9f5f03](https://github.com/aurora-silicon/linux/commit/a2e5af9f5f0334dbcdd069cc2944f11313f5d67c), [54101717e1de](https://github.com/aurora-silicon/linux/commit/54101717e1dee1b6fd64f32ae8888ec8429b821e), [c0e47197c71c](https://github.com/aurora-silicon/linux/commit/c0e47197c71c80b57974130a06386e101bbdc7ee) | Ryan Murray | drm/asahi: allocate imported and dumb GEM objects as asahi objects | Active — MEM; `drivers/gpu/drm/asahi/gem.rs` | M,D | +| [c29c639e0934](https://github.com/aurora-silicon/linux/commit/c29c639e09349ce24a2bf768c33476e2e27b2512), [c3a21491589b](https://github.com/aurora-silicon/linux/commit/c3a21491589bb62b2279867805b8e87fd2629e4d), [f2760d2f42ec](https://github.com/aurora-silicon/linux/commit/f2760d2f42ec4ede63abb18918ef4abeb4b3de35) | Dj | drm/asahi: preserve TA prefix dependencies across render pairs | Active — SHARED; `drivers/gpu/drm/asahi/g17/queue/render.rs` | R,H | +| [7598420667b6](https://github.com/aurora-silicon/linux/commit/7598420667b6be8788d768316e4218ff3c3fc1ff), [d2f283ac0589](https://github.com/aurora-silicon/linux/commit/d2f283ac058952ea84da562a7f6e6a911ebf879a), [1af8de012d22](https://github.com/aurora-silicon/linux/commit/1af8de012d2221144395f900729619b5065b14fd) | Dj | drm/asahi: provision G17P ray traversal pools from Gen1 SCS demand | Gated — RT; `drivers/gpu/drm/asahi/g17/command.rs` | F | +| [e864801fe49c](https://github.com/aurora-silicon/linux/commit/e864801fe49c9dc6d1fd6e4d1b6c2a607e672f71), [a075a1fab446](https://github.com/aurora-silicon/linux/commit/a075a1fab446a49270d387274f7793d3e9ee6560), [6ffb2dc7a658](https://github.com/aurora-silicon/linux/commit/6ffb2dc7a658e2e6bcaf48a75461b8d7e84549bf) | DjDeveloperr | drm/asahi: recycle settled quarantined G17 compute queues [#305](https://github.com/aurora-silicon/linux/pull/305) | Active — REC; `drivers/gpu/drm/asahi/g17/queue/compute.rs` | F,Q | +| [ae76590d6ea0](https://github.com/aurora-silicon/linux/commit/ae76590d6ea0313a4741fdce5025f6c336eaf90c), [b9fd334a9c96](https://github.com/aurora-silicon/linux/commit/b9fd334a9c96f58640e0ef6ce2010749b85e7c4f), [8ad79fc25869](https://github.com/aurora-silicon/linux/commit/8ad79fc25869a9a5cd36262cc8a702b4327cf437) | DjDeveloperr | drm/asahi: g17: replay render groups firmware retired without executing [#307](https://github.com/aurora-silicon/linux/pull/307) | Active — REC; `drivers/gpu/drm/asahi/g17/job.rs` | F,Q | +| [41786756b43d](https://github.com/aurora-silicon/linux/commit/41786756b43da732fa85c6ac3e23a33e3baa9335), [0a2cdd456013](https://github.com/aurora-silicon/linux/commit/0a2cdd456013347efd1bc2a73d79e291d2d77fbc), [167852510065](https://github.com/aurora-silicon/linux/commit/1678525100652694b0aaee977ea2012b487d6a51) | DjDeveloperr | drm/asahi: g17: never kill compute commands with ray traversal scratch [#308](https://github.com/aurora-silicon/linux/pull/308) | Gated — RT; `drivers/gpu/drm/asahi/g17/queue/compute/exit.rs` | F | +| [d3f4f5dd1b59](https://github.com/aurora-silicon/linux/commit/d3f4f5dd1b592ba925f469133ebac876a101ddb7), [ec3aa2ff51b8](https://github.com/aurora-silicon/linux/commit/ec3aa2ff51b8ad6f9f39c70bf563866ee81baecb), [48b2631fdaab](https://github.com/aurora-silicon/linux/commit/48b2631fdaab66dc7bd850b3006c0f85a73aba0e) | DjDeveloperr | drm/asahi: g17: ray queries in render stages [#309](https://github.com/aurora-silicon/linux/pull/309) | Gated — RT; `drivers/gpu/drm/asahi/g17/command.rs` | F | +| [079f81d5a637](https://github.com/aurora-silicon/linux/commit/079f81d5a637e459a33e8f75a97b91f2454f1499), [2d1fcbe1a980](https://github.com/aurora-silicon/linux/commit/2d1fcbe1a9808f52f2427e4c1237b2b0eba7b7fb), [03426fb46c9c](https://github.com/aurora-silicon/linux/commit/03426fb46c9ce29e5b58db52cfefd09fac156fea) | Ryan Murray | soc: apple: mailbox, rtkit: Allow holding received messages | Active — MBOX; `drivers/soc/apple/mailbox.c` | B,O | +| [a98bb5de4b2e](https://github.com/aurora-silicon/linux/commit/a98bb5de4b2e6dd5d54150008bd322f97c8dbd0d), [c623f821fdfa](https://github.com/aurora-silicon/linux/commit/c623f821fdfaada5df34d2a79fcc4586b0d7323b), [23b2b6f4a1c9](https://github.com/aurora-silicon/linux/commit/23b2b6f4a1c93dbc90768e271c92bc914e5d676f) | Ryan Murray | soc: apple: pmp: log tunables the boot loader does not provide at debug level [#317](https://github.com/aurora-silicon/linux/pull/317) | Active — PMP; `drivers/soc/apple/pmp.rs` | P,B | +| [689592f3219f](https://github.com/aurora-silicon/linux/commit/689592f3219f4733c038cdf72e5a911084d22521), [bc1d13129d6d](https://github.com/aurora-silicon/linux/commit/bc1d13129d6d88ee93515eb547967d87170e5da3), [f80758084279](https://github.com/aurora-silicon/linux/commit/f80758084279064eb1c60e6bfa56f7649bb86a4d) | Ryan Murray | iommu: apple-dart: Demote informational probe messages to debug [#314](https://github.com/aurora-silicon/linux/pull/314) | Active — DART; `drivers/iommu/apple-dart.c` | D,B | +| [e28469b8a38e](https://github.com/aurora-silicon/linux/commit/e28469b8a38e4102fd5c5b877bf51439fc00fca3), [00baae48e464](https://github.com/aurora-silicon/linux/commit/00baae48e464c44cb754d52552d3bd1f431d52f7) | Ryan Murray | iommu: apple-dart: Invalidate every stream in a domain bitmap | Active — DART; `drivers/iommu/Kconfig` | D,B | +| [aade7be0a12a](https://github.com/aurora-silicon/linux/commit/aade7be0a12a38ae8bb276fac80df27eaea97e32), [b13ede3059b0](https://github.com/aurora-silicon/linux/commit/b13ede3059b0bc51bca6033e405606e525ec2259), [4cf2113dc363](https://github.com/aurora-silicon/linux/commit/4cf2113dc36329544edc54b09dc775c5888c3975) | Ryan Murray | soc: apple: mcc: factor out disabling and flushing planes [#317](https://github.com/aurora-silicon/linux/pull/317) | Active — MCC; `drivers/soc/apple/mcc.c` | M,P | +| [ef010a009b3f](https://github.com/aurora-silicon/linux/commit/ef010a009b3ffb15af9794ed37083d1ad2c4facb), [0cba4dd60674](https://github.com/aurora-silicon/linux/commit/0cba4dd60674aa9ded2c27eaae1ff7bf4f1e243e), [8bf158a74ea7](https://github.com/aurora-silicon/linux/commit/8bf158a74ea7e2a5a7e42082fc882d3508119a09) | Ryan Murray | soc: apple: mcc: disable the planes already enabled when enabling fails [#317](https://github.com/aurora-silicon/linux/pull/317) | Active — MCC; `drivers/soc/apple/mcc.c` | M,P | +| [a099186592d6](https://github.com/aurora-silicon/linux/commit/a099186592d6aa010ab7da6d71c7c88d1801345d), [e46e30ff7457](https://github.com/aurora-silicon/linux/commit/e46e30ff7457d01b6e9cb97e8d7a5b0fd8fe20e6), [4653ef5a873f](https://github.com/aurora-silicon/linux/commit/4653ef5a873f16c68a1e9e9080a41395628ca9d4) | Ryan Murray | pmdomain: apple: pmc: undo a partial agent enable [#317](https://github.com/aurora-silicon/linux/pull/317) | Active — PMC; `drivers/pmdomain/apple/pmc.c` | P,C,R | +| [49bb592b9e9f](https://github.com/aurora-silicon/linux/commit/49bb592b9e9f626d06af3296172be8d3aa99a320), [43f4894e7c18](https://github.com/aurora-silicon/linux/commit/43f4894e7c18a46f5e379dd994f0fbd5def67ed7), [a86e8e5637c0](https://github.com/aurora-silicon/linux/commit/a86e8e5637c07504ca09b7bfa5639492af065d8c) | Ryan Murray | pmdomain: apple: pmc: restore the AP's own DCS vote when a limit is lifted [#317](https://github.com/aurora-silicon/linux/pull/317) | Gated — DCS; `drivers/pmdomain/apple/Kconfig` | P | +| [75984fc70c8f](https://github.com/aurora-silicon/linux/commit/75984fc70c8f252d96910dad4cd8635f4977b74f), [6351fe2e5e29](https://github.com/aurora-silicon/linux/commit/6351fe2e5e29f4447d997214d785f10885ef7538), [dba445e67326](https://github.com/aurora-silicon/linux/commit/dba445e6732685313e036e73dc2753c7ed8cbcb6) | Ryan Murray | soc: apple: clpc: remove the debugfs override of the floor [#317](https://github.com/aurora-silicon/linux/pull/317) | Active — CLPC; `drivers/soc/apple/clpc.c` | P,C,R,I | +| [9e4e936ed101](https://github.com/aurora-silicon/linux/commit/9e4e936ed10105188f37f3f8b2c27042e2e2ec58), [c47472a2343b](https://github.com/aurora-silicon/linux/commit/c47472a2343b6c35235e48db178cf76ae94c1f73), [9dfd51203aa8](https://github.com/aurora-silicon/linux/commit/9dfd51203aa80ce8867d98ba03c79fbb283039c2) | Ryan Murray | soc: apple: clpc: make the demand policy per-SoC data, drop the GPU input [#317](https://github.com/aurora-silicon/linux/pull/317) | Active — CLPC; `drivers/soc/apple/clpc.c` | P,C,R,I | +| [64290b216821](https://github.com/aurora-silicon/linux/commit/64290b21682151a3eb4c93d5f666d07f56aae87c), [d19ce09e3508](https://github.com/aurora-silicon/linux/commit/d19ce09e35089ec81bcc82f55198498901df10ac), [10e182579152](https://github.com/aurora-silicon/linux/commit/10e1825791523747c5f0d4ac44570fe4418ff586) | Ryan Murray | pmdomain: apple: pmgr-pwrstate: log always-on domains left off at info level [#317](https://github.com/aurora-silicon/linux/pull/317) | Active — PMGR; `drivers/pmdomain/apple/pmgr-pwrstate.c` | B,F | +| [57dafd4b412f](https://github.com/aurora-silicon/linux/commit/57dafd4b412f8b49be5362a98503be11138cd388), [f86db4089cee](https://github.com/aurora-silicon/linux/commit/f86db4089cee44d2f54de73f60aa32c8a07ffd3a), [1ef9c2113ecd](https://github.com/aurora-silicon/linux/commit/1ef9c2113ecdf2851908e4bd9793876d92152d27) | Ryan Murray | soc: apple: rtkit: Do not warn when buffers are retained on purpose [#314](https://github.com/aurora-silicon/linux/pull/314) | Active — RTKIT; `drivers/soc/apple/rtkit.c` | B,F | +| [5ebe92d17a32](https://github.com/aurora-silicon/linux/commit/5ebe92d17a32112b13e74553bab5b0e0b7c0e147), [7b29e7caccc4](https://github.com/aurora-silicon/linux/commit/7b29e7caccc4896949b0dff4dd8caf7c5fa3f0c0), [9779b2457e8f](https://github.com/aurora-silicon/linux/commit/9779b2457e8f13b0f257964570c9883f079e65f6) | Ryan Murray | pmdomain: apple: pmc: keep the DCS limit state when the rail enable fails [#345](https://github.com/aurora-silicon/linux/pull/345) | Gated — DCS; `drivers/pmdomain/apple/pmc.c` | P | +| [962dacdbb6ad](https://github.com/aurora-silicon/linux/commit/962dacdbb6ad38d9fc723d9f8ea3b3d8dee0e6a9), [4209657d7d94](https://github.com/aurora-silicon/linux/commit/4209657d7d948671c9872e1cbb53f66774793607), [1c797d3eb5cb](https://github.com/aurora-silicon/linux/commit/1c797d3eb5cb79f7e80696b4d7c93800172771c0) | Ryan Murray | cpufreq: apple-soc: say why a cluster with verified transitions fails init [#345](https://github.com/aurora-silicon/linux/pull/345) | Active — CPU; `drivers/cpufreq/apple-soc-cpufreq.c` | C,I,P | +| [854b2e6ec82e](https://github.com/aurora-silicon/linux/commit/854b2e6ec82ea030f3dcfd8ff024393019dd3818), [5a02a0cd2865](https://github.com/aurora-silicon/linux/commit/5a02a0cd28651e4eb3f2eede40af991d8d265a51), [13857139ed46](https://github.com/aurora-silicon/linux/commit/13857139ed4643ef532281808bc8035ad64c9a10) | Ryan Murray | soc: apple: mcc: print the boot DMA device before dropping its node [#345](https://github.com/aurora-silicon/linux/pull/345) | Active — MCC; `drivers/soc/apple/mcc.c` | M,P | +| [e157c990ad98](https://github.com/aurora-silicon/linux/commit/e157c990ad98e3008b552e45d0c85448f95e4266), [ec4fc264985c](https://github.com/aurora-silicon/linux/commit/ec4fc264985c5505be71434f96cc949d0037801f), [0b65493ccd0f](https://github.com/aurora-silicon/linux/commit/0b65493ccd0f4a779e592258032cb40ce03d1a1c) | Ryan Murray | pmdomain: apple: pmgr-pwrstate: tidy the rate-limited error messages [#345](https://github.com/aurora-silicon/linux/pull/345) | Active — PMGR; `drivers/pmdomain/apple/pmgr-pwrstate.c` | B,F | +| [3a9f56606ec3](https://github.com/aurora-silicon/linux/commit/3a9f56606ec3001c7cd7df137cf49a6ae2db0157), [3762bb8f345d](https://github.com/aurora-silicon/linux/commit/3762bb8f345de262f1efca74de864457a3adc9aa), [a516c3f7c3f8](https://github.com/aurora-silicon/linux/commit/a516c3f7c3f85aeb05efe5e3aff5b7890c7d022c) | Ryan Murray | iommu: apple-dart: Invalidate every DART even if a TLB sync step fails [#345](https://github.com/aurora-silicon/linux/pull/345) | Active — DART; `drivers/iommu/apple-dart-test.c` | D,B | +| [8a2875a6789d](https://github.com/aurora-silicon/linux/commit/8a2875a6789d2a4f1452beafd50e4de134b30683), [0f997e1c9354](https://github.com/aurora-silicon/linux/commit/0f997e1c9354b9aa18c7ae13cdf61328b1f50d9d), [df5213734e31](https://github.com/aurora-silicon/linux/commit/df5213734e31c372fc36a2375539e4d69b5f815a) | Ryan Murray | soc: apple: rtkit: Only parse an initial crash log in inherited sessions [#345](https://github.com/aurora-silicon/linux/pull/345) | Active — RTKIT; `drivers/soc/apple/rtkit.c` | B,F | +| [d6caa25b65d2](https://github.com/aurora-silicon/linux/commit/d6caa25b65d2ca7fc7315ee568e0cbd250a9593c), [f501cf5f491d](https://github.com/aurora-silicon/linux/commit/f501cf5f491da7527d71b86cd164e094aac050fd), [82494bf5c6b0](https://github.com/aurora-silicon/linux/commit/82494bf5c6b07739782eeac837b9f6a96c70ca36) | Ryan Murray | iommu/apple-dart: Do not reset a DART that kept its state across sleep [#336](https://github.com/aurora-silicon/linux/pull/336) | Active — DART; `drivers/iommu/apple-dart-test.c` | D,B | +| [91d9f93cf842](https://github.com/aurora-silicon/linux/commit/91d9f93cf842a892913e7ce56218ddc27292a6c1), [dc02d89a1465](https://github.com/aurora-silicon/linux/commit/dc02d89a1465719e6ab3b3a5e31dbea08e1a8f68), [39bc76211b30](https://github.com/aurora-silicon/linux/commit/39bc76211b30ca23b29eb740ffdf0ae8cf2ed5d3) | Ryan Murray | iommu/apple-dart: Keep a retained DART in bypass from being reset [#336](https://github.com/aurora-silicon/linux/pull/336) | Active — DART; `drivers/iommu/apple-dart-test.c` | D,B | +| [b6b28c111cff](https://github.com/aurora-silicon/linux/commit/b6b28c111cff20b5312fee34ca544ce6f7fd00a5), [469fd41db0a3](https://github.com/aurora-silicon/linux/commit/469fd41db0a33df014d0523648c5e1f052895c9a), [d9fc21447419](https://github.com/aurora-silicon/linux/commit/d9fc214474191909db140a5ba565a014819bd6de) | Ryan Murray | iommu/apple-dart: Restore the translations when the reset on resume fails [#336](https://github.com/aurora-silicon/linux/pull/336) | Active — DART; `drivers/iommu/apple-dart-test.c` | D,B | +| [4bfe19d0ad80](https://github.com/aurora-silicon/linux/commit/4bfe19d0ad8042e95b784fe2e439e15c8f869ac5), [cd1ff978d660](https://github.com/aurora-silicon/linux/commit/cd1ff978d6600a354b93fd829d9c9588715c44f1), [43c7fc8f82a6](https://github.com/aurora-silicon/linux/commit/43c7fc8f82a65f2e3807f41f03421df2b27ed3de) | Ryan Murray | irqchip/apple-aic: Allow interrupts to wake the system | Active — AIC; `drivers/irqchip/irq-apple-aic.c` | O,C,I | +| [345f6329c88b](https://github.com/aurora-silicon/linux/commit/345f6329c88b1ea97ecf1c373bc573a9c20947e7), [eee098196369](https://github.com/aurora-silicon/linux/commit/eee098196369f3a2aebe4f99333c53d538caa1c5) | Ryan Murray | drm/asahi: queue background work on system_dfl_wq | Active — SHARED; `drivers/gpu/drm/asahi/g17/mod.rs` | C,R,H | +| [e04141b6136c](https://github.com/aurora-silicon/linux/commit/e04141b6136c63f61679eda781cabe349297d58e), [a741120e3ca0](https://github.com/aurora-silicon/linux/commit/a741120e3ca0d349dffab6cc32086935ecb98d81) | Chris Kearney | soc: apple: validate PMP boot arguments before patching | Active — PMP; `drivers/soc/apple/pmp.rs` | P,B | +| [8a74c2db9581](https://github.com/aurora-silicon/linux/commit/8a74c2db9581fd106dc90be11fb1a1e075bf07fa), [d67dbeedca63](https://github.com/aurora-silicon/linux/commit/d67dbeedca63d1389fb5754d419fffae0c678155) | Chris Kearney | soc: apple: bound PMP arguments by the mapped SRAM size | Active — PMP; `drivers/soc/apple/pmp.rs` | P,B | +| [6c53c0ff9715](https://github.com/aurora-silicon/linux/commit/6c53c0ff9715f055de8af928ed57277f1f0f30a2), [687f1c4f2190](https://github.com/aurora-silicon/linux/commit/687f1c4f21906c698f38c29c467f4aa74425509e) | Eryk Wieliczko | soc: apple: acknowledge serialized PMP device power commands | Active — PMP; `drivers/pmdomain/apple/pmp-report.c` | P,B | +| [5a68774e87a8](https://github.com/aurora-silicon/linux/commit/5a68774e87a8baa711c211757b709966b2193be5), [fefec69b9fca](https://github.com/aurora-silicon/linux/commit/fefec69b9fca69f98a026a4dbdf9bde89b64544b) | Chris Kearney | soc: apple: retry unsent RTKit endpoint starts once | Active — RTKIT; `drivers/soc/apple/rtkit.c` | B,F | +| [dc7d0b49a07d](https://github.com/aurora-silicon/linux/commit/dc7d0b49a07df88aab2ee9cbb3fae17db8694f2b), [5bd0a91f565c](https://github.com/aurora-silicon/linux/commit/5bd0a91f565cf815b9f999b3e704edba2563aac8) | Marius Gehrmann | soc: apple: rtkit: Honour apple,sync-syslog-ack | Active — RTKIT; `drivers/soc/apple/rtkit-internal.h` | B,F | +| [d67d380d1519](https://github.com/aurora-silicon/linux/commit/d67d380d1519babe4b9d080830e31bca56da4d8f), [9d0290148561](https://github.com/aurora-silicon/linux/commit/9d029014856181eb6190324b8300d82590ab2e8b) | Chris Kearney | soc: apple: rtkit: cap the kdebug buffer, and take a late completion | Active — RTKIT; `drivers/soc/apple/rtkit.c` | B,F | +| [1369d18b4ec7](https://github.com/aurora-silicon/linux/commit/1369d18b4ec7bf0a631bc68fbdf7d894f2a4c542), [0d9ac5799dd4](https://github.com/aurora-silicon/linux/commit/0d9ac5799dd4dffe26513b100e93930bb3833a53) | Eryk Wieliczko | soc: apple: acknowledge OSLog progress in negotiated and inherited sessions | Active — RTKIT; `drivers/soc/apple/rtkit-internal.h` | B,F | +| [e47d06ceb56f](https://github.com/aurora-silicon/linux/commit/e47d06ceb56fd73444f3e17954f961dce5edfdcf), [47e137e98667](https://github.com/aurora-silicon/linux/commit/47e137e986675ca44843155e66b731c167932749) | Chris Kearney | iommu/apple-dart: reserve retained firmware tables and root slots | Active — DART; `drivers/iommu/apple-dart.c` | D,B | +| [e663049674c8](https://github.com/aurora-silicon/linux/commit/e663049674c81f6818a88b594f029d0bf9f45ccc), [058d2b7355cf](https://github.com/aurora-silicon/linux/commit/058d2b7355cffe72dad9053486a26ae2822d51a2) | DjDeveloperr | soc: apple: finish the merge of DART, QSPI and SMC hwmon | Active — DART; `drivers/iommu/apple-dart.c` | D,B | +| [d97a27e94fd8](https://github.com/aurora-silicon/linux/commit/d97a27e94fd8967ce1c3846dcc71628a8b39db36), [6dbfbcb21eb0](https://github.com/aurora-silicon/linux/commit/6dbfbcb21eb09d07ae59037a1ceca8cc3500841f) | Eryk Wieliczko | drm/asahi: validate GEM size before page rounding | Active — MEM; `drivers/gpu/drm/asahi/gem.rs` | M,D | +| [6d457ae0a70c](https://github.com/aurora-silicon/linux/commit/6d457ae0a70c583dc760d05ac552829d7370060f), [b038d2767d6e](https://github.com/aurora-silicon/linux/commit/b038d2767d6e08c0dae43c87da89dfd77d719f7c) | Chris Kearney | arm64: dts: apple: t8140: drop the t6000 fallback from the GFX report | Active translated data — FW | B,P | +| [496040e469aa](https://github.com/aurora-silicon/linux/commit/496040e469aa1ea3457c89cbd677d9f964614cca), [17bf156987c5](https://github.com/aurora-silicon/linux/commit/17bf156987c5f7be583bd5efc86430b1055efc36) | Chris Kearney | pmdomain: apple: pmp-report: match T8140 report entries by compatible | Active — PMP; `drivers/pmdomain/apple/pmp-report.c` | P,B | +| [e033fe71b24d](https://github.com/aurora-silicon/linux/commit/e033fe71b24da2b2aabc90b9c5faa5bd0f497d9f), [5f7925d83ca6](https://github.com/aurora-silicon/linux/commit/5f7925d83ca680ba7760dbd16b9e0cb61029161a) | Chris Kearney | drm/asahi: keep the range TLBI base address in its field | Gated — UAT; `drivers/gpu/drm/asahi/mem.rs` | D | +| [6582e3d5ef6f](https://github.com/aurora-silicon/linux/commit/6582e3d5ef6f47ff7e0a11d3fffa47f15ef22a44), [f7132eb38e81](https://github.com/aurora-silicon/linux/commit/f7132eb38e81e5af14711b3da87aa6cf96b3ba98) | Ryan Murray | iommu: apple-dart: Factor out the DT DMA window parse | Active — DART; `drivers/iommu/apple-dart.c` | D,B | +| [fc7981e78871](https://github.com/aurora-silicon/linux/commit/fc7981e78871a2ec91b38835bc84e03611b4e860), [ff11ec1083af](https://github.com/aurora-silicon/linux/commit/ff11ec1083af04c76c1d91fcebc78333681cf519) | Chris Kearney | pmdomain: apple: validate PMP startup tables before seeding | Active — PMP; `drivers/pmdomain/apple/.kunitconfig.apple-pmp` | P,B | +| [4c673adf2c11](https://github.com/aurora-silicon/linux/commit/4c673adf2c110c8264da8b0130eaf181fd6bd560), [782906a65540](https://github.com/aurora-silicon/linux/commit/782906a65540c569f6afa8b49b8c6ac22c9e8dc5) | Ryan Murray | pmdomain: apple: pmp-report: select OF_DYNAMIC | Active — PMP; `drivers/pmdomain/apple/Kconfig` | P,B | +| [f78d7690bfd2](https://github.com/aurora-silicon/linux/commit/f78d7690bfd2d927462c7af34242d6476a0cf59c), [a41d634596e5](https://github.com/aurora-silicon/linux/commit/a41d634596e536031e719b5ea8a3082220e8ec61) | Ryan Murray | drm/asahi: mmu: preallocate enough GPUVAs to unmap across two mappings | Active — MEM; `drivers/gpu/drm/asahi/mmu.rs` | M,D | +| [10fc7af5e0b8](https://github.com/aurora-silicon/linux/commit/10fc7af5e0b897112afaa5fc9ab791f241244969), [755ac9ed1425](https://github.com/aurora-silicon/linux/commit/755ac9ed142576a6e27da54a39b68290e861b34b) | DjDeveloperr | drm/asahi: g17: move the G17P UAPI past the M3 and J613 numbers | Active translated data — FW; `drivers/gpu/drm/asahi/g17/command.rs` | B,C,R | +| [6d5b51d6e818](https://github.com/aurora-silicon/linux/commit/6d5b51d6e818fc0b8915b43a030854d6d96ff330), [62aae2f0f8cb](https://github.com/aurora-silicon/linux/commit/62aae2f0f8cb52a40b2cf4ea21212ad2badd267c) | Ryan Murray | drm/asahi: uapi: use integration/aurora's wording | Active translated data — FW | B,C,R | +| [ff25cb1691ff](https://github.com/aurora-silicon/linux/commit/ff25cb1691ffa6e285dbcb014992e3838f4ad6fe) | Ryan Murray | drm/asahi: Adapt the lifetime and module-ownership changes to Linux 7.3 [#375](https://github.com/aurora-silicon/linux/pull/375) | Active — DRM; `drivers/gpu/drm/asahi/g17/job.rs` | C,H,M,F | +| [2348e8c3acd2](https://github.com/aurora-silicon/linux/commit/2348e8c3acd2920ca583d801645e8387fd80c7f2) | Ryan Murray | drm/asahi: regs: Hold the G17 SGX mapping as a DevresIoMem [#375](https://github.com/aurora-silicon/linux/pull/375) | Active — MEM; `drivers/gpu/drm/asahi/regs.rs` | M,D | +| [d4128b7c28ce](https://github.com/aurora-silicon/linux/commit/d4128b7c28ce177d227ff5488145e8eedd970610) | Ryan Murray | drm/asahi: make SG backing leases revocable [#375](https://github.com/aurora-silicon/linux/pull/375) | Active — DRM; `drivers/gpu/drm/asahi/gem.rs` | C,H,M,F | +| [464cb2e30ae3](https://github.com/aurora-silicon/linux/commit/464cb2e30ae395d13723d161f2aef41066dbb79d) | Ryan Murray | drm/asahi: attribute GPU memory to its creator and limit it [#375](https://github.com/aurora-silicon/linux/pull/375) | Active — MEM; `drivers/gpu/drm/asahi/asahi_core.rs` | M,D | +| [47f408da5996](https://github.com/aurora-silicon/linux/commit/47f408da5996da161de66282da83550bf436450e) | Ryan Murray | drm/asahi: clean up Clippy diagnostics in Aurora additions [#375](https://github.com/aurora-silicon/linux/pull/375) | Active — OBS; `drivers/gpu/drm/asahi/g17/command.rs` | O,F | +| [e44a6b59f5d2](https://github.com/aurora-silicon/linux/commit/e44a6b59f5d248ee1a026b9dbb6dbd244e35a1d7) | Ryan Murray | soc: apple: pmp: use lossless command and bootargs conversions | Active — PMP; `drivers/soc/apple/pmp.rs` | P,B | +| [761626d47af3](https://github.com/aurora-silicon/linux/commit/761626d47af3eb6da3ff8c998373646349aa7169) | Ryan Murray | drm/apple: quiet routine H17P external run-mode syslog records | Active — DISPLAY; `drivers/gpu/drm/apple/Makefile` | B | +| [ac38eb0f57a1](https://github.com/aurora-silicon/linux/commit/ac38eb0f57a17f59eb2ce8158df1fb81aeff2955) | Ryan Murray | drm/asahi: g17: wait for a compute command's stamp before releasing it [#375](https://github.com/aurora-silicon/linux/pull/375) | Active — SHARED; `drivers/gpu/drm/asahi/g17/queue/compute.rs` | C,H | +| [1e3b9caebc7b](https://github.com/aurora-silicon/linux/commit/1e3b9caebc7b1924acc3643222ff95b85e62bdb7) | Ryan Murray | drm/asahi: memacct: Read the module parameters by value on Linux 7.3 [#375](https://github.com/aurora-silicon/linux/pull/375) | Active — MEM; `drivers/gpu/drm/asahi/memacct.rs` | M,D | +| [db2943a27f88](https://github.com/aurora-silicon/linux/commit/db2943a27f88dcd011ae97498cbf2250a4245463) | Jonathan Goyvaerts | soc: apple: rtkit: Log unknown oslog messages at debug level | Active — RTKIT; `drivers/soc/apple/rtkit.c` | B,F | +| [1797c344df7f](https://github.com/aurora-silicon/linux/commit/1797c344df7f7631b2f9be7c9be1a55d8852b164) | Jonathan Goyvaerts | pmdomain: apple: pmp-report: Add T6040 support [#374](https://github.com/aurora-silicon/linux/pull/374) | Active — PMP; `drivers/pmdomain/apple/pmp-report.c` | P,B | +| [927c31856676](https://github.com/aurora-silicon/linux/commit/927c318566760e7b4845d36904022c38271b925d) | Jonathan Goyvaerts | soc: apple: pmp: Add T6040 support [#374](https://github.com/aurora-silicon/linux/pull/374) | Active — PMP; `drivers/soc/apple/pmp.rs` | P,B | +| [7fcc7fd38c43](https://github.com/aurora-silicon/linux/commit/7fcc7fd38c43b6aa64ea2270dcd32cdd57d19f37) | Ryan Murray | drm/asahi: carry G17 admission prerequisites to 7.3 [#377](https://github.com/aurora-silicon/linux/pull/377) | Active — MEM; `drivers/gpu/drm/asahi/g17/freelist.rs` | M,D | +| [c9f72dc51116](https://github.com/aurora-silicon/linux/commit/c9f72dc51116c12b3aa840185eee5a45f4b298b5) | Ryan Murray | drm/asahi: carry G17 pool-release recovery and stamp-wait fixup to 7.3 [#377](https://github.com/aurora-silicon/linux/pull/377) | Active — REC; `drivers/gpu/drm/asahi/g17/queue/compute.rs` | F,Q | +| [10e9da2303fc](https://github.com/aurora-silicon/linux/commit/10e9da2303fc79dc205989075855350020bf115b) | Ryan Murray | drm/asahi: hand abandoned compute growth back to witness settlement [#377](https://github.com/aurora-silicon/linux/pull/377) | Active — SHARED; `drivers/gpu/drm/asahi/g17/freelist.rs` | C,H | +| [e933ad436de0](https://github.com/aurora-silicon/linux/commit/e933ad436de06981d95e9de6ce5626f4ba99e6ff) | Ryan Murray | drm/asahi: borrow the compute pool mutably for header replacement [#377](https://github.com/aurora-silicon/linux/pull/377) | Active — GMEM; `drivers/gpu/drm/asahi/g17/freelist.rs` | M,C,R | +| [04358c1ebe86](https://github.com/aurora-silicon/linux/commit/04358c1ebe86cfbfdc926cfb41ff3e3549248279) | Ryan Murray | drm/asahi: hold failed growth reservations until aliases have gone [#377](https://github.com/aurora-silicon/linux/pull/377) | Active — SHARED; `drivers/gpu/drm/asahi/g17/runtime.rs` | C,R,H | +| [d765e50ada90](https://github.com/aurora-silicon/linux/commit/d765e50ada90e7f00a4ad608c9ff027d0a07a82a) | Ryan Murray | drm/asahi: drop tentative growth aliases before returning a fresh QID [#377](https://github.com/aurora-silicon/linux/pull/377) | Active — SHARED; `drivers/gpu/drm/asahi/g17/runtime.rs` | C,R,H | +| [780956abdb5f](https://github.com/aurora-silicon/linux/commit/780956abdb5fe59c76ca7343bc7f19770df658dd) | Ryan Murray | drm/asahi: compose shared capacity admission with accepted recovery port [#377](https://github.com/aurora-silicon/linux/pull/377) | Active — MEM; `drivers/gpu/drm/asahi/g17/buffer.rs` | M,D | +| [b52bd36805e0](https://github.com/aurora-silicon/linux/commit/b52bd36805e0c35bac5c8255aae8cfad7cce5dd7) | Ryan Murray | drm/asahi: give native ray-query jobs a bounded execution allowance [#377](https://github.com/aurora-silicon/linux/pull/377) | Gated — RT; `drivers/gpu/drm/asahi/g17/job.rs` | F | +| [a5bc25b2ce19](https://github.com/aurora-silicon/linux/commit/a5bc25b2ce1997452676894fc8c9b401c7396e6c) | Ryan Murray | drm/asahi: cover render helpers and AS builds with bounded execution [#377](https://github.com/aurora-silicon/linux/pull/377) | Gated — RT; `drivers/gpu/drm/asahi/g17/command.rs` | F | +| [9827b6bb277a](https://github.com/aurora-silicon/linux/commit/9827b6bb277af5912490229f6fe56d3c575dafe3) | Ryan Murray | drm/asahi: bound repeated timeout recovery and G17 mailbox stalls [#377](https://github.com/aurora-silicon/linux/pull/377) | Active — MBOX; `drivers/gpu/drm/asahi/g17/dependency.rs` | B,O | +| [c314dbb5bd42](https://github.com/aurora-silicon/linux/commit/c314dbb5bd4284697bd065db87eb038021b18125) | Ryan Murray | drm/asahi: expire background pool-release and render-replay polls [#377](https://github.com/aurora-silicon/linux/pull/377) | Active — REC; `drivers/gpu/drm/asahi/g17/queue/compute.rs` | F,Q | +| [6c2d17ff4079](https://github.com/aurora-silicon/linux/commit/6c2d17ff40796899d7190580cabaeea749c73d0a) | Ryan Murray | drm/sched: initialise free_guilty in drm_sched_init() [#381](https://github.com/aurora-silicon/linux/pull/381) | Active — DRM; `drivers/gpu/drm/scheduler/sched_main.c` | C,H,M,F | +| [0d1871c3cfde](https://github.com/aurora-silicon/linux/commit/0d1871c3cfde9cc45b8bc244dde4775dbf4e05c6) | Ryan Murray | rust: drm: sched: zero the scheduler before drm_sched_init() [#381](https://github.com/aurora-silicon/linux/pull/381) | Active — DRM; `rust/kernel/drm/sched.rs` | C,H,M,F | +| [24399672582c](https://github.com/aurora-silicon/linux/commit/24399672582c155df35cadf8cb81a3397958f9e2) | Ryan Murray | drm/asahi: retry G17 retained compute publication pressure [#382](https://github.com/aurora-silicon/linux/pull/382) | Active — SHARED; `drivers/gpu/drm/asahi/g17/queue/compute.rs` | C,H | +| [5ae95851753d](https://github.com/aurora-silicon/linux/commit/5ae95851753d377e13b7e3ac9012452a7fdaccc9) | Jonathan Goyvaerts | pmdomain: apple: pmc: Add the T6040 voter interface [#385](https://github.com/aurora-silicon/linux/pull/385) | Active — PMC; `drivers/pmdomain/apple/pmc-test.c` | P,C,R | +| [7bbd2ad37554](https://github.com/aurora-silicon/linux/commit/7bbd2ad375543683c070aca6487fae75a79fc747) | Jonathan Goyvaerts | soc: apple: clpc: Add the T6040 demand table [#385](https://github.com/aurora-silicon/linux/pull/385) | Active — CLPC; `drivers/soc/apple/clpc.c` | P,C,R,I | +| [b3765bd38ecb](https://github.com/aurora-silicon/linux/commit/b3765bd38ecbfaa5368b760c9d908964abf8c7a5) | Jonathan Goyvaerts | soc: apple: mcc: Allow up to 16 cache planes [#385](https://github.com/aurora-silicon/linux/pull/385) | Active — MCC; `drivers/soc/apple/Kconfig` | M,P | +| [f236c33daca6](https://github.com/aurora-silicon/linux/commit/f236c33daca696a326bd652799bd189ac2b6421f) | Jonathan Goyvaerts | soc: apple: clpc: Request a state only after sustained load [#385](https://github.com/aurora-silicon/linux/pull/385) | Active — CLPC; `drivers/soc/apple/clpc.c` | P,C,R,I | +| [32960bbd0e9c](https://github.com/aurora-silicon/linux/commit/32960bbd0e9cf24931f18d25131eef24f74f3d31) | Jonathan Goyvaerts | iommu/apple-dart: Mirror the firmware's display mappings on T6040 [#386](https://github.com/aurora-silicon/linux/pull/386) | Active — DART; `drivers/iommu/apple-dart.c` | D,B | +| [833c6ed10ffc](https://github.com/aurora-silicon/linux/commit/833c6ed10ffc2ac2d38b7bbb0d85dcc2b8e171df) | DjDeveloperr | drm/asahi: reduce minimum G17 target record backing [#390](https://github.com/aurora-silicon/linux/pull/390) | Active — MEM; `drivers/gpu/drm/asahi/hw/t8140.rs` | M,D | +| [145361fdc90e](https://github.com/aurora-silicon/linux/commit/145361fdc90e82f4bddf6234ec46d0e023f41f86) | DjDeveloperr | drm/asahi: order G17 queue owner changes after scheduler releases [#390](https://github.com/aurora-silicon/linux/pull/390) | Active — SHARED; `drivers/gpu/drm/asahi/g17/queue/compute.rs` | C,R,H | +| [1d478ee6c06b](https://github.com/aurora-silicon/linux/commit/1d478ee6c06b56b48782d08b693d9aa9e5c43435) | DjDeveloperr | drm/asahi: keep G17 render kick ring storage for the life of the device [#390](https://github.com/aurora-silicon/linux/pull/390) | Active — SHARED; `drivers/gpu/drm/asahi/g17/kick.rs` | R,H | +| [7304ddbf6d4d](https://github.com/aurora-silicon/linux/commit/7304ddbf6d4d47b7976efcd20623524c9c53b0b1) | DjDeveloperr | drm/asahi: keep G17 render capacity waits from stopping the runtime [#390](https://github.com/aurora-silicon/linux/pull/390) | Active — SHARED; `drivers/gpu/drm/asahi/g17/freelist.rs` | R,H | +| [72d57c0b9ac9](https://github.com/aurora-silicon/linux/commit/72d57c0b9ac9b8d5fcd58c59eebfa7f9c605399c) | DjDeveloperr | drm/asahi: wait for G17 contexts of closing queues at queue creation [#390](https://github.com/aurora-silicon/linux/pull/390) | Active — REC; `drivers/gpu/drm/asahi/g17/capacity.rs` | F,Q | +| [a4351b50cd8d](https://github.com/aurora-silicon/linux/commit/a4351b50cd8d6f194bb71252acf9c9e2cf636db4) | DjDeveloperr | drm/asahi: keep G17 publication waits out of the submit ioctl [#390](https://github.com/aurora-silicon/linux/pull/390) | Active — SHARED; `drivers/gpu/drm/asahi/g17/completion.rs` | C,R,H | +| [c556dacd085b](https://github.com/aurora-silicon/linux/commit/c556dacd085b148da86b47792dc34d14f02ace86) | DjDeveloperr | drm/asahi: rerun the G17 event pass for completions missed while a pair is borrowed [#390](https://github.com/aurora-silicon/linux/pull/390) | Active — SHARED; `drivers/gpu/drm/asahi/g17/runtime/render.rs` | C,R,H | +| [8744e9417249](https://github.com/aurora-silicon/linux/commit/8744e9417249175511480765dbcc99f0a09c0fb9) | DjDeveloperr | drm/asahi: reuse G17 compute timestamp pages within a context [#390](https://github.com/aurora-silicon/linux/pull/390) | Active — TIMESTAMP; `drivers/gpu/drm/asahi/g17/completion.rs` | T,C | +| [6f7faeecea0e](https://github.com/aurora-silicon/linux/commit/6f7faeecea0eccc8ee317ba163867df339d54fa5) | Jonathan Goyvaerts | drm/asahi: g17: do not advertise soft faults [#391](https://github.com/aurora-silicon/linux/pull/391) | Active — SHARED; `drivers/gpu/drm/asahi/g17/mod.rs` | C,R,H | +| [b0912f542fd0](https://github.com/aurora-silicon/linux/commit/b0912f542fd0655f10631977a8c63abc51a85ada) | Jonathan Goyvaerts | drm/asahi: g17: reread a torn borrowed render snapshot [#391](https://github.com/aurora-silicon/linux/pull/391) | Active — SHARED; `drivers/gpu/drm/asahi/g17/queue/render/retirement.rs` | R,H | +| [30ea4b130473](https://github.com/aurora-silicon/linux/commit/30ea4b13047377972cb75b78a4cbe5885cf46d1d) | Jonathan Goyvaerts | drm/asahi: g17: do not fail a blamed render that attribution cleared [#391](https://github.com/aurora-silicon/linux/pull/391) | Gated — KEY; `drivers/gpu/drm/asahi/g17/recovery.rs` | F | +| [e3d8d1b2fc1f](https://github.com/aurora-silicon/linux/commit/e3d8d1b2fc1feada69ec028203f727f642a150ac) | Jonathan Goyvaerts | drm/asahi: g17: skip already-signalled successful input fences [#391](https://github.com/aurora-silicon/linux/pull/391) | Active — SHARED; `drivers/gpu/drm/asahi/g17/queue/mod.rs` | C,R,H | +| [2321a1bc6ea6](https://github.com/aurora-silicon/linux/commit/2321a1bc6ea6c8a62a275685c0542a293c9b5438) | Jonathan Goyvaerts | drm/asahi: allow HIGH queue priority with CAP_SYS_NICE or DRM master [#391](https://github.com/aurora-silicon/linux/pull/391) | Active — DRM; `drivers/gpu/drm/asahi/g17/mod.rs` | B,M | +| [77a2bbe1a951](https://github.com/aurora-silicon/linux/commit/77a2bbe1a9512960e8c9ba17b466b2c9ab51fb5f) | Ryan Murray | soc: apple: rtkit: Keep warning about unknown oslog messages | Active — RTKIT; `drivers/soc/apple/rtkit.c` | B,F | +| [b2adcfc94046](https://github.com/aurora-silicon/linux/commit/b2adcfc94046a10a989c6fd61fdb1bf7131ef78a) | Ryan Murray | drm/asahi: fail large G17 backing population without direct reclaim [#397](https://github.com/aurora-silicon/linux/pull/397) | Active — DRM; `drivers/gpu/drm/asahi/file.rs` | C,H,M,F | +| [632e4676500b](https://github.com/aurora-silicon/linux/commit/632e4676500bab6fd88817b1cf767afb1bf5da52) | Ryan Murray | drm/asahi: expose memory admission capacities in GET_PARAMS [#397](https://github.com/aurora-silicon/linux/pull/397) | Active — MEM; `drivers/gpu/drm/asahi/file.rs` | M,D | +| [ffe8011a479c](https://github.com/aurora-silicon/linux/commit/ffe8011a479ccd74dde747225c9baf67e2a02ba6) | Chris Kearney | drm/asahi: g15: reject errored input dependencies before publication [#395](https://github.com/aurora-silicon/linux/pull/395) | Active — SHARED; `drivers/gpu/drm/asahi/g17/mod.rs` | C,R,H | +| [faef7fd12e22](https://github.com/aurora-silicon/linux/commit/faef7fd12e22a4d56873b91981d412fd981cfd21) | Chris Kearney | drm/asahi: expose the inherited-error policy only for native G15 [#395](https://github.com/aurora-silicon/linux/pull/395) | Active — SHARED; `drivers/gpu/drm/asahi/g17/mod.rs` | C,R,H | +| [8736001f2b13](https://github.com/aurora-silicon/linux/commit/8736001f2b130a9e477fdef7d1db45f31591bfec) | Eryk Wieliczko | pmdomain: apple: pmgr-pwrstate: Honour apple,no-auto-pm [#395](https://github.com/aurora-silicon/linux/pull/395) | Active — PMGR; `drivers/pmdomain/apple/pmgr-pwrstate.c` | B,F | +| [2aff1b95c550](https://github.com/aurora-silicon/linux/commit/2aff1b95c55039846beb6451c737154254aa3ae0) | Eryk Wieliczko | soc: apple: rtkit: describe and adopt retained system buffers | Active — RTKIT; `drivers/soc/apple/rtkit-internal.h` | B,F | +| [71500694f45b](https://github.com/aurora-silicon/linux/commit/71500694f45b1fd27b05c9ed030b2cc803fb0f2f) | Eryk Wieliczko | drm/asahi: share tracked VM lifetime and native query support [#400](https://github.com/aurora-silicon/linux/pull/400) | Active — MEM; `drivers/gpu/drm/asahi/g17/completion.rs` | M,D | +| [444c52644826](https://github.com/aurora-silicon/linux/commit/444c52644826e99eecf65e9f643744c9e380bca9) | Ryan Murray | soc: apple: pmp: Keep power commands after an unhandled message on other boards | Active — PMP; `drivers/soc/apple/pmp.rs` | P,B | +| [76c60f146885](https://github.com/aurora-silicon/linux/commit/76c60f1468853b4d28e3fed6e9c207c8a2f28410) | Ryan Murray | iommu/apple-dart: Map retained DARTs non-posted like every other DART [#400](https://github.com/aurora-silicon/linux/pull/400) | Active — DART; `drivers/iommu/apple-dart.c` | D,B | +| [d5df65ffcf66](https://github.com/aurora-silicon/linux/commit/d5df65ffcf66e54d9f23de05635512a2cf0ad41d) | Jonathan Goyvaerts | soc: apple: rtkit: add apple_rtkit_resume_idle() | Active — RTKIT; `drivers/soc/apple/rtkit.c` | B,F | +| [f82267ce1172](https://github.com/aurora-silicon/linux/commit/f82267ce117278fb915879abbbd0f355286cecf3) | Jonathan Goyvaerts | soc: apple: rtkit: resume the AP power state before the IOP's | Active — RTKIT; `drivers/soc/apple/rtkit.c` | B,F | +| [e772e7f87659](https://github.com/aurora-silicon/linux/commit/e772e7f87659243ab7b94c2b5bb77ab485dc1128) | DjDeveloperr | iommu/apple-dart: accept mirrored pages in handed-off firmware slots [#404](https://github.com/aurora-silicon/linux/pull/404) | Active — DART; `drivers/iommu/apple-dart.c` | D,B | +| [ff5c39ae1782](https://github.com/aurora-silicon/linux/commit/ff5c39ae1782cd1d826eddf48e0b08b1b2af61eb) | DjDeveloperr | iommu/apple-dart: keep stream invalidates out of handed-off slots [#404](https://github.com/aurora-silicon/linux/pull/404) | Active — DART; `drivers/iommu/apple-dart.c` | D,B | +| [c2bace5bb285](https://github.com/aurora-silicon/linux/commit/c2bace5bb28512471318d71abd5073b2bc7e2af1) | Ryan Murray | soc: apple: pmp: start endpoint 7 on every T6041 PMP [#402](https://github.com/aurora-silicon/linux/pull/402) | Active — PMP; `drivers/soc/apple/pmp.rs` | P,B | +| [2d8c0fe879e2](https://github.com/aurora-silicon/linux/commit/2d8c0fe879e26583b62655534a3aba86b1dfc80f) | Jonathan Goyvaerts | drm/asahi: mmu: make the secondary root of PublishedRoots optional [#420](https://github.com/aurora-silicon/linux/pull/420) | Active — MEM; `drivers/gpu/drm/asahi/g17/mod.rs` | M,D | +| [40507bad232c](https://github.com/aurora-silicon/linux/commit/40507bad232c7539f3c7c2540bac818a7aa90f26) | Jonathan Goyvaerts | drm/asahi: g17: support a single firmware coprocessor [#420](https://github.com/aurora-silicon/linux/pull/420) | Active — SHARED; `drivers/gpu/drm/asahi/g17/event.rs` | C,R,H | +| [750dd1a8e590](https://github.com/aurora-silicon/linux/commit/750dd1a8e5902e4e7478ce9213278b2131b63266) | Jonathan Goyvaerts | drm/asahi: g17: describe the firmware build in a variant table [#420](https://github.com/aurora-silicon/linux/pull/420) | Active translated data; MCache gated by V25G83.mcache=false — FW; `drivers/gpu/drm/asahi/g17/command.rs` | B,C,R | +| [9038ef8d328c](https://github.com/aurora-silicon/linux/commit/9038ef8d328cef14e038513ccaf55dc03869006b) | Jonathan Goyvaerts | drm/asahi: g17: take the per-SoC tables from Config [#420](https://github.com/aurora-silicon/linux/pull/420) | Active translated data — FW; `drivers/gpu/drm/asahi/g17/initdata.rs` | B,C,R | +| [24d1e5f4d8d3](https://github.com/aurora-silicon/linux/commit/24d1e5f4d8d31dd320048546cd53230a61203a9f) | Jonathan Goyvaerts | drm/asahi: g17: operating points with one voltage per cluster [#420](https://github.com/aurora-silicon/linux/pull/420) | Active — SHARED; `drivers/gpu/drm/asahi/g17/fw/initdata.rs` | C,R,H | +| [1ef423a08b3c](https://github.com/aurora-silicon/linux/commit/1ef423a08b3cbc81856afbe3c5854ac25b0893b6) | Jonathan Goyvaerts | drm/asahi: g17: register windows with several instances [#420](https://github.com/aurora-silicon/linux/pull/420) | Active — SHARED; `drivers/gpu/drm/asahi/g17/fw/initdata.rs` | C,R,H | +| [f23df96e697b](https://github.com/aurora-silicon/linux/commit/f23df96e697bc05a47029efd4fe786f900b08a83) | Jonathan Goyvaerts | drm/asahi: g17: let the variant choose the init acknowledgement [#420](https://github.com/aurora-silicon/linux/pull/420) | Active translated data — FW; `drivers/gpu/drm/asahi/g17/fw/variant.rs` | B,C,R | +| [39e990b44482](https://github.com/aurora-silicon/linux/commit/39e990b44482f46fb6bed6c3b7eb2413e0f0331a) | Jonathan Goyvaerts | drm/asahi: mmu: firmware tables in no-map reserved memory [#420](https://github.com/aurora-silicon/linux/pull/420) | Active — MEM; `drivers/gpu/drm/asahi/g17/mod.rs` | M,D | +| [54eccc50cc77](https://github.com/aurora-silicon/linux/commit/54eccc50cc770bdbe9306be4337c548f399e2eb8) | Jonathan Goyvaerts | drm/asahi: mmu: per-SoC UAT cache, TLB and handoff policy [#420](https://github.com/aurora-silicon/linux/pull/420) | Gated — UAT; `drivers/gpu/drm/asahi/mem.rs` | D | +| [abb36e8aa320](https://github.com/aurora-silicon/linux/commit/abb36e8aa3203f4cb28c1b2498d636617d01111d) | Jonathan Goyvaerts | drm/asahi: g17: 25G83 initialization layout [#420](https://github.com/aurora-silicon/linux/pull/420) | Active translated data — FW; `drivers/gpu/drm/asahi/g17/fw/initdata.rs` | B,C,R | +| [0d9efa515188](https://github.com/aurora-silicon/linux/commit/0d9efa515188d93cd775e240b20bf4ad29a258c8) | Jonathan Goyvaerts | drm/asahi: g17: render lead commands for the 25G83 firmware [#420](https://github.com/aurora-silicon/linux/pull/420) | Active translated data — FW; `drivers/gpu/drm/asahi/g17/buffer.rs` | B,C,R | +| [f8dc19862078](https://github.com/aurora-silicon/linux/commit/f8dc19862078a7ad72cd3e10fe22052a02609e76) | Jonathan Goyvaerts | drm/asahi: g17: 25G83 render and compute commands [#420](https://github.com/aurora-silicon/linux/pull/420) | Active translated data — FW; `drivers/gpu/drm/asahi/g17/buffer/target.rs` | B,C,R | +| [20e51646e036](https://github.com/aurora-silicon/linux/commit/20e51646e036432fa9498a24e5330b8d02068612) | Jonathan Goyvaerts | drm/asahi: g17: add the T6040 (M4 Pro) [#420](https://github.com/aurora-silicon/linux/pull/420) | Active translated data — FW; `drivers/gpu/drm/asahi/g17/fw/variant.rs` | B,C,R | +| [244f347d7dee](https://github.com/aurora-silicon/linux/commit/244f347d7deedb8a67eda1f0175ce001a5e7509c) | Jonathan Goyvaerts | soc: apple: rtkit: drop apple_rtkit_resume_idle() | Active — RTKIT; `drivers/soc/apple/rtkit.c` | B,F | +| [a214e242a034](https://github.com/aurora-silicon/linux/commit/a214e242a03404fc7324ff0efdc595a6b37a53c5) | Jonathan Goyvaerts | drm/asahi: g17: bounded cross-VM grace for render timeouts [#413](https://github.com/aurora-silicon/linux/pull/413) | Active — REC; `drivers/gpu/drm/asahi/g17/job.rs` | F,Q | +| [24349f4a4593](https://github.com/aurora-silicon/linux/commit/24349f4a4593fa4e02ce731f5e73a228aa27aed8) | Jonathan Goyvaerts | drm/asahi: g17: retire abandoned spared renders without failing clients [#413](https://github.com/aurora-silicon/linux/pull/413) | Active — REC; `drivers/gpu/drm/asahi/g17/completion.rs` | F,Q | +| [9af21ada7212](https://github.com/aurora-silicon/linux/commit/9af21ada7212737042705f3081d153332516a182) | Jonathan Goyvaerts | drm/asahi: g17: spare innocent renders once the cross-VM grace runs out [#413](https://github.com/aurora-silicon/linux/pull/413) | Active — SHARED; `drivers/gpu/drm/asahi/g17/runtime/backend.rs` | R,H | +| [29f38d83f6a6](https://github.com/aurora-silicon/linux/commit/29f38d83f6a6208d4ff3c4187042b3b0b476d242) | Jonathan Goyvaerts | drm/asahi: g17: keep a VM usable behind its spared render quarantine [#413](https://github.com/aurora-silicon/linux/pull/413) | Active — REC; `drivers/gpu/drm/asahi/g17/completion.rs` | F,Q | +| [c71f1177e773](https://github.com/aurora-silicon/linux/commit/c71f1177e7730db49e9a7ec9cdf150a0feeb05ab) | Jonathan Goyvaerts | drm/asahi: g17: hold the GPU powered for idle_off_delay_ms after work [#413](https://github.com/aurora-silicon/linux/pull/413) | Active — POWER; `drivers/gpu/drm/asahi/g17/mod.rs` | I,C,R,P | +| [645b4e55cf16](https://github.com/aurora-silicon/linux/commit/645b4e55cf16c1cedffba87258bdaafd5b15dcd8) | Jonathan Goyvaerts | drm/asahi: g17: replay every render group firmware dropped unexecuted [#413](https://github.com/aurora-silicon/linux/pull/413) | Active — REC; `drivers/gpu/drm/asahi/g17/completion.rs` | F,Q | +| [94e719a0cec2](https://github.com/aurora-silicon/linux/commit/94e719a0cec2a65502ce5413d2ee9a105c1cc8d9) | Ryan Murray | drm/asahi: g17: never replay a render group a recovery blamed [#418](https://github.com/aurora-silicon/linux/pull/418) | Active — REC; `drivers/gpu/drm/asahi/g17/completion.rs` | F,Q | +| [1054f6763014](https://github.com/aurora-silicon/linux/commit/1054f6763014fe4b4032a08ae7ccce721be4d385) | Ryan Murray | drm/asahi: g17: fix the render timeout head and replay documentation [#418](https://github.com/aurora-silicon/linux/pull/418) | Active — REC; `drivers/gpu/drm/asahi/g17/queue/render.rs` | F,Q | +| [45ac35fa2796](https://github.com/aurora-silicon/linux/commit/45ac35fa279642f98ea7326144716755fb3a39a2) | Ryan Murray | drm/asahi: g17: move the fw-data crash log mapping to its own file [#420](https://github.com/aurora-silicon/linux/pull/420) | Active — SHARED; `drivers/gpu/drm/asahi/g17/data_segment.rs` | C,R,H | +| [fd21bf6faed6](https://github.com/aurora-silicon/linux/commit/fd21bf6faed6f978223e61444424ee918c199838) | Ryan Murray | drm/asahi: g17: take the core-layout buffers from Config [#420](https://github.com/aurora-silicon/linux/pull/420) | Active translated data — FW; `drivers/gpu/drm/asahi/g17/fw/compute.rs` | B,C,R | +| [c0b9fbfa9fb0](https://github.com/aurora-silicon/linux/commit/c0b9fbfa9fb048d5bc67ffb9e17168b686b1962d) | Ryan Murray | drm/asahi: g17: check the voltage cells of the GPU operating points [#420](https://github.com/aurora-silicon/linux/pull/420) | Active translated data — FW; `drivers/gpu/drm/asahi/g17/initdata.rs` | B,C,R | +| [1c5a2ef5783a](https://github.com/aurora-silicon/linux/commit/1c5a2ef5783a565e6b27da871d30e4e637e1ce69) | Ryan Murray | drm/asahi: g17: accept only the two host words of a pointer block [#420](https://github.com/aurora-silicon/linux/pull/420) | Active translated data — FW; `drivers/gpu/drm/asahi/g17/fw/queue.rs` | B,C,R | +| [df6b70b85cbf](https://github.com/aurora-silicon/linux/commit/df6b70b85cbfada36604f511b9ba8da21b13eb7d) | Ryan Murray | drm/asahi: g17: pass the fragment ID to the barrier unchanged [#420](https://github.com/aurora-silicon/linux/pull/420) | Active — SHARED; `drivers/gpu/drm/asahi/g17/fw/queue.rs` | C,R,H | +| [496a59f89601](https://github.com/aurora-silicon/linux/commit/496a59f8960191dc511b92328b13ccc7e31ad1ff) | Ryan Murray | drm/asahi: g17: do not spare a timeout behind work that already failed [#418](https://github.com/aurora-silicon/linux/pull/418) | Active — REC; `drivers/gpu/drm/asahi/g17/queue/compute.rs` | F,Q | +| [fc1a16b4722c](https://github.com/aurora-silicon/linux/commit/fc1a16b4722ce9a9eb6b77fc0af4163873cacc9e) | Ryan Murray | drm/asahi: g17: name the blocking work in the watchdog lines [#418](https://github.com/aurora-silicon/linux/pull/418) | Active — OBS; `drivers/gpu/drm/asahi/g17/queue/compute.rs` | O,F | +| [894ec76dc892](https://github.com/aurora-silicon/linux/commit/894ec76dc89219caae27f9d6845521337b16e8bf) | Ryan Murray | drm/asahi: g17: treat a torn render sample in the timeout fallback as no proof [#418](https://github.com/aurora-silicon/linux/pull/418) | Active — REC; `drivers/gpu/drm/asahi/g17/queue/render.rs` | F,Q | +| [7d167e382f40](https://github.com/aurora-silicon/linux/commit/7d167e382f40542b25ffceb596d0982ee002b7ca) | Ryan Murray | drm/asahi: g17: fail the dropped successors of an exhausted render replay [#418](https://github.com/aurora-silicon/linux/pull/418) | Active — REC; `drivers/gpu/drm/asahi/g17/queue/render.rs` | F,Q | +| [5ee97891f61f](https://github.com/aurora-silicon/linux/commit/5ee97891f61f552c8c1d677db50725c413fb3ed7) | Ryan Murray | drm/asahi: g17: retire finished renders before attributing a render timeout [#418](https://github.com/aurora-silicon/linux/pull/418) | Active — REC; `drivers/gpu/drm/asahi/g17/runtime/backend.rs` | F,Q | +| [25f4c6c3ab18](https://github.com/aurora-silicon/linux/commit/25f4c6c3ab18f329ba2af41b102c30d53e361ae6) | Ryan Murray | thermal: apple: Describe the PMP policy KUnit test module [#417](https://github.com/aurora-silicon/linux/pull/417) | Active — PMP; `drivers/thermal/apple-pmp-policy-test.c` | P,B | +| [b9a8f3938f3d](https://github.com/aurora-silicon/linux/commit/b9a8f3938f3daf5926b7c86204113f6730aec136) | Ryan Murray | drm/asahi: g17: pass the queue count to Pool::new() in the fault admission test [#420](https://github.com/aurora-silicon/linux/pull/420) | Active — GMEM; `drivers/gpu/drm/asahi/g17/queue/admission.rs` | M,C,R | +| [bec833e0bc58](https://github.com/aurora-silicon/linux/commit/bec833e0bc581f74744265f0613832e8291db0cc) | Jonathan Goyvaerts | drm/asahi: pgtable: check the struct page of a table outside the reserved regions [#427](https://github.com/aurora-silicon/linux/pull/427) | Active — MEM; `drivers/gpu/drm/asahi/mmu.rs` | M,D | +| [a335c77de52a](https://github.com/aurora-silicon/linux/commit/a335c77de52a274029bb4e42f96d6afde5712a2f) | Jonathan Goyvaerts | drm/asahi: identify the T6040 GPU firmware by its loaded text [#428](https://github.com/aurora-silicon/linux/pull/428) | Active translated data — FW; `drivers/gpu/drm/asahi/g17/fw/variant.rs` | B,C,R | +| [d69f14f4e4fb](https://github.com/aurora-silicon/linux/commit/d69f14f4e4fbe9af21b70ee5c1b3ff9125c7a9cf) | DjDeveloperr | soc: apple: clpc: raise the T8140 fabric floor for GPU work [#440](https://github.com/aurora-silicon/linux/pull/440) | Active after port; 0d63308820ab — GPUFLOOR; `drivers/soc/apple/clpc.c` | P,C,R,H,I | +| [d1468a5f9a50](https://github.com/aurora-silicon/linux/commit/d1468a5f9a50a23cf741f56d83e6690441b90482) | Ryan | drm/asahi: gpu: drop the unused module_parameters import [#395](https://github.com/aurora-silicon/linux/pull/395) | Active — MEM; `drivers/gpu/drm/asahi/gpu.rs` | M,D | +| [26f5eb68dbce](https://github.com/aurora-silicon/linux/commit/26f5eb68dbce039de470b789d754d2ec3541c15e) | Ryan Murray | drm/asahi: admit the alternate T6040 loaded 25G83 image [#453](https://github.com/aurora-silicon/linux/pull/453) | Active translated data — FW; `drivers/gpu/drm/asahi/g17/fw/variant.rs` | B,C,R | +| [88aebfe0c656](https://github.com/aurora-silicon/linux/commit/88aebfe0c656676c530faa8f85df8d7b69a7ddb0) | DjDeveloperr | irqchip/apple-aic: Report the effective affinity of AICv2+ IRQs [#447](https://github.com/aurora-silicon/linux/pull/447) | Active — AIC; `drivers/irqchip/irq-apple-aic.c` | O,C,I | +| [eb346c1a25d8](https://github.com/aurora-silicon/linux/commit/eb346c1a25d813a1411a34fafc7d4bdee418e0d4), [42a176b951d9](https://github.com/aurora-silicon/linux/commit/42a176b951d90e746e33d27d6eb06a1773579afe) | Haoxiang Li | cpufreq: apple-soc: Fix OPP table cleanup | Active — CPU; `drivers/cpufreq/apple-soc-cpufreq.c` | C,I,P | +| [ae37f03d4163](https://github.com/aurora-silicon/linux/commit/ae37f03d4163df8143b2144e742916b91bf90654) | Ryan Murray | thermal: apple-smc-cpu: Add the J700 CPU thermal policy [#22](https://github.com/aurora-silicon/linux/pull/22) | Active — CPU; `drivers/cpufreq/apple-soc-cpufreq.c` | C,I,P | +| [583b9fa52fbf](https://github.com/aurora-silicon/linux/commit/583b9fa52fbf6a47f150736ad4a1aaa0d616ac02), [a8872354b695](https://github.com/aurora-silicon/linux/commit/a8872354b695a066ac6411791a691f26fcde55bf) | Oliver Lukschander | pmdomain: apple: pmp-report: initialize the report lock [#56](https://github.com/aurora-silicon/linux/pull/56) | Active — PMP; `drivers/pmdomain/apple/pmp-report.c` | P,B | +| [8ca5289bec3a](https://github.com/aurora-silicon/linux/commit/8ca5289bec3a2b6f3629c9df516b357778273a72) | Joshua Warren | drm/asahi: connect the snapshot pointer to the C sysfs static [#197](https://github.com/aurora-silicon/linux/pull/197) [#380](https://github.com/aurora-silicon/linux/pull/380) [#470](https://github.com/aurora-silicon/linux/pull/470) | Active; carried/refined form — MEM; `drivers/gpu/drm/asahi/gpu.rs` | M,D | +| [628bf1df4312](https://github.com/aurora-silicon/linux/commit/628bf1df43123ead994b028fd7b82a209e67aa30), [983575d7b492](https://github.com/aurora-silicon/linux/commit/983575d7b492f6824401cd21fe02278812211e27) | Ryan Murray | drm/asahi: initialize driver data before claiming destructor ownership [#171](https://github.com/aurora-silicon/linux/pull/171) [#380](https://github.com/aurora-silicon/linux/pull/380) [#470](https://github.com/aurora-silicon/linux/pull/470) | Active; carried/refined form — MEM; `drivers/gpu/drm/asahi/driver.rs` | M,B | +| [b10d60ce3916](https://github.com/aurora-silicon/linux/commit/b10d60ce3916c44dfb19b1f1b197bdca40bbac34) | Ryan Murray | drm/asahi: retain two-phase data ownership with stats registration [#380](https://github.com/aurora-silicon/linux/pull/380) [#470](https://github.com/aurora-silicon/linux/pull/470) | Active; carried/refined form — MEM; `drivers/gpu/drm/asahi/driver.rs` | M,B | +| [9266541b8d35](https://github.com/aurora-silicon/linux/commit/9266541b8d35c1cd7146eec9a9b31b39fb7f29cc) | Ryan Murray | treewide: describe hardware behaviour in comments [#233](https://github.com/aurora-silicon/linux/pull/233) [#380](https://github.com/aurora-silicon/linux/pull/380) [#470](https://github.com/aurora-silicon/linux/pull/470) | Active; carried/refined form — MBOX; `drivers/gpu/drm/asahi/g17/buffer.rs` | B,O | +| [8533f072dc30](https://github.com/aurora-silicon/linux/commit/8533f072dc30e076206631f68f41fb4f8d7512a7) | Janne Grunau | iommu: apple-dart: Support specifying the DMA aperture in the DT | Active; carried/refined form — DART; `drivers/iommu/apple-dart.c` | D,B | +| [a9e24ae824ec](https://github.com/aurora-silicon/linux/commit/a9e24ae824ec205cf90f3545b0125631adf1326a) | Ryan Murray | merge: retain the 7.3 implementations where older ports duplicate them [#470](https://github.com/aurora-silicon/linux/pull/470) | Active; carried/refined form — PMGR; `drivers/bluetooth/hci_bcm4377.c` | B,F | +| [00248157ec1f](https://github.com/aurora-silicon/linux/commit/00248157ec1fc4e6369cd26cb8304bb56df7415d), [b2258cfadd69](https://github.com/aurora-silicon/linux/commit/b2258cfadd697cbe10c1e29b54f512c8f297bbe6) | Joshua Warren | soc: apple: mailbox: Make the send-empty IRQ optional (poll TX) | Active — MBOX; `drivers/soc/apple/mailbox.c` | B,O | +| [00e21c12c05a](https://github.com/aurora-silicon/linux/commit/00e21c12c05af1d540a2dc256a8cea3d17d63edc) | Lyude Paul | rust: drm: gem: Add export() callback | Active — DRM; `drivers/gpu/drm/nova/gem.rs` | C,H,M,F | +| [03329eff23bd](https://github.com/aurora-silicon/linux/commit/03329eff23bd3a72bdcfa09a3b98d0ac15107e6e), [35b057e57222](https://github.com/aurora-silicon/linux/commit/35b057e572224c39f79367df9e7c464f1822d15a), [e68c60f81285](https://github.com/aurora-silicon/linux/commit/e68c60f81285248fb50578f268343b06a6452a67) | DjDeveloperr | rust: drm: gpuvm: add non-blocking private mapping traversal [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — DRM; `rust/kernel/drm/gpuvm.rs` | C,H,M,F | +| [033943b2b6ed](https://github.com/aurora-silicon/linux/commit/033943b2b6edb176aaebffa917e2273c3bf8f625) | Ryan Murray | drm: apple: parser: cap the number of timing modes [#345](https://github.com/aurora-silicon/linux/pull/345) | Active; carried/refined form — DISPLAY; `drivers/gpu/drm/apple/parser.c` | B | +| [08e4dd7596c9](https://github.com/aurora-silicon/linux/commit/08e4dd7596c91cdb5b761a0d272e40d75320306b), [29db1d8ab517](https://github.com/aurora-silicon/linux/commit/29db1d8ab517933c2e0114fb3c6bf9beda5417aa), [437f51c11728](https://github.com/aurora-silicon/linux/commit/437f51c11728f3183b0c17af6264547e44a60c26) | Ryan Murray | soc: apple: synchronize the ASCWrap mailbox provider with j700 [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — MBOX; `drivers/soc/apple/mailbox.c` | B,O | +| [0db024825eb1](https://github.com/aurora-silicon/linux/commit/0db024825eb15d5ffd51ae6bb6b71ea4899ff3c2) | Ryan Murray | rust: drm: update shmem KUnit fixtures for the 7.2 file API | Active — DRM; `rust/kernel/drm/gem/shmem.rs` | C,H,M,F | +| [17f9a8193f55](https://github.com/aurora-silicon/linux/commit/17f9a8193f553f12afb69a14c849e12889842e8b) | Ryan Murray | rust: drm: gem: shmem: leave an imported object's SG table to the attachment | Active — DRM; `rust/kernel/drm/gem/shmem.rs` | C,H,M,F | +| [1f6c1fbb7057](https://github.com/aurora-silicon/linux/commit/1f6c1fbb7057db9206dce7187cfd3ec73233cd67) | Ryan Murray | drm/apple: dcp: Release the fixed display route with the device [#345](https://github.com/aurora-silicon/linux/pull/345) | Active; carried/refined form — DISPLAY; `drivers/gpu/drm/apple/dcp.c` | B | +| [29a2e2acec42](https://github.com/aurora-silicon/linux/commit/29a2e2acec42f8361d72bfd9506e3a0527afaf40) | DjDeveloperr | sched/core: add scoped utilization floors for kernel waiters [#171](https://github.com/aurora-silicon/linux/pull/171) | Active; carried/refined form — DRM | C,H,M,F | +| [29a913a5c222](https://github.com/aurora-silicon/linux/commit/29a913a5c222f2bec2ecae62a4dc4af223e5962d) | Jonathan Goyvaerts | soc: apple: mailbox: support the T6040 ASCWrap v6 [#420](https://github.com/aurora-silicon/linux/pull/420) | Active — MBOX; `drivers/soc/apple/mailbox.c` | B,O | +| [2b59aed7191c](https://github.com/aurora-silicon/linux/commit/2b59aed7191cccf0f6239e50e9a165f49b649529) | Ryan Murray | drm/asahi: avoid fence locks when pruning G17 frontiers [#201](https://github.com/aurora-silicon/linux/pull/201) | Active; carried/refined form — SHARED; `drivers/gpu/drm/asahi/g17/fence.rs` | C,R,H | +| [2dc22a191a5c](https://github.com/aurora-silicon/linux/commit/2dc22a191a5cf3109fb18dcfda0fec94bd4df18b), [a45a44cc6e2b](https://github.com/aurora-silicon/linux/commit/a45a44cc6e2b6bbdb189debda045a2174d07e039) | Ryan Murray | rust: drm: gem: shmem: let drivers allocate the objects the core creates | Active — DRM; `rust/kernel/drm/gem/mod.rs` | C,H,M,F | +| [32418c60d4cd](https://github.com/aurora-silicon/linux/commit/32418c60d4cdd6b37e2e9c7e88c7e299d473cf19) | Ryan Murray | drm/apple: dcp: Do not hold the mailbox of an unbound DCP for sleep [#345](https://github.com/aurora-silicon/linux/pull/345) | Active; carried/refined form — DISPLAY; `drivers/gpu/drm/apple/dcp.c` | B | +| [351fc1af825c](https://github.com/aurora-silicon/linux/commit/351fc1af825c49264de7e171a983d2c0c83f1601) | Ryan Murray | dt-bindings: power: apple,pmc: describe the DCS limit [#219](https://github.com/aurora-silicon/linux/pull/219) | Gated — DCS | P | +| [3b9a0383f589](https://github.com/aurora-silicon/linux/commit/3b9a0383f589ebaaa5bcd5c3242e294d6051f0e0) | Janne Grunau | FIXME: drm/gpuvm: Add DRM_GPUVA_REPEAT flag and logic | Active — DRM; `rust/kernel/drm/gpuvm/sm_ops.rs` | C,H,M,F | +| [3c102dba66df](https://github.com/aurora-silicon/linux/commit/3c102dba66df1119625c11c80dca301fb308563c) | Asahi Lina | rust: drm: mm: Add DRM MM Range Allocator abstraction | Active — DRM; `rust/kernel/drm/mm.rs` | C,H,M,F | +| [3d0c9c889919](https://github.com/aurora-silicon/linux/commit/3d0c9c8899198a8ad6d0d0241283ba04d2286fd7) | Ryan Murray | drm: apple: parser: bound the timing catalog allocation [#345](https://github.com/aurora-silicon/linux/pull/345) | Active; carried/refined form — DISPLAY; `drivers/gpu/drm/apple/parser.c` | B | +| [3d3fe96509d1](https://github.com/aurora-silicon/linux/commit/3d3fe96509d102770f72ce5f7829baa3c0f7d1ce), [df2e3cd3b778](https://github.com/aurora-silicon/linux/commit/df2e3cd3b77882ec523b42184d05596e60094072) | Ryan Murray | rust: soc: apple: mailbox: Fix receiver teardown and expose the device | Active — MBOX; `rust/kernel/soc/apple/mailbox.rs` | B,O | +| [3fd71b22d86c](https://github.com/aurora-silicon/linux/commit/3fd71b22d86c3e2397ed1da8a15123053d46a423), [536484539477](https://github.com/aurora-silicon/linux/commit/53648453947771b7ca24a61a585bffdbe1853242) | Ryan Murray | soc: apple: mailbox: Add T8140 ASC and AOP setup mailboxes | Active — MBOX; `drivers/soc/apple/mailbox.c` | B,O | +| [43fc98be783b](https://github.com/aurora-silicon/linux/commit/43fc98be783b0e9833242c1d8d111b49366b134b), [7c99d8debcc5](https://github.com/aurora-silicon/linux/commit/7c99d8debcc54ebf3f76ede14ced9b845db18215), [b0cc4eeb554a](https://github.com/aurora-silicon/linux/commit/b0cc4eeb554a74c2db220e35ceb14955142a79ed) | DjDeveloperr | drm/syncobj: apply expiring capacity hints during blocking waits [#171](https://github.com/aurora-silicon/linux/pull/171) | Active — DRM; `drivers/gpu/drm/drm_syncobj.c` | C,H,M,F | +| [4a577fcddddb](https://github.com/aurora-silicon/linux/commit/4a577fcddddbefec7d13bfd4ae3503893602d29b) | DjDeveloperr | rust: expose the DRM duplicate-reservation flag used by GPUVM [#171](https://github.com/aurora-silicon/linux/pull/171) | Active; carried/refined form — DRM | B,M | +| [4aa16bb5021e](https://github.com/aurora-silicon/linux/commit/4aa16bb5021e427507e15a9a48848d0484a4f3d6) | Eryk Wieliczko | rust: expose fence and DRM lifecycle helpers for native GPU backends | Active — DRM; `rust/kernel/dma_fence.rs` | C,H,M,F | +| [4f7a3272df96](https://github.com/aurora-silicon/linux/commit/4f7a3272df9627d98bc11a7da0c8189ca4aba196) | DjDeveloperr | drm/shmem-helper: populate revocable CPU mappings one page at a time [#171](https://github.com/aurora-silicon/linux/pull/171) | Active; carried/refined form — DRM; `drivers/gpu/drm/drm_gem_shmem_helper.c` | C,H,M,F | +| [5443fc6aeb7a](https://github.com/aurora-silicon/linux/commit/5443fc6aeb7a0fc574805b8b0b1513bbd02dcab4) | Ryan Murray | soc: apple: mailbox: only include reachable KUnit tests | Active — MBOX; `drivers/soc/apple/mailbox.c` | B,O | +| [5611637f6851](https://github.com/aurora-silicon/linux/commit/5611637f685115048c64231e7ac729f23ab20001), [5eff5323da02](https://github.com/aurora-silicon/linux/commit/5eff5323da0282c8fb9adfc57acdc9217e6d7995) | Ryan Murray | soc: apple: mailbox: drain only a present send-empty IRQ | Active — MBOX; `drivers/soc/apple/mailbox.c` | B,O | +| [56920e3e6752](https://github.com/aurora-silicon/linux/commit/56920e3e675230ced9a2ccab81bb7e9aa9c2ccf0) | Janne Grunau | rust: drm: gpuvm: Add sm_can_merge_flags | Active — DRM; `rust/kernel/drm/gpuvm/mod.rs` | C,H,M,F | +| [6154ed6d0f41](https://github.com/aurora-silicon/linux/commit/6154ed6d0f410dd69cbb3e218981b6dc549ac5df) | Janne Grunau | rust: drm: file: Use Registered device in DriverFile::open trait | Active — DRM; `drivers/gpu/drm/nova/file.rs` | C,H,M,F | +| [63d3b0f41939](https://github.com/aurora-silicon/linux/commit/63d3b0f419399d027c865d4ccffa12e3617a73b8) | Ryan Murray | drm: apple: afk: factor the receive bounds check into a helper [#345](https://github.com/aurora-silicon/linux/pull/345) | Active; carried/refined form — DISPLAY; `drivers/gpu/drm/apple/afk.c` | B | +| [6740c25f03c1](https://github.com/aurora-silicon/linux/commit/6740c25f03c1f552b0bdc0ad5b02926d96dd99ad) | Ryan Murray | drm/asahi: Log G17 firmware recovery causes before acknowledgement [#200](https://github.com/aurora-silicon/linux/pull/200) | Active; carried/refined form — OBS; `drivers/gpu/drm/asahi/g17/queue/compute.rs` | O,F | +| [6803b9328c48](https://github.com/aurora-silicon/linux/commit/6803b9328c483c492e0a26fadf0a7cd03a5fe7cc) | Janne Grunau | rust: drm: gpuvm: GpuVa flags and REPEAT logic | Active — DRM; `rust/kernel/drm/gpuvm/sm_ops.rs` | C,H,M,F | +| [6a127c6ff62c](https://github.com/aurora-silicon/linux/commit/6a127c6ff62cde78267225a7094cda2771504c28) | Janne Grunau | rust: drm: file: Add as_raw() | Active — DRM; `drivers/gpu/drm/nova/file.rs` | C,H,M,F | +| [6db05cf4b231](https://github.com/aurora-silicon/linux/commit/6db05cf4b231f020020f31a479bd45ccff83520f) | DjDeveloperr | drm/shmem-helper: index fault pages by object offset [#171](https://github.com/aurora-silicon/linux/pull/171) | Active; carried/refined form — DRM; `drivers/gpu/drm/drm_gem_shmem_helper.c` | C,H,M,F | +| [70d9b6f64ee7](https://github.com/aurora-silicon/linux/commit/70d9b6f64ee76e9da96b752ee3933e2d0a66af0b) | Ryan Murray | drm: apple: free property dictionaries the connector does not keep [#345](https://github.com/aurora-silicon/linux/pull/345) | Active; carried/refined form — DISPLAY; `drivers/gpu/drm/apple/connector.c` | B | +| [72c42b6ddae6](https://github.com/aurora-silicon/linux/commit/72c42b6ddae68c8c58c5f5d720fda5079ae24302) | Janne Grunau | rust: drm: driver: Add feature flags used by asahi | Active — DRM; `rust/kernel/drm/device.rs` | C,H,M,F | +| [765577e012da](https://github.com/aurora-silicon/linux/commit/765577e012da1ee83e6eb56029bdbb9f1f799dce) | Ryan Murray | drm/apple: dptxep: Remove call counters and tidy tunnel comments [#345](https://github.com/aurora-silicon/linux/pull/345) | Active; carried/refined form — DISPLAY; `drivers/gpu/drm/apple/dptxep.c` | B | +| [77a9ab842a02](https://github.com/aurora-silicon/linux/commit/77a9ab842a02e980873a2298caa4dfe8d56faaff) | Ryan Murray | rust: drm: gpuvm: do not leak the VM when exec_lock() fails | Active — DRM; `rust/kernel/drm/gpuvm/mod.rs` | C,H,M,F | +| [7a155e383d2d](https://github.com/aurora-silicon/linux/commit/7a155e383d2d8e70fd76075ffb7427f4c7a3c326), [c98d2b872a2b](https://github.com/aurora-silicon/linux/commit/c98d2b872a2b02241ce4a34d9fe6841cd32b38c3) | Ryan Murray | soc: apple: mailbox: Keep the send-empty IRQ enable balanced | Active — MBOX; `drivers/soc/apple/mailbox.c` | B,O | +| [7c57598ac57c](https://github.com/aurora-silicon/linux/commit/7c57598ac57cb7804a9f2e89d0975e2005985b96) | Janne Grunau | rust: drm: gem: Support locking gpuva.lock | Active — DRM; `rust/kernel/drm/gem/mod.rs` | C,H,M,F | +| [7dce231ad73d](https://github.com/aurora-silicon/linux/commit/7dce231ad73d98e102b4b9e0b362cbab21750854) | DjDeveloperr | drm/syncobj: skip wait hint lookup for ineligible waits [#171](https://github.com/aurora-silicon/linux/pull/171) | Active; carried/refined form — DRM; `drivers/gpu/drm/drm_syncobj.c` | C,H,M,F | +| [7ed160b9b5f2](https://github.com/aurora-silicon/linux/commit/7ed160b9b5f28df712e98088d4368188b2e28072), [c7df21582810](https://github.com/aurora-silicon/linux/commit/c7df2158281045e6fa9bfa6b7c170910f1089d2e) | Ryan Murray | soc: apple: mailbox: Tidy T8140 comments and resume error logging [#314](https://github.com/aurora-silicon/linux/pull/314) | Active — OBS; `drivers/soc/apple/mailbox.c` | O,F | +| [887533ef3bd8](https://github.com/aurora-silicon/linux/commit/887533ef3bd88de5ec05f93be23d2fefd3570cb5) | Ryan Murray | rust: drm: gem: shmem: wait for backing release instead of failing a lease | Active — DRM; `rust/kernel/drm/gem/shmem.rs` | C,H,M,F | +| [8cdeb29b542a](https://github.com/aurora-silicon/linux/commit/8cdeb29b542af3cd5ffb3b535330ad4fd67b97a7) | Janne Grunau | HACK: rust: drm: gem: shmem: Make VMap.ptr pub | Active — DRM; `rust/kernel/drm/gem/shmem.rs` | C,H,M,F | +| [9142d428cb69](https://github.com/aurora-silicon/linux/commit/9142d428cb6956aeb36ffebabe42e1a5c2396c2c) | Ryan Murray | drm: apple: afk: bound received queue entries without 32-bit wrap [#345](https://github.com/aurora-silicon/linux/pull/345) | Active; carried/refined form — DISPLAY; `drivers/gpu/drm/apple/afk.c` | B | +| [92d02170df1e](https://github.com/aurora-silicon/linux/commit/92d02170df1ecf2273d13ba63c0cedbb891996d3) | Jonathan Goyvaerts | arm64: dts: apple: t6040: Add the GPU [#420](https://github.com/aurora-silicon/linux/pull/420) | Active translated data — FW | B,P | +| [9429b186dc3e](https://github.com/aurora-silicon/linux/commit/9429b186dc3edb6658bcdd02e451a5cb1c5f0844) | Ryan Murray | rust: drm: report client memory in fdinfo | Active — DRM; `rust/kernel/drm/device.rs` | C,H,M,F | +| [9523159a7288](https://github.com/aurora-silicon/linux/commit/9523159a72886c5169e7bfd566de9c0aa545ba85) | Ryan Murray | soc: apple: clpc: drop the syscon select and the GPU from Kconfig [#345](https://github.com/aurora-silicon/linux/pull/345) | Active; carried/refined form — CLPC; `drivers/soc/apple/Kconfig` | P,C,R,I | +| [a3828ea8a63d](https://github.com/aurora-silicon/linux/commit/a3828ea8a63d2488f2080dd1f4f6ce8ebd133623) | Asahi Lina | rust: drm: syncobj: Add DRM Sync Object abstraction | Active — DRM; `rust/kernel/drm/mod.rs` | B,M | +| [a6f8817ebbf6](https://github.com/aurora-silicon/linux/commit/a6f8817ebbf6c420aee66897f717ab4e94a4a717) | Ryan Murray | drm/asahi: Reuse fully retired guilty G17 compute queues [#200](https://github.com/aurora-silicon/linux/pull/200) | Active; carried/refined form — REC; `drivers/gpu/drm/asahi/g17/queue/compute.rs` | F,Q | +| [b147e1f460b5](https://github.com/aurora-silicon/linux/commit/b147e1f460b56e398bc085b2457b1ba163cf4505) | Jonathan Goyvaerts | arm64: dts: apple: t6040-j616s: Add the gpu alias [#420](https://github.com/aurora-silicon/linux/pull/420) | Active translated data — FW | B,P | +| [b1f7d09dcf33](https://github.com/aurora-silicon/linux/commit/b1f7d09dcf33e3bb06e81d846783b698d604be5b) | Ryan Murray | drm/apple: dcp: Initialise the start completion before checking RTKit [#345](https://github.com/aurora-silicon/linux/pull/345) | Active; carried/refined form — DISPLAY; `drivers/gpu/drm/apple/dcp.c` | B | +| [b965be95b07f](https://github.com/aurora-silicon/linux/commit/b965be95b07f46ef0be02f47a3a02017d912e4cc) | Asahi Lina | rust: drm: gpuvm: Add part of the asahi downstream gpuvm abstractions | Active — DRM; `rust/kernel/drm/gpuvm/mod.rs` | B,M | +| [ba69d4dde975](https://github.com/aurora-silicon/linux/commit/ba69d4dde9751976c323055ef683d76febf82a2b) | Ryan Murray | drm: apple: afk: validate the receive write pointer and bound a drain [#345](https://github.com/aurora-silicon/linux/pull/345) | Active; carried/refined form — DISPLAY; `drivers/gpu/drm/apple/afk.c` | B | +| [c0e5730f8058](https://github.com/aurora-silicon/linux/commit/c0e5730f8058f14c8fdeb5cd96213e57822f7bbb) | Ryan Murray | drm/apple: dcp: Tidy RTKit start-up logging [#345](https://github.com/aurora-silicon/linux/pull/345) | Active; carried/refined form — DISPLAY; `drivers/gpu/drm/apple/dcp.c` | B | +| [c4722f38cdcc](https://github.com/aurora-silicon/linux/commit/c4722f38cdccca6dbbbc446dfa2d715ab96e9554) | Ryan Murray | arm64: dts: apple: t6040: Do not claim the GPU's PMP report entry at boot [#402](https://github.com/aurora-silicon/linux/pull/402) | Active — PMP | P,B | +| [cd71c1e7ff90](https://github.com/aurora-silicon/linux/commit/cd71c1e7ff901c3c35492ebe5b8bebb0702e5480) | Jonathan Goyvaerts | arm64: dts: apple: t6040-j616s: describe the GPU firmware text segment [#428](https://github.com/aurora-silicon/linux/pull/428) | Active translated data — FW | B,P | +| [d50642d23a7e](https://github.com/aurora-silicon/linux/commit/d50642d23a7edb9ace6837d6471981e84d0d4ef6) | Ryan Murray | iommu: apple-dart: Fix the KUnit test after deferred root publication [#314](https://github.com/aurora-silicon/linux/pull/314) | Active; carried/refined form — DART; `drivers/iommu/apple-dart-test.c` | D,B | +| [d6799a78db13](https://github.com/aurora-silicon/linux/commit/d6799a78db13ad06a2f26b66aad97c7e7626714d) | Ryan Murray | drm/apple: dcp: Initialise work items at probe [#345](https://github.com/aurora-silicon/linux/pull/345) | Active; carried/refined form — DISPLAY; `drivers/gpu/drm/apple/dcp.c` | B | +| [d8b45282c1e7](https://github.com/aurora-silicon/linux/commit/d8b45282c1e70cb4b947d2aadfb2636fd4ee763a) | Lyude Paul | rust: drm: gem: Add BaseObject::prime_export() | Active — DRM; `rust/kernel/drm/gem/mod.rs` | C,H,M,F | +| [defdcb801774](https://github.com/aurora-silicon/linux/commit/defdcb8017744fe0aa57061ed304e793935b52bc) | Ryan Murray | soc: apple: build the CLPC floors by default [#219](https://github.com/aurora-silicon/linux/pull/219) | Active; carried/refined form — CLPC; `drivers/soc/apple/Kconfig` | P,C,R,I | +| [e1468f83c1da](https://github.com/aurora-silicon/linux/commit/e1468f83c1daf236b69c6868a3a05a9d6f994f9b) | Janne Grunau | rust: drm: gem: Add missing trailing '//' for kernel vertical style | Active — DRM; `rust/kernel/drm/gem/mod.rs` | C,H,M,F | +| [e6d369606edf](https://github.com/aurora-silicon/linux/commit/e6d369606edff3c24dc3b1f5dda036cc77a8887c) | Ryan Murray | drm/shmem: revoke CPU PFNs before releasing purge backing [#171](https://github.com/aurora-silicon/linux/pull/171) | Active; carried/refined form — DRM; `drivers/gpu/drm/drm_gem_shmem_helper.c` | C,H,M,F | +| [f0a8afc04957](https://github.com/aurora-silicon/linux/commit/f0a8afc0495743e502fc860fa2bc0fc96e95b22a) | Sami Tolvanen | rust: drm: add a registration guard for the Normal device context | Active — DRM; `rust/kernel/drm/device.rs` | C,H,M,F | +| [fd2a0321db11](https://github.com/aurora-silicon/linux/commit/fd2a0321db11572ac201663697c83c9330ef65a3) | DjDeveloperr | driver core: track devres node membership for direct removal [#390](https://github.com/aurora-silicon/linux/pull/390) | Active — DRM; `drivers/base/base.h` | B,M | + +### N/A, superseded, other-backend and imported-neighbor records + +These scoped candidates are included to make exclusions reviewable. A shared +path query or an omnibus PR is not evidence of a Neo performance improvement. + +| Commits / encountered aliases | Author | Improvement / record | T6040 after ports — evidence key and changed source | Measurement | +| --- | --- | --- | --- | --- | +| [4f772d2c2d6c](https://github.com/aurora-silicon/linux/commit/4f772d2c2d6cb336d4df08ae6c554b8387510c20) | Janne Grunau | drm/asahi: initdata: Fold GlobalsSub struct into Globals | Not applicable — LEGACY; `drivers/gpu/drm/asahi/fw/initdata.rs` | V | +| [532ac90e08af](https://github.com/aurora-silicon/linux/commit/532ac90e08affc3bbcf59e273e00f1573b7a05d0) | Janne Grunau | drm/asahi: Avoid variable/field ref shadowing in pin-init | Not applicable — LEGACY; `drivers/gpu/drm/asahi/initdata.rs` | V | +| [253d85ab9854](https://github.com/aurora-silicon/linux/commit/253d85ab98543f838ecc6be70edb7a43d966d505) | Sasha Finkelstein | drm/asahi: starlight-debug | Not applicable — LEGACY; `drivers/gpu/drm/asahi/gpu.rs` | V | +| [83526ea43d4f](https://github.com/aurora-silicon/linux/commit/83526ea43d4fffdb10a3dff36a37a587ffaa6c14) | Janne Grunau | drm/asahi: workqueue: Drop completed work items | Not applicable — LEGACY; `drivers/gpu/drm/asahi/workqueue.rs` | V | +| [b1e32124e9e9](https://github.com/aurora-silicon/linux/commit/b1e32124e9e94ebd3e1345eb54f687fcfbfee509) | Janne Grunau | drm/asahi: rustfmt | Not applicable — VERIFY; `drivers/gpu/drm/asahi/mmu.rs` | V | +| [8508e5105997](https://github.com/aurora-silicon/linux/commit/8508e5105997a43be75e8a3ffe0c3d8ddc00155c), [e092d51bc9d7](https://github.com/aurora-silicon/linux/commit/e092d51bc9d773f7058fe578515c5f915c0adf2c) | Yureka Lilian | pmdomain: add pmp-report offsets for t6030,t8122 | Not applicable — G15; `drivers/pmdomain/apple/pmp-report.c` | V | +| [357978884c89](https://github.com/aurora-silicon/linux/commit/357978884c895e5fc35af84fed8adf0583bc3353), [a89f7da611ad](https://github.com/aurora-silicon/linux/commit/a89f7da611ade4b9d5f40dd45fe5b41d059e3399) | Yureka Lilian | arm64: dts: apple: add minimal MacBook Neo device tree | Not applicable — PERIPH | V | +| [dfe1431f40cf](https://github.com/aurora-silicon/linux/commit/dfe1431f40cf15ec4aebd1864b4c69707b3c43c2), [b796a047ec25](https://github.com/aurora-silicon/linux/commit/b796a047ec253074e2cd6b5f38a8f19949b50271), [9359ea48b718](https://github.com/aurora-silicon/linux/commit/9359ea48b7184cdee39b7c76b5f0d4a923807cf3) | Ryan Murray | arm64: dts: apple: Add T8140/J700 device trees with audio and camera | Not applicable — PERIPH | V | +| [0d31fa4418ec](https://github.com/aurora-silicon/linux/commit/0d31fa4418ec7c18ab984d6cfb0c4d10b80a0af1), [bffb1ed450d5](https://github.com/aurora-silicon/linux/commit/bffb1ed450d58d0c19f9e312c8b0c2314b9f1c53), [d575cca893bc](https://github.com/aurora-silicon/linux/commit/d575cca893bce9bedb5375be6bb8c209991faec6) | Ryan Murray | arm64: dts: apple: t8140: Add SPMI controller and PMIC NVMEM cells | Not applicable — PERIPH | V | +| [780ab337fd92](https://github.com/aurora-silicon/linux/commit/780ab337fd92e4511f4e4ee698820ae15d007cbc), [368b5343b9f7](https://github.com/aurora-silicon/linux/commit/368b5343b9f7c69683ee5a0c5239a6f96e8177fd), [c3874394cec6](https://github.com/aurora-silicon/linux/commit/c3874394cec613afc6b5c8615eae5cdd013e558c) | Ryan Murray | arm64: dts: apple: t8140: Add SMC node | Not applicable — PERIPH | V | +| [53296a0f9b6e](https://github.com/aurora-silicon/linux/commit/53296a0f9b6eae07ebce8a55c8b16309d87b2656), [985ee0213ef3](https://github.com/aurora-silicon/linux/commit/985ee0213ef3154c4d8c9763157f9620a7ce0243), [b8850ad18ff8](https://github.com/aurora-silicon/linux/commit/b8850ad18ff8d0a489c810fdafece6b8349f8694) | Ryan Murray | arm64: dts: apple: t8140: Add ANS2 NVMe controller | Not applicable — PERIPH | V | +| [4767bc0b221c](https://github.com/aurora-silicon/linux/commit/4767bc0b221cce5fb0c20dbacffe6f6bca7bc4a0), [5b9145de86c1](https://github.com/aurora-silicon/linux/commit/5b9145de86c13364ed3b9d465334674e7dc87083), [0b9461d4be5a](https://github.com/aurora-silicon/linux/commit/0b9461d4be5a77c0c74138900b8b92a7e193f8bb) | Ryan Murray | arm64: dts: apple: t8140-j700: Name the ALS calibration file | Not applicable — PERIPH | V | +| [2b45c6011879](https://github.com/aurora-silicon/linux/commit/2b45c601187962fca0b9289fdb1408e36ef5f838), [613b821cb4bb](https://github.com/aurora-silicon/linux/commit/613b821cb4bb48ccb999f75c4d5815f094f095be), [aefc56975b57](https://github.com/aurora-silicon/linux/commit/aefc56975b57d0e07a4a3e92d7e7af85f0a45cb4) | Ryan Murray | arm64: dts: apple: t8140-pmgr: Add ATC0 and nub SPMI A0 power domains | Not applicable — PERIPH | V | +| [e5992bd6f02c](https://github.com/aurora-silicon/linux/commit/e5992bd6f02c42e2e7404be54a41d8acaf685bb4), [dc6bc1f1613f](https://github.com/aurora-silicon/linux/commit/dc6bc1f1613f7e836b9514f40c07cb424622fca1), [0e335bcc4a79](https://github.com/aurora-silicon/linux/commit/0e335bcc4a799cc546ff5a6e9d5cdde644a53eea) | Ryan Murray | arm64: dts: apple: t8140: Add the MTP coprocessor and DockChannel HID nodes | Not applicable — PERIPH | V | +| [601fe7359e94](https://github.com/aurora-silicon/linux/commit/601fe7359e94f9f527487f2d1725cbfad8ea1ff2), [2d6ae0d0f507](https://github.com/aurora-silicon/linux/commit/2d6ae0d0f5073b6e29cb970926855de13d24a770), [895980a58c46](https://github.com/aurora-silicon/linux/commit/895980a58c46a6a1ec2a4e2bb2d5b9e815d4b349) | Ryan Murray | arm64: dts: apple: t8140: Add the USB-C controller, ATC PHY and SPMI A0 bus | Not applicable — PERIPH | V | +| [addbb17c2a81](https://github.com/aurora-silicon/linux/commit/addbb17c2a810bcaa1d68f6f9bc8e7fb401a508a), [7d1387799cba](https://github.com/aurora-silicon/linux/commit/7d1387799cba4442ba7ea99a95a962535cf26fc2), [7ded6b918bd3](https://github.com/aurora-silicon/linux/commit/7ded6b918bd3436fa2dc81b8b65f336bbe66ae85) | Ryan Murray | arm64: dts: apple: t8140-j700: Enable the keyboard and trackpad | Not applicable — PERIPH | V | +| [70f40a78d8ea](https://github.com/aurora-silicon/linux/commit/70f40a78d8ea8db8b300d3d084b4228b3e172f19), [c94e793fea2a](https://github.com/aurora-silicon/linux/commit/c94e793fea2ad398c3aade3d3649ebc377189ec2), [b8f170b9347b](https://github.com/aurora-silicon/linux/commit/b8f170b9347ba943a11a30f6642a9532758965e4) | Ryan Murray | arm64: dts: apple: t8140-j700: Enable the USB-C ports | Not applicable — PERIPH | V | +| [ea013658bf93](https://github.com/aurora-silicon/linux/commit/ea013658bf93d944617d01034cd7a5a3ece105a8), [e5a7b83611cc](https://github.com/aurora-silicon/linux/commit/e5a7b83611ccc434923524ea7bfb27f4e9c2a2ec), [09b788437876](https://github.com/aurora-silicon/linux/commit/09b788437876d50bf0d13d0d8cf7bfe9487a934b) | Ryan Murray | arm64: dts: apple: t8140-j700: Enable the USB DARTs | Not applicable — PERIPH | V | +| [a97766963d24](https://github.com/aurora-silicon/linux/commit/a97766963d247d039cd76be83fd896b895e1ad2b), [e499013c56ad](https://github.com/aurora-silicon/linux/commit/e499013c56ad475dd45866c64e28dd33f95235c3), [d880e057bc1e](https://github.com/aurora-silicon/linux/commit/d880e057bc1e250d54a0b7dc2db23928f802a1d0) | Ryan Murray | arm64: dts: apple: t8140: Add the SMC RTC | Not applicable — PERIPH | V | +| [50e24690d43e](https://github.com/aurora-silicon/linux/commit/50e24690d43e525c1b2b217aa828a18e4c6503b9), [21ab6edf3da3](https://github.com/aurora-silicon/linux/commit/21ab6edf3da3111ed771155a9dbd2c83118cd0dc), [e90d32748ea7](https://github.com/aurora-silicon/linux/commit/e90d32748ea7857d242afc714365949e9ad86a3b) | Ryan Murray | arm64: dts: apple: t8140-j700: Describe the remaining SMC temperatures | Not applicable — PERIPH | V | +| [2cb796ddf423](https://github.com/aurora-silicon/linux/commit/2cb796ddf423b6d76e9d2ab1231f0b48ddbc2614), [e8d16b77d690](https://github.com/aurora-silicon/linux/commit/e8d16b77d6901f18a468d3d1b636990aa97f89cd), [23b613dfe8e7](https://github.com/aurora-silicon/linux/commit/23b613dfe8e757e1b599dc7cdfb7f9717687dc21) | Ryan Murray | arm64: dts: apple: t8140-j700: Describe the SMC current, power and voltage rails | Not applicable — PERIPH | V | +| [0cd1a28f2113](https://github.com/aurora-silicon/linux/commit/0cd1a28f2113c72a53052fb2733c2aced2f54476), [c2845ec53a91](https://github.com/aurora-silicon/linux/commit/c2845ec53a915dcadc4e68bbd59f97ca0764ddd6) | DjDeveloperr | iommu: apple-dart: support tunneled PCIe-C DMA aliases | Not applicable — TUNNEL; `drivers/iommu/Kconfig` | V | +| [6ff009379755](https://github.com/aurora-silicon/linux/commit/6ff00937975526780a22aa763211ba612f042997), [ee60f6cdcff8](https://github.com/aurora-silicon/linux/commit/ee60f6cdcff8561bff5ec48b10ba2a536a23ed9f) | DjDeveloperr | iommu: apple-dart: support retained PCIe-C tunnel domains | Not applicable — TUNNEL; `drivers/iommu/apple-dart.c` | V | +| [05ae2b2dfc28](https://github.com/aurora-silicon/linux/commit/05ae2b2dfc2853d7ae0f797d6299661e957116f3), [5987e8a08203](https://github.com/aurora-silicon/linux/commit/5987e8a08203974fe539aed3ce2de8ad5a3990a7) | Ryan Murray | arm64: dts: apple: t8140: Describe observed behaviour, not other implementations | Not applicable — VERIFY | V | +| [61f979fc1825](https://github.com/aurora-silicon/linux/commit/61f979fc18256a03db72933dde7f356a44f31eb2), [bf3cb50f012f](https://github.com/aurora-silicon/linux/commit/bf3cb50f012f9ecaeb465e861d8e896e580a69a0), [4fa6c2b053a6](https://github.com/aurora-silicon/linux/commit/4fa6c2b053a65088ca20641480fd9ed8da55716e) | Ace | arm64: dts: apple: Add diagnostic J700 PCIe root-port topology | Not applicable — PERIPH | V | +| [c309c0ebb058](https://github.com/aurora-silicon/linux/commit/c309c0ebb058bf6cd1df2503d121ace539448ecc), [5a4974fde407](https://github.com/aurora-silicon/linux/commit/5a4974fde40748a151a4c23c0df730f1148bc2df) | Ace | arm64: dts: apple: t8140-j700: enable bounded PCIe endpoint bootstrap and enumeration | Not applicable — PERIPH | V | +| [56e442c7cbbe](https://github.com/aurora-silicon/linux/commit/56e442c7cbbe49c2613a5209fbcf772e90bec10e), [716b60de9620](https://github.com/aurora-silicon/linux/commit/716b60de96207e43d3c06eeb9c424f147a5d16a3), [8a03c5341e27](https://github.com/aurora-silicon/linux/commit/8a03c5341e27e9066e15f924e88aa44f848b5294) | DjDeveloperr | arm64: dts: apple: enable AVD on T8140 | Not applicable — PERIPH | V | +| [2b9ef0954320](https://github.com/aurora-silicon/linux/commit/2b9ef0954320320edbee5b75e742176aef3ee8b3), [6f57627a710e](https://github.com/aurora-silicon/linux/commit/6f57627a710e655f30ec73ece845d18c52663d78), [4088c3b1854c](https://github.com/aurora-silicon/linux/commit/4088c3b1854cdbaf106d8bfe72510551bf9fd43c) | Ryan Murray | arm64: dts: apple: t8140: add disabled display power domains | Not applicable — PERIPH | V | +| [7e14b8e9e9a2](https://github.com/aurora-silicon/linux/commit/7e14b8e9e9a2bcb983725fc9a0cb3c016a5ed12b), [1b05c65c4276](https://github.com/aurora-silicon/linux/commit/1b05c65c42763d955c7ca69ef849d6bf5fac7355), [004200ec1cc8](https://github.com/aurora-silicon/linux/commit/004200ec1cc8005fa692d797d2c5fb009ec9e05a) | Ryan Murray | arm64: dts: apple: t8140: add disabled internal display DARTs | Not applicable — PERIPH | V | +| [d7de8bb51e4f](https://github.com/aurora-silicon/linux/commit/d7de8bb51e4f654b1c1d33c8cabd3290a9e231ea), [0c29d4d4a4ea](https://github.com/aurora-silicon/linux/commit/0c29d4d4a4ea28e024596f605676dc0d1d1e3ba0), [d2f162f6d7c2](https://github.com/aurora-silicon/linux/commit/d2f162f6d7c24ed783657112086f7b56e1fb0cbe) | Ryan Murray | arm64: dts: apple: t8140: add disabled external display DARTs | Not applicable — PERIPH | V | +| [7a28ede30a1f](https://github.com/aurora-silicon/linux/commit/7a28ede30a1ff3894d8f2407ba657d51e8d9bd95), [042b2a224bf6](https://github.com/aurora-silicon/linux/commit/042b2a224bf671bacb56b6e5345b3eb399c7ec02), [aec8cb1999f7](https://github.com/aurora-silicon/linux/commit/aec8cb1999f731bbe1895509dbcc6e2fed977348) | Ryan Murray | arm64: dts: apple: t8140: Add the SEP, its DART and mailbox, and SPI2 | Not applicable — PERIPH | V | +| [07029412ae8d](https://github.com/aurora-silicon/linux/commit/07029412ae8d5832c1323d3eeb8a08b09f7b5be0), [1eb7bfe18b01](https://github.com/aurora-silicon/linux/commit/1eb7bfe18b01c0a088dd8e25a72ee5af3a09531a), [29987787b923](https://github.com/aurora-silicon/linux/commit/29987787b9238b15b41b92da87e5fcbf8a571e8c) | Ryan Murray | arm64: dts: apple: t8140-j700: Enable the SEP and the Touch ID sensor | Not applicable — PERIPH | V | +| [7d84216f74e8](https://github.com/aurora-silicon/linux/commit/7d84216f74e8e9e5827eabc9f173524ce5520c9b), [6049af950206](https://github.com/aurora-silicon/linux/commit/6049af950206644d95b8cd3ca11fe0722ff71257), [52f1ba512244](https://github.com/aurora-silicon/linux/commit/52f1ba51224431d256250c69784e1996f6ee63cc) | Acelogic | arm64: dts: apple: wire the J700 AVE encoder | Not applicable — PERIPH | V | +| [5f1fbb7bd670](https://github.com/aurora-silicon/linux/commit/5f1fbb7bd670ca8a26c303594f9ed54535e51f02), [519558a43644](https://github.com/aurora-silicon/linux/commit/519558a43644d79c2cc15135554748ffe6fadc81), [f898b116beaa](https://github.com/aurora-silicon/linux/commit/f898b116beaaab7add264a0f074b21743875d3ae) | Ryan Murray | arm64: dts: apple: Describe additional T8140 peripheral power states | Not applicable — PERIPH | V | +| [555932c62f03](https://github.com/aurora-silicon/linux/commit/555932c62f03ec2833c5db9b39e2d2dc956501cb), [ed2ef28706b7](https://github.com/aurora-silicon/linux/commit/ed2ef28706b7c2ed13aef2f90eb7191b4e34338e), [f75a1041fca4](https://github.com/aurora-silicon/linux/commit/f75a1041fca487774f29b133f736296f4d73a4e3) | Ryan Murray | arm64: dts: apple: Wire up remaining supported J700 peripherals | Not applicable — PERIPH | V | +| [a3d7465380ff](https://github.com/aurora-silicon/linux/commit/a3d7465380ffbf5f9cb4eddb550fabfdd71d3a17), [49fdcbcee4f4](https://github.com/aurora-silicon/linux/commit/49fdcbcee4f4569e6b65f7912e05502a38299743) | Ryan Murray | arm64: dts: apple: describe the J700 PMU alarm function | Not applicable — PERIPH | V | +| [d810cb82e82d](https://github.com/aurora-silicon/linux/commit/d810cb82e82de69013754b6031d00a83a6d15e41), [4e0543ee6caf](https://github.com/aurora-silicon/linux/commit/4e0543ee6cafb4250aaf332f894773412b86dfe2), [b9a339703e95](https://github.com/aurora-silicon/linux/commit/b9a339703e95a6a65e0e4515caa4c4f6afb0d9ce) | Ryan Murray | arm64: dts: apple: describe the disabled T8140 Neural Engine | Not applicable — PERIPH | V | +| [19f7cbdd84fe](https://github.com/aurora-silicon/linux/commit/19f7cbdd84fef12ca56aad9b654b46465cc2e198), [6b57d5423048](https://github.com/aurora-silicon/linux/commit/6b57d5423048ac4c70cd99d7473494ac3d37b2db), [93bc234385f5](https://github.com/aurora-silicon/linux/commit/93bc234385f5bf51b276e9d2e5abe8280e2ff92c) | Ryan Murray | arm64: dts: apple: t8140: Leave display coprocessor power to firmware | Not applicable — PERIPH | V | +| [0f5548d142b1](https://github.com/aurora-silicon/linux/commit/0f5548d142b1d73c44d270c91d5c359272dae499), [7c3c5ee5a070](https://github.com/aurora-silicon/linux/commit/7c3c5ee5a0702bb5b9f0ae4cde6b837cbafa3485), [9e6acbbd21ba](https://github.com/aurora-silicon/linux/commit/9e6acbbd21ba92409dd4f24074532b0bad997792) | Ryan Murray | arm64: dts: apple: t8140: Describe the internal display coprocessor | Not applicable — PERIPH | V | +| [100ff1ebedd3](https://github.com/aurora-silicon/linux/commit/100ff1ebedd337758a4ff3ba3fd3f2f46a9f5be9), [45ae8211efea](https://github.com/aurora-silicon/linux/commit/45ae8211efeafe3bff64b569370942c5218f1fec), [061370b4c70f](https://github.com/aurora-silicon/linux/commit/061370b4c70fd888ea6dc520a8988208f4aebca8) | Ryan Murray | arm64: dts: apple: t8140: Hand the J700 panel over to the DCP | Not applicable — PERIPH | V | +| [29f8091fe351](https://github.com/aurora-silicon/linux/commit/29f8091fe351a0cdb781cf18ac5748ca15feca42), [4f3906d225f4](https://github.com/aurora-silicon/linux/commit/4f3906d225f44739df672ac0b76715407f52e586), [a7fa3d8fbf56](https://github.com/aurora-silicon/linux/commit/a7fa3d8fbf5690df6553896a4f984b8cdc8069c2) | Ryan Murray | arm64: dts: apple: t8140: describe each power state once | Not applicable — PERIPH | V | +| [89c54797ae77](https://github.com/aurora-silicon/linux/commit/89c54797ae77fea8e8878a68e0b56f382cfb0409), [07969983cb1e](https://github.com/aurora-silicon/linux/commit/07969983cb1edefaec1b4b0aabb2307b0b14dacd), [953220ab6d74](https://github.com/aurora-silicon/linux/commit/953220ab6d748bd05db3854f374b701696f6c795) | Ryan Murray | arm64: dts: apple: t8140: describe the external display DARTs once | Not applicable — PERIPH | V | +| [55a5beb425ae](https://github.com/aurora-silicon/linux/commit/55a5beb425ae7c901851c2185e3219d8c0348fcb), [f0234a79d172](https://github.com/aurora-silicon/linux/commit/f0234a79d172479cb7dbcdb713daa5739ad87f74), [c80113dfaab9](https://github.com/aurora-silicon/linux/commit/c80113dfaab9e985b9f617bfde06c481fd17376c) | Ryan Murray | arm64: dts: apple: t8140-j700: give the two RTCs fixed numbers | Not applicable — PERIPH | V | +| [9e1fb240e1df](https://github.com/aurora-silicon/linux/commit/9e1fb240e1df4064a9232007daa5c8797dfbcda4), [19e2b5adfe69](https://github.com/aurora-silicon/linux/commit/19e2b5adfe6905ab2f3fff6ad1a12801d711c607), [ea044d49c76c](https://github.com/aurora-silicon/linux/commit/ea044d49c76c18079848672c2779c7521a6b8fc3) | Ryan Murray | arm64: dts: apple: t8140: Keep the GPIO power state on for the SEP | Not applicable — PERIPH | V | +| [4334bbd7162a](https://github.com/aurora-silicon/linux/commit/4334bbd7162af085ed1c8c7b63859bed8c6646ef), [f1c076a9bf8a](https://github.com/aurora-silicon/linux/commit/f1c076a9bf8a850bec0140fb3e91a9607aadcf4e), [30812f491575](https://github.com/aurora-silicon/linux/commit/30812f491575ede27e5be059fd6afc4e985166f1) | DjDeveloperr | arm64: dts: apple: t8140-j700: keep the DCS rail out of thermal control | Not applicable — DCSOFF | P,R | +| [baacaf604632](https://github.com/aurora-silicon/linux/commit/baacaf60463212b2270f0ad0b50cdabe4e27ab6d), [071651bd5c30](https://github.com/aurora-silicon/linux/commit/071651bd5c30330ecb9980c3c1f2cc4e2f04d1a6), [40a9c929c55f](https://github.com/aurora-silicon/linux/commit/40a9c929c55f2b13c39e732fc2ae3f10e1506634) | Ryan Murray | arm64: dts: apple: describe disabled J700 external resources | Not applicable — PERIPH | V | +| [13af1cb29b95](https://github.com/aurora-silicon/linux/commit/13af1cb29b95b1fc260870868d3c54a596cc483d), [f284458cdfc3](https://github.com/aurora-silicon/linux/commit/f284458cdfc33d2746bb4ede9174adc3afaa2f7f) | Ryan Murray | arm64: dts: apple: t8140: describe the external binding the display master recognizes | Not applicable — PERIPH | V | +| [a7f9b86ea187](https://github.com/aurora-silicon/linux/commit/a7f9b86ea18778f313b73cd174debe54e4dbc09c), [cffe838aca8e](https://github.com/aurora-silicon/linux/commit/cffe838aca8e275526730bd6f3fee5ebcaec0c47), [205a56d33cb0](https://github.com/aurora-silicon/linux/commit/205a56d33cb07184d7dfe17ab8aba0996cff3efe) | Ryan Murray | arm64: dts: apple: retain the inherited external display supply | Not applicable — PERIPH | V | +| [328dacb9d1ad](https://github.com/aurora-silicon/linux/commit/328dacb9d1adcbc1be84e8dae660c72bc12a3005), [3d16013f9795](https://github.com/aurora-silicon/linux/commit/3d16013f979572f7f7f6d1573f2af739e8ac25b4), [2e6003f6db07](https://github.com/aurora-silicon/linux/commit/2e6003f6db07b0e0a6b2bf2b977753bee47556b0) | Ryan Murray | arm64: dts: apple: add an explicit external display trial tree | Not applicable — PERIPH | V | +| [6b82a41053b8](https://github.com/aurora-silicon/linux/commit/6b82a41053b87c8bd0917f72f71778293aa7f276), [70cce5cd20ef](https://github.com/aurora-silicon/linux/commit/70cce5cd20efb8a71d49563b45481c32e0de23a6), [0a0d25f4a99b](https://github.com/aurora-silicon/linux/commit/0a0d25f4a99b66a08a2ab4a22314782aaad7b3e2) | DjDeveloperr | arm64: dts: apple: t8140-j700: drive the SPI NOR on a single data line | Not applicable — PERIPH | V | +| [d56b7ce044f2](https://github.com/aurora-silicon/linux/commit/d56b7ce044f274b153a3b1f9192d3fd4c7cb2d85), [df94160d7567](https://github.com/aurora-silicon/linux/commit/df94160d756756e14d4bc818fe0652bcdb9adc30) | DjDeveloperr | arm64: dts: apple: t8140-j700: enable the validated external display profile by default | Not applicable — PERIPH | V | +| [f9e24b02bc85](https://github.com/aurora-silicon/linux/commit/f9e24b02bc8547acba1453cf462f0f210e707ba7), [be5f3162c985](https://github.com/aurora-silicon/linux/commit/be5f3162c9854d6b5ea8ca9e8f29f764b2c6251b) | Eryk Wieliczko | arm64: dts: apple: t8140: mark firmware-owned power domains | Not applicable — PERIPH | V | +| [7df682a70733](https://github.com/aurora-silicon/linux/commit/7df682a707337025aad3c093252ea7f8504e8345), [b0714e8783f6](https://github.com/aurora-silicon/linux/commit/b0714e8783f685f213c15890c7942c1c03d6c1e3) | Chris Kearney | PCI: apple: enumerate Thunderbolt PCIe on t8103 | Not applicable — LEGACY; `drivers/iommu/apple-dart.c` | V | +| [4c02ff022799](https://github.com/aurora-silicon/linux/commit/4c02ff022799be8aef47108a61671a6a6cd86038), [b388a175a612](https://github.com/aurora-silicon/linux/commit/b388a175a612be6c5d10056c6a10a7219030272d) | Chris Kearney | PCI: apple: finish tunneled IOMMU commands before the port clock stops | Not applicable — TUNNEL; `drivers/iommu/apple-dart.c` | V | +| [9e8a900fee27](https://github.com/aurora-silicon/linux/commit/9e8a900fee275872aa1019df27a4944b73c8940a), [a2b13c1f0369](https://github.com/aurora-silicon/linux/commit/a2b13c1f03696ae69101c8ed51b8901d32dffcd4), [5a7b76f31c3a](https://github.com/aurora-silicon/linux/commit/5a7b76f31c3a8ccdc48fd586e5b646fa0c49499d) | Chris Kearney | iommu/apple-dart: enforce command gating during tunnel teardown | Not applicable — TUNNEL; `drivers/iommu/apple-dart-test.c` | V | +| [39e90a052954](https://github.com/aurora-silicon/linux/commit/39e90a052954d1305a45ca0065eda42ec4aff639), [d15623fd3261](https://github.com/aurora-silicon/linux/commit/d15623fd32614870a9eb62d25afdd823a39bcde7), [65e3e0ed688a](https://github.com/aurora-silicon/linux/commit/65e3e0ed688a5a933a60d92ca79b6136c357b876) | Chris Kearney | iommu/apple-dart: report tunnel command-resume failures | Not applicable — TUNNEL; `drivers/iommu/apple-dart-test.c` | V | +| [3a94483e259c](https://github.com/aurora-silicon/linux/commit/3a94483e259cbc8d0aab638eaa4af5b72c4db6e6), [6e0c3cdf7855](https://github.com/aurora-silicon/linux/commit/6e0c3cdf78550987513b0d4c15ffec90b442b787), [64dfa24ca989](https://github.com/aurora-silicon/linux/commit/64dfa24ca989fec06367a7199164a70de7d99e35) | Chris Kearney | PCI: apple: describe tunneled PM ordering by Linux dependencies | Not applicable — TUNNEL; `drivers/iommu/apple-dart.c` | V | +| [5d4dd51e2769](https://github.com/aurora-silicon/linux/commit/5d4dd51e27695b5d9570e56b050e3574dfc1fd38), [e7292daedcaa](https://github.com/aurora-silicon/linux/commit/e7292daedcaa2186e5b8b484343dc3ec239b2805), [615be505e22c](https://github.com/aurora-silicon/linux/commit/615be505e22c592c1a87c193b22c58dc4c064840) | Chris Kearney | thunderbolt: restore M1 tunneled PCIe state and recover late link loss | Not applicable — TUNNEL; `drivers/iommu/apple-dart-test.c` | V | +| [95caa2cf7ac7](https://github.com/aurora-silicon/linux/commit/95caa2cf7ac7244080527b90da0d43ec917e1513), [92f33b680514](https://github.com/aurora-silicon/linux/commit/92f33b6805149c955b543a6e2c29f88c42886f6d), [fc559d251fca](https://github.com/aurora-silicon/linux/commit/fc559d251fcac7e904607a36fb4b17770e29f239) | Acelogic | irqchip/apple-aic: use architectural guest timer masks on T8142 | Not applicable — OTHERCHIP; `drivers/irqchip/irq-apple-aic.c` | V | +| [4d1bcf62c1af](https://github.com/aurora-silicon/linux/commit/4d1bcf62c1af6fa29a32e94d821f4174098cbd29), [4ae1f9604a1a](https://github.com/aurora-silicon/linux/commit/4ae1f9604a1aa503dd1b452578bf694dbadaca25), [40bc3b97c1d6](https://github.com/aurora-silicon/linux/commit/40bc3b97c1d6a75ff5f8af9bc75322388a5381e1) | DjDeveloperr | soc: apple: rtkit-helper: adopt a running T8140 MTP session | Not applicable — PERIPH; `drivers/soc/apple/rtkit-helper.c` | V | +| [ad7993e04cd2](https://github.com/aurora-silicon/linux/commit/ad7993e04cd2e39c4fe011740268a537868bb8c0), [58a792863c4f](https://github.com/aurora-silicon/linux/commit/58a792863c4fdd79342e5e83ece8f652993084ab), [80cabfefcdc5](https://github.com/aurora-silicon/linux/commit/80cabfefcdc52036a7902268ff42f2a9370fe5c2) | Acelogic | cpufreq: apple: Add bounded T8152 E and P/M performance domains | Not applicable — OTHERCHIP; `drivers/cpufreq/apple-soc-cpufreq.c` | V | +| [81aefe80b586](https://github.com/aurora-silicon/linux/commit/81aefe80b586fa545b20eb90090202cde51718e7), [48f073156423](https://github.com/aurora-silicon/linux/commit/48f0731564236dc79c43bbcdadf6b05cfd807e8a), [296dbb45adff](https://github.com/aurora-silicon/linux/commit/296dbb45adff8654c6f1f8029e7b6b567f990e0b) | Acelogic | cpufreq: apple: add T8142 command-state transitions | Not applicable — OTHERCHIP; `drivers/cpufreq/apple-soc-cpufreq.c` | V | +| [4c3405d11d29](https://github.com/aurora-silicon/linux/commit/4c3405d11d29ec8a49a14bdc546cef4411153b32), [b08be27ef235](https://github.com/aurora-silicon/linux/commit/b08be27ef235dfedb3d18cf4a91a821be8a7effc) | Ryan Murray | cpufreq: apple: retain the unmanaged P-state ceiling on T8152 | Not applicable — OTHERCHIP; `drivers/cpufreq/apple-soc-cpufreq.c` | V | +| [24f4a32e8ec9](https://github.com/aurora-silicon/linux/commit/24f4a32e8ec9438b198e7ca97658154bcaedc6f7), [129471fc83b8](https://github.com/aurora-silicon/linux/commit/129471fc83b89e08275048a812779583ef197a93), [c0bd8b9fec16](https://github.com/aurora-silicon/linux/commit/c0bd8b9fec163274b9e6056ae94fe54d53b565fb), [b4ee9bd8d923](https://github.com/aurora-silicon/linux/commit/b4ee9bd8d923949b19cc22484262670948cafc57) | DjDeveloperr | drm/asahi: clarify the opening polling budget [#171](https://github.com/aurora-silicon/linux/pull/171) | Not applicable — VERIFY; `drivers/gpu/drm/asahi/g17/mod.rs` | V | +| [52f6e8610625](https://github.com/aurora-silicon/linux/commit/52f6e86106255b26e84671649c5f4acb0f434cde), [840488b0b60d](https://github.com/aurora-silicon/linux/commit/840488b0b60d3162e92f050cc59a2b3967036544), [66631bc63295](https://github.com/aurora-silicon/linux/commit/66631bc632956bb7dfd1b4fb44e48ae90a10140e), [d0aadc1e99e6](https://github.com/aurora-silicon/linux/commit/d0aadc1e99e6f11a54bbca5ee685d7c015866789) | DjDeveloperr | drm/asahi: describe the G17 word tables without their generation history [#171](https://github.com/aurora-silicon/linux/pull/171) | Not applicable — VERIFY; `drivers/gpu/drm/asahi/g17/fw/initdata.rs` | V | +| [1c771d73ac6c](https://github.com/aurora-silicon/linux/commit/1c771d73ac6c5e953be9582f1b74ccb358e6322d), [d24b1a49853f](https://github.com/aurora-silicon/linux/commit/d24b1a49853fe6949b44790761678cd2d38e623e), [a6e06b47b49d](https://github.com/aurora-silicon/linux/commit/a6e06b47b49d2666a53b5bc45ba8e0fa22e74944) | Joshua Warren | drm/asahi: Add stats_export module parameter | Not applicable — STAT; `drivers/gpu/drm/asahi/Makefile` | T | +| [3f096100f658](https://github.com/aurora-silicon/linux/commit/3f096100f658c1563dafaa0f0ca8b77ea2a87fe3), [f436b4da5480](https://github.com/aurora-silicon/linux/commit/f436b4da5480140e5ba95d68e46f1e7dd7615db8) | Ryan Murray | drm/asahi: tie stats registration to a per-device snapshot owner | Not applicable — STAT; `drivers/gpu/drm/asahi/g17/mod.rs` | T | +| [fd13851e08b6](https://github.com/aurora-silicon/linux/commit/fd13851e08b689661332ee823ad65eafd4418c1a), [c91a9274e4b2](https://github.com/aurora-silicon/linux/commit/c91a9274e4b2736d5589826b07c8dc54c0e35055) | Ryan Murray | drm/asahi: describe the G17 tiler-heap shrink policy in hardware terms | Not applicable — VERIFY; `drivers/gpu/drm/asahi/g17/buffer.rs` | V | +| [af5c7b24a0e5](https://github.com/aurora-silicon/linux/commit/af5c7b24a0e5932026c8cbab8633f2563bf761ba), [f0ec35d78e5a](https://github.com/aurora-silicon/linux/commit/f0ec35d78e5adf9988c1bc71418d0a764c8d4ba3), [337b14463d50](https://github.com/aurora-silicon/linux/commit/337b14463d5010296efb0b17dfaa11b8f7601aae) | Ryan Murray | arm64: dts: apple: t8140: Fold the trailing soc override into the soc node [#314](https://github.com/aurora-silicon/linux/pull/314) | Not applicable — PERIPH | V | +| [3cd89c634a60](https://github.com/aurora-silicon/linux/commit/3cd89c634a60071014683a27d497e46b8e2e2fc5), [f5ba7b6ee41c](https://github.com/aurora-silicon/linux/commit/f5ba7b6ee41ca4545745bfcc312fdbc0abf3e491), [6f1b4ab6f2f0](https://github.com/aurora-silicon/linux/commit/6f1b4ab6f2f036dedd104b83df774d6284ecab74) | Ryan Murray | arm64: dts: apple: t8140: Tidy the mailbox and pinctrl comments [#314](https://github.com/aurora-silicon/linux/pull/314) | Not applicable — PERIPH | V | +| [553e961d56c9](https://github.com/aurora-silicon/linux/commit/553e961d56c9ecbf6d58561c98468496955ad1d3), [38dd24cdc5a3](https://github.com/aurora-silicon/linux/commit/38dd24cdc5a3e13555afcac85f28d8ca3fc77a8e), [742366203cc8](https://github.com/aurora-silicon/linux/commit/742366203cc8a1f9356d82a43d52e2cb6fa97621) | Ryan Murray | pmdomain: apple: pmp-report: remove the raw DVFS debugfs dump [#317](https://github.com/aurora-silicon/linux/pull/317) | Not applicable — RAWDIAG; `drivers/pmdomain/apple/pmp-report.c` | O | +| [d04cbb892d26](https://github.com/aurora-silicon/linux/commit/d04cbb892d26dca250de4ff6f8c6cde5446682fb), [34754dd5bf3b](https://github.com/aurora-silicon/linux/commit/34754dd5bf3bd3d21b3409b6768af8b8fa38a045), [21c1d3f21ac4](https://github.com/aurora-silicon/linux/commit/21c1d3f21ac49f4223993ed1beefca1d6d6e89cd) | Ryan Murray | arm64: dts: apple: t8140: state why PMGR domains off at boot are always-on [#317](https://github.com/aurora-silicon/linux/pull/317) | Not applicable — PERIPH | V | +| [c3760b2f1cbd](https://github.com/aurora-silicon/linux/commit/c3760b2f1cbd4e17586ced9ac0d9ae7e8fe7d024), [732facd15036](https://github.com/aurora-silicon/linux/commit/732facd1503659e30fb096e0276e2efcdfa41ed7), [0d1799c7c179](https://github.com/aurora-silicon/linux/commit/0d1799c7c179d3b6e914eeaee59da7529e824565) | Ryan Murray | arm64: dts: apple: t8140-j700: trim measurement notes from comments [#317](https://github.com/aurora-silicon/linux/pull/317) | Not applicable — PERIPH | V | +| [ac9e0f78ef56](https://github.com/aurora-silicon/linux/commit/ac9e0f78ef560ef9d7a5c5655c7253e938b08d71), [f2928aa2ef66](https://github.com/aurora-silicon/linux/commit/f2928aa2ef662ff9045367ee9910a62a0f0b4e4b) | Ryan Murray | pmdomain: apple: pmc: Test partial enables and probe unwind | Not applicable — VERIFY; `drivers/pmdomain/apple/pmc-test.c` | V | +| [e8e0eae211f9](https://github.com/aurora-silicon/linux/commit/e8e0eae211f9d97ad777522ac8bf0c331ebf20f8), [a20499ed1321](https://github.com/aurora-silicon/linux/commit/a20499ed1321d5e5054d0f3dfd3f3a622e2938be) | Ryan Murray | soc: apple: mcc: Test partial allocation enable and cache handoff | Not applicable — VERIFY; `drivers/soc/apple/Kconfig` | V | +| [f2669637e7a6](https://github.com/aurora-silicon/linux/commit/f2669637e7a6c7e24f8eac823097156728ce64a8), [e3d59133d7a7](https://github.com/aurora-silicon/linux/commit/e3d59133d7a793ac15e3fa7328d11dafff8d8c92), [d6094473be4c](https://github.com/aurora-silicon/linux/commit/d6094473be4ceae69e8385ffb03891a22c60c3e4) | Ryan Murray | soc: apple: rtkit-helper: Terminate log messages [#345](https://github.com/aurora-silicon/linux/pull/345) | Not applicable — PERIPH; `drivers/soc/apple/rtkit-helper.c` | V | +| [8a83508a3870](https://github.com/aurora-silicon/linux/commit/8a83508a3870e8dd8d15ade42c759ed1be8fc9c1), [bee2492c2bac](https://github.com/aurora-silicon/linux/commit/bee2492c2bac7c4cf6022cffadd92b1e4fae2f54), [0afefd991028](https://github.com/aurora-silicon/linux/commit/0afefd9910288c1783a2756c4240dfc80ee8470f) | Ryan Murray | iommu: apple-dart: Return IOMEM_ERR_PTR() from the tunneled MMIO helper [#345](https://github.com/aurora-silicon/linux/pull/345) | Not applicable — TUNNEL; `drivers/iommu/apple-dart.c` | V | +| [d970f88dd819](https://github.com/aurora-silicon/linux/commit/d970f88dd819b24905808cac119647625e382303), [6b8a1244cb2c](https://github.com/aurora-silicon/linux/commit/6b8a1244cb2c8e0da6290fe794fb442b5cb9404e), [27da3a5d53ca](https://github.com/aurora-silicon/linux/commit/27da3a5d53ca11f2aeff4916d25f40308c378a07) | Eryk Wieliczko | arm64: dts: apple: t8140-j700: give the boot framebuffer its panel | Not applicable — PERIPH | V | +| [8a9127fdd1f5](https://github.com/aurora-silicon/linux/commit/8a9127fdd1f54bfe2913e7996073a994eb41ff04), [8feaf5d2fa71](https://github.com/aurora-silicon/linux/commit/8feaf5d2fa714a11984b1eeafd5dd2e076ec4af6), [748b2a25f2ca](https://github.com/aurora-silicon/linux/commit/748b2a25f2ca960017eef868e2323a27e07f0fe2) | Ryan Murray | drm/asahi: clean up the statistics Rust style | Not applicable — STAT; `drivers/gpu/drm/asahi/channel.rs` | T | +| [c987a1793e75](https://github.com/aurora-silicon/linux/commit/c987a1793e759da730ff84384a601d9d96d916d3), [37b3b1f7de85](https://github.com/aurora-silicon/linux/commit/37b3b1f7de8547eca29a4414465b5b4d705d413d) | Chris Kearney | drm/apple: wait for T6030 RTKit callbacks before a DCP is freed | Not applicable — G15; `drivers/gpu/drm/apple/dcp.c` | V | +| [9639e3803ffb](https://github.com/aurora-silicon/linux/commit/9639e3803ffb7f73c58dbbacad17a6e40b7596ac), [346fd6083ab2](https://github.com/aurora-silicon/linux/commit/346fd6083ab2d45a079fe512c6b3e68affd4cc6f) | Ryan Murray | drm/asahi: free completed commands on system_dfl_wq | Not applicable — LEGACY; `drivers/gpu/drm/asahi/workqueue.rs` | V | +| [3e6775c52948](https://github.com/aurora-silicon/linux/commit/3e6775c52948793951b618817b3715eec5f91924), [b9527e10b2b6](https://github.com/aurora-silicon/linux/commit/b9527e10b2b683dff7d4b718e9af6f93fb0a005f) | Ryan Murray | arm64: add an isolated T8140 graphics and power profile | Not applicable — PERIPH | V | +| [7f40cd79f058](https://github.com/aurora-silicon/linux/commit/7f40cd79f0582c4ca1abbad36f1e923d8163779f), [58db6bfedcec](https://github.com/aurora-silicon/linux/commit/58db6bfedcec7cb4a2be3843c5d3665b4680553e) | Chris Kearney | soc: apple: test every PMP integer replacement width | Not applicable — VERIFY; `drivers/soc/apple/pmp_bootargs.rs` | V | +| [0a1f130cd042](https://github.com/aurora-silicon/linux/commit/0a1f130cd0427037fc94a4ad39e54a3c66d5dbac), [f11518fc495d](https://github.com/aurora-silicon/linux/commit/f11518fc495d51dc3c5d8f67cc10d7cb02a4d739) | Eryk Wieliczko | pmdomain: apple: add the GFX TVM gate | Not applicable — G16; `drivers/pmdomain/apple/Makefile` | V | +| [4e7c8a74e4a5](https://github.com/aurora-silicon/linux/commit/4e7c8a74e4a5ef03a1d06b5cd73f01c68b75a63d), [fe7d063e4cee](https://github.com/aurora-silicon/linux/commit/fe7d063e4cee437b5196c438f9f19a6ad7e22afc) | Satya Benson | pmdomain: apple: gfx-tvm: accept the T8122 | Not applicable — G15; `drivers/pmdomain/apple/gfx-tvm.c` | V | +| [460c0a77ca86](https://github.com/aurora-silicon/linux/commit/460c0a77ca860aef48d7ed449d65a878cffab9ed), [c8aed729e9bf](https://github.com/aurora-silicon/linux/commit/c8aed729e9bf8d23f0adabff999aa16d34201d76) | Joshua Warren | gpu: let the firmware preempt compute work on G14X | Not applicable — LEGACY; `drivers/gpu/drm/asahi/asahi_core.rs` | V | +| [80797e4f1b2b](https://github.com/aurora-silicon/linux/commit/80797e4f1b2bbde71964ea2253553d2c2dbbe379), [9c7ab8b412a5](https://github.com/aurora-silicon/linux/commit/9c7ab8b412a50123bf7dbc642368aa7f930d2d41) | Ryan Murray | drm/asahi: gate compile-only M3 backends before resource setup | Not applicable — G15; `drivers/gpu/drm/asahi/asahi_core.rs` | V | +| [700bfca794e5](https://github.com/aurora-silicon/linux/commit/700bfca794e50093c6e4d2b4231aca6252451a41), [860d185ccc57](https://github.com/aurora-silicon/linux/commit/860d185ccc575a47df4962cb1cf32da8f27ff35f) | Chris Kearney | drm/asahi: add the M3 (G15S) GPU runtime (M3-only split) | Not applicable — G15; `drivers/gpu/drm/asahi/m3/m3_compute_layout.rs` | V | +| [01680f395e55](https://github.com/aurora-silicon/linux/commit/01680f395e557e93b331ad510fbf255e6622c2a0), [a846942e367d](https://github.com/aurora-silicon/linux/commit/a846942e367dade6002d2d456ae6249c50d04d39) | Eryk Wieliczko | drm/asahi: share M3 M4 host retirement accounting (M3-only split) | Not applicable — G15; `drivers/gpu/drm/asahi/m3/agx_host_progress.rs` | V | +| [9593d5f2498f](https://github.com/aurora-silicon/linux/commit/9593d5f2498f3243100c828cc7bbf7ecdaddb861), [4bcb6f1acab4](https://github.com/aurora-silicon/linux/commit/4bcb6f1acab42a1772bf62aaae00c5fb1f6007cd) | Eryk Wieliczko | drm/asahi: account M3 GEM and coherent firmware backing lifetimes (M3-only split) | Not applicable — G15; `drivers/gpu/drm/asahi/m3/agx_memory_stats.rs` | V | +| [bba3fa8f4562](https://github.com/aurora-silicon/linux/commit/bba3fa8f4562761864f8f57b1a461bcd50ec3d64), [3e963ea31aca](https://github.com/aurora-silicon/linux/commit/3e963ea31acaad3e4a78dc137f12339fa8ebb5ee) | Chris Kearney | drm/asahi: expose bounded native retirement timing snapshots (M3-only split) | Not applicable — G15; `drivers/gpu/drm/asahi/m3/agx_timing_stats.rs` | V | +| [6d24d16ce24c](https://github.com/aurora-silicon/linux/commit/6d24d16ce24c8af1902191781e1255807ccd8aae), [1324a0258bb0](https://github.com/aurora-silicon/linux/commit/1324a0258bb0d0987993a3c34c2a74b0f2093cce) | Chris Kearney | drm/asahi: expose host-tracked pending GPU activity separately from completions (M3-only split) | Not applicable — G15; `drivers/gpu/drm/asahi/m3/agx_host_progress.rs` | V | +| [a4bbcd4baf93](https://github.com/aurora-silicon/linux/commit/a4bbcd4baf933a15e5bc75b2c7c5603d8b1b6998), [8faa4de3948b](https://github.com/aurora-silicon/linux/commit/8faa4de3948b0795c068eeeb0456a6a7dfc45077) | Chris Kearney | drm/asahi: avoid repeated activity fences while GPU work is pending (M3-only split) | Not applicable — G15; `drivers/gpu/drm/asahi/m3/agx_host_progress.rs` | V | +| [2397ac8a0861](https://github.com/aurora-silicon/linux/commit/2397ac8a086119bba90f6fc0d8d430e035d27f30), [b62d349e0fc6](https://github.com/aurora-silicon/linux/commit/b62d349e0fc6e160b9dabd167166a2d87ecfdbce) | Eryk Wieliczko | drm/asahi: remove M3 lifetime submission cap with wrapping wire stamps (M3-only split) | Not applicable — G15; `drivers/gpu/drm/asahi/m3/m3_timeline.rs` | V | +| [ce78a5b0c517](https://github.com/aurora-silicon/linux/commit/ce78a5b0c5173dbb9b5d7d0017488c43a07f222f), [578aa862e357](https://github.com/aurora-silicon/linux/commit/578aa862e357f0f61e18dc8e26181eeb5c0b9d7b) | Eryk Wieliczko | drm/asahi: preserve nonzero M3 stamps and dependencies through rollover (M3-only split) | Not applicable — G15; `drivers/gpu/drm/asahi/m3/m3_timeline.rs` | V | +| [646db65b2128](https://github.com/aurora-silicon/linux/commit/646db65b212826a29bd969332760722077926d61), [1d6a90b4117f](https://github.com/aurora-silicon/linux/commit/1d6a90b4117fe9cb84aa7863f491d18d37fcf0c5) | Chris Kearney | drm/asahi: remove unused backend helpers and narrow layout visibility (M3-only split) | Not applicable — G15; `drivers/gpu/drm/asahi/m3/m3_compute_storage.rs` | V | +| [a91573de916a](https://github.com/aurora-silicon/linux/commit/a91573de916a12ddc980c75d977567e57725e449), [f939d03f703f](https://github.com/aurora-silicon/linux/commit/f939d03f703fe760f413a87d435769b3aa970d21) | Chris Kearney | treewide: apple: reword M3 comments and messages as plain technical statements (M3-only split) | Not applicable — G15; `drivers/gpu/drm/asahi/m3/m3_compute_sequence.rs` | V | +| [997a7dba8158](https://github.com/aurora-silicon/linux/commit/997a7dba81582c94d1bdcc26aaae6031a486390c), [262009a6c7f9](https://github.com/aurora-silicon/linux/commit/262009a6c7f9dc7b6ca4abb158b669870a6180c5) | Chris Kearney | drm/asahi: select M3 runtime configuration per SoC (M3-only split) | Not applicable — G15; `drivers/gpu/drm/asahi/m3/m3_init_storage.rs` | V | +| [a8065eb937ce](https://github.com/aurora-silicon/linux/commit/a8065eb937cee67032a68207827419c515e52e11), [d16a8c32954b](https://github.com/aurora-silicon/linux/commit/d16a8c32954b412bb3bf9f61a7c48a68dc9303b1) | Chris Kearney | drm/asahi: m3: derive runtime IO maps from IO mappings (M3-only split) | Not applicable — G15; `drivers/gpu/drm/asahi/m3/m3_init_storage.rs` | V | +| [c3d978ed0793](https://github.com/aurora-silicon/linux/commit/c3d978ed0793058248d9994e868f5c1043cc18f7), [7f04f81b5a19](https://github.com/aurora-silicon/linux/commit/7f04f81b5a191b8ff9b1a3d454656af3993b47be) | Satya Benson | drm/asahi: m3: size render TPC and preemption storage by cluster count (M3-only split) | Not applicable — G15; `drivers/gpu/drm/asahi/m3/m3_pass_layout.rs` | V | +| [4287d2b5166b](https://github.com/aurora-silicon/linux/commit/4287d2b5166bb92f791ae652dba8361cfc732aeb), [d94dcc88b26c](https://github.com/aurora-silicon/linux/commit/d94dcc88b26ca0e964b5298ee1298ed068cbf6eb) | Satya Benson | drm/asahi: m3: publish queued packets from the completion worker (M3-only split) | Not applicable — G15; `drivers/gpu/drm/asahi/m3/m3_batch_policy.rs` | V | +| [6046b500e687](https://github.com/aurora-silicon/linux/commit/6046b500e6872cfe56d53cb65e9b0619cb1f066c), [6787ae6d5717](https://github.com/aurora-silicon/linux/commit/6787ae6d57170ef75c454f6f744d4c4d557cb9a7) | Satya Benson | drm/asahi: m3: bound overlapping render batches to free pass slots (M3-only split) | Not applicable — G15; `drivers/gpu/drm/asahi/m3/m3_batch_policy.rs` | V | +| [4ea0ea7df543](https://github.com/aurora-silicon/linux/commit/4ea0ea7df5437fad2829ec8eb68be6e9f3544591), [ce9df27fd4b6](https://github.com/aurora-silicon/linux/commit/ce9df27fd4b67bd47194655b57efecdec2321081) | Satya Benson | drm/asahi: m3: budget render coalescing by measured per-VM GPU time (M3-only split) | Not applicable — G15; `drivers/gpu/drm/asahi/m3/m3_batch_policy.rs` | V | +| [b877dfb7e3e8](https://github.com/aurora-silicon/linux/commit/b877dfb7e3e89e31fef97057c68581660b6f256e), [d57591e332b4](https://github.com/aurora-silicon/linux/commit/d57591e332b49cafc12aaf8af77845525dda760b) | Chris Kearney | drm/asahi: m3: compare waiting urgency with the active render batch (M3-only split) | Not applicable — G15; `drivers/gpu/drm/asahi/m3/m3_batch_policy.rs` | V | +| [f9ede2dda5dd](https://github.com/aurora-silicon/linux/commit/f9ede2dda5dd7a0fa8ce0e1143724e7be01b31a0), [7c912ffa2635](https://github.com/aurora-silicon/linux/commit/7c912ffa2635dd08673a43c09442a143a39b624d) | Chris Kearney | drm/asahi: retain full firmware-log payloads and use ring entry sizes (M3-only split) | Not applicable — G15; `drivers/gpu/drm/asahi/m3/m3_init_layout.rs` | V | +| [02e89516c254](https://github.com/aurora-silicon/linux/commit/02e89516c254d55b4ae686ccae460af8ddd30901), [e2bbe50fc3fd](https://github.com/aurora-silicon/linux/commit/e2bbe50fc3fd42e5dfbd74e5aff9181af86bb096) | Chris Kearney | drm/asahi: keep firmware-log payload clear of render counter pages (M3-only split) | Not applicable — G15; `drivers/gpu/drm/asahi/m3/m3_init_storage.rs` | V | +| [eb1dec967643](https://github.com/aurora-silicon/linux/commit/eb1dec967643c6b4fbbd528812befd07434951d8), [11896702843e](https://github.com/aurora-silicon/linux/commit/11896702843eb979e00beb871090800d9c53a4e0) | Chris Kearney | drm/asahi: coalesce eligible T6030 render completion stamp flushes (M3-only split) | Not applicable — G15; `drivers/gpu/drm/asahi/m3/m3_batch_policy.rs` | V | +| [407c4ba9d5c8](https://github.com/aurora-silicon/linux/commit/407c4ba9d5c8f14292a7ba75afdc929475a0e40c), [7feec149fdc7](https://github.com/aurora-silicon/linux/commit/7feec149fdc7795ff82698683c86dc4b3a2a486b) | Chris Kearney | drm/asahi: order opt-in T6030 mixed batches with firmware barriers (M3-only split) | Not applicable — G15; `drivers/gpu/drm/asahi/m3/m3_batch_policy.rs` | V | +| [d8c059ddae69](https://github.com/aurora-silicon/linux/commit/d8c059ddae697f618e0b70b4b2ddee103ba9a1eb), [0377e6c2e9ad](https://github.com/aurora-silicon/linux/commit/0377e6c2e9ad549130015a52de84c0c24e47a343) | Chris Kearney | drm/asahi: make T6030 internal render measurements optional (M3-only split) | Not applicable — G15; `drivers/gpu/drm/asahi/m3/m3_batch_policy.rs` | V | +| [ad54cf1de60e](https://github.com/aurora-silicon/linux/commit/ad54cf1de60ea638b0ab96e80d9bea7d7fc726ea), [cd13ef870e00](https://github.com/aurora-silicon/linux/commit/cd13ef870e00dd86aed1b582b393c6a697bec326) | Chris Kearney | drm/asahi: use stage-specific render sequence length fields (M3-only split) | Not applicable — G15; `drivers/gpu/drm/asahi/m3/m3_command_layout.rs` | V | +| [16fef343da41](https://github.com/aurora-silicon/linux/commit/16fef343da414828fe4098a566951945838bb370), [689764765782](https://github.com/aurora-silicon/linux/commit/68976476578273e77d0d5f05f77f437f610dbee8) | Chris Kearney | drm/asahi: add bounded T6030 active power-state sampling (M3-only split) | Not applicable — G15; `drivers/gpu/drm/asahi/m3/m3_active_power.rs` | V | +| [019434491afe](https://github.com/aurora-silicon/linux/commit/019434491afe2cd57bc001515778d9412c9b0747), [cd09499b2a78](https://github.com/aurora-silicon/linux/commit/cd09499b2a78ce5e0ca1b628af94403c56467256) | Chris Kearney | drm/asahi: add opt-in T6030 consistent performance requests (M3-only split) | Not applicable — G15; `drivers/gpu/drm/asahi/m3/m3_active_power.rs` | V | +| [b09e6d9fca33](https://github.com/aurora-silicon/linux/commit/b09e6d9fca33c554a597774f1a6cd4c4ca54622b), [263fd8538d83](https://github.com/aurora-silicon/linux/commit/263fd8538d83cca3809589da9b78cb1d982c0a21) | Chris Kearney | drm/asahi: refresh an opt-in T6030 busy performance request (M3-only split) | Not applicable — G15; `drivers/gpu/drm/asahi/m3/m3_consistent_perf.rs` | V | +| [9bc784357fe8](https://github.com/aurora-silicon/linux/commit/9bc784357fe8affe0875fb911be88ac953ca30db), [db732ce1f244](https://github.com/aurora-silicon/linux/commit/db732ce1f24488887872abc3b61b45de95285252) | Chris Kearney | drm/asahi: add an opt-in T6030 Globals debug-word clear (M3-only split) | Not applicable — G15; `drivers/gpu/drm/asahi/m3/m3_globals_debug.rs` | V | +| [02bde1b70c31](https://github.com/aurora-silicon/linux/commit/02bde1b70c312f6d4d964c7a2b1737be0de5a909), [651fbc49524d](https://github.com/aurora-silicon/linux/commit/651fbc49524d677ff86766dd2d85bb1bbf6ab3eb) | Chris Kearney | drm/asahi: m3: groundwork for more than two clusters and T6031 tables (M3-only split) | Not applicable — G15; `drivers/gpu/drm/asahi/m3/m3_pass_layout.rs` | V | +| [0f47fbf31934](https://github.com/aurora-silicon/linux/commit/0f47fbf31934d0b1684b5412e235315304687d4a), [783f9478d373](https://github.com/aurora-silicon/linux/commit/783f9478d3737cfae945aad01e02e9dea30c2249) | Chris Kearney | drm/asahi: reject unsupported command fields on the M1/M2 queue path | Not applicable — LEGACY; `drivers/gpu/drm/asahi/queue/mod.rs` | V | +| [1de2dbd89acb](https://github.com/aurora-silicon/linux/commit/1de2dbd89acb80f37aec08fa897374dfebe56b10), [294e9f9ebfb4](https://github.com/aurora-silicon/linux/commit/294e9f9ebfb42dbc77a85f635b5bfabaa9315003) | Chris Kearney | drm/asahi: excise unbound G16 and G17 runtimes (M3-only split: TA/3D command encoders) | Not applicable — G15; `drivers/gpu/drm/asahi/m3/agx_render.rs` | V | +| [fd1a2a3d4e60](https://github.com/aurora-silicon/linux/commit/fd1a2a3d4e60ce3f3965d2a47d1904ce3a6ed158), [e91bc024eef8](https://github.com/aurora-silicon/linux/commit/e91bc024eef8a6fde320f7fa062a034a2fe0bccb) | Eryk Wieliczko | drm/asahi: carry validated M3 depth compression bindings | Not applicable — G15; `drivers/gpu/drm/asahi/m3/m3_fragment_command.rs` | V | +| [bc0a841680f3](https://github.com/aurora-silicon/linux/commit/bc0a841680f3b36193e83e18c339d269d142de03), [2c301fab2412](https://github.com/aurora-silicon/linux/commit/2c301fab241280c36f20c57d7d240ff0f48ad88f) | garymeyerca | drm/asahi: gate T8122 G15G identity before startup | Not applicable — G15; `drivers/gpu/drm/asahi/m3/mod.rs` | V | +| [e10c013299bf](https://github.com/aurora-silicon/linux/commit/e10c013299bf639ca0c8f08574150970e381f203), [be90c110d9df](https://github.com/aurora-silicon/linux/commit/be90c110d9dfcae3918efad4ba9a3d5015bb5bf4) | Chris Kearney | drm/asahi: select M3 runtime configuration per SoC | Not applicable — G15; `drivers/gpu/drm/asahi/m3/m3_soc.rs` | V | +| [ed90e7bfde51](https://github.com/aurora-silicon/linux/commit/ed90e7bfde5160c20b1e4165f577e874e60fc102), [a6b472702f24](https://github.com/aurora-silicon/linux/commit/a6b472702f24666ee52febc1c2eb93bf21c8d44a) | Satya Benson | drm/asahi: m3: T8122: the G15G compute register list | Not applicable — G15; `drivers/gpu/drm/asahi/m3/m3_compute_layout.rs` | V | +| [278d0abfc612](https://github.com/aurora-silicon/linux/commit/278d0abfc6128a83aa00d9181bdf2c716b608c89), [7949c0d6139c](https://github.com/aurora-silicon/linux/commit/7949c0d6139c53f8e016cd773744386407a8c9cf) | Satya Benson | drm/asahi: m3: T8122: the G15G TA and 3D register lists | Not applicable — G15; `drivers/gpu/drm/asahi/m3/m3_fragment_command.rs` | V | +| [7231c0beb83e](https://github.com/aurora-silicon/linux/commit/7231c0beb83ec1976716d28456e09df3e159f2e5), [bfd3e0bf0bbc](https://github.com/aurora-silicon/linux/commit/bfd3e0bf0bbc64ee99213b4b5b3824b588411bba) | Satya Benson | drm/asahi: m3: T8122: the runtime HwDataB words | Not applicable — G15; `drivers/gpu/drm/asahi/m3/m3_soc.rs` | V | +| [6ad35705d81d](https://github.com/aurora-silicon/linux/commit/6ad35705d81dd1e65f8ef9e1f4136a0080ead760), [f6e58511c69a](https://github.com/aurora-silicon/linux/commit/f6e58511c69ae5cdef73b3ba0d87de8de9a74e01) | Chris Kearney | drm/asahi: m3: T8122: the remaining runtime InitData words | Not applicable — G15; `drivers/gpu/drm/asahi/m3/m3_soc.rs` | V | +| [2a8cdd0ce1d4](https://github.com/aurora-silicon/linux/commit/2a8cdd0ce1d4a8ceef673922b9517e4780996a0c), [d109a5c9e597](https://github.com/aurora-silicon/linux/commit/d109a5c9e59768669f2d1750ab1abd4560a42e28) | Chris Kearney | drm/asahi: coalesce eligible T6030 render completion stamp flushes | Not applicable — G15; `drivers/gpu/drm/asahi/m3/m3_fragment_command.rs` | V | +| [bb1f75a18b32](https://github.com/aurora-silicon/linux/commit/bb1f75a18b3265ab5e4a06e424df43ed0371c002), [d4dba3df31cf](https://github.com/aurora-silicon/linux/commit/d4dba3df31cff8f96ddeafad6a4b2709fe942d62) | Chris Kearney | drm/asahi: use stage-specific render sequence length fields | Not applicable — G15; `drivers/gpu/drm/asahi/m3/m3_fragment_command.rs` | V | +| [1e8f813be7c6](https://github.com/aurora-silicon/linux/commit/1e8f813be7c681b558ffcd501ea278d6848099f4), [3d7945cbbd0f](https://github.com/aurora-silicon/linux/commit/3d7945cbbd0f9dd9d7806c402e70a53815c30738) | Satya Benson | drm/asahi: m3: T8122: write the HwDataA +0x418c block on 14.8.3 | Not applicable — G15; `drivers/gpu/drm/asahi/m3/mod.rs` | V | +| [8730d3dce2b7](https://github.com/aurora-silicon/linux/commit/8730d3dce2b7e8d9f05ad437c4cf89169e0c5b78), [453d66a437b0](https://github.com/aurora-silicon/linux/commit/453d66a437b02027ad0b6fb194242dd29bb0d31d) | Chris Kearney | pmdomain: apple: pmp-report: add the T6031 report | Not applicable — G15; `drivers/pmdomain/apple/pmp-report-test.c` | V | +| [a66386d03830](https://github.com/aurora-silicon/linux/commit/a66386d03830555c976850001c12644921b0a028), [a8e5d3a57a0e](https://github.com/aurora-silicon/linux/commit/a8e5d3a57a0e19360cffa4dc04ae500ba832e0ea) | Marius Gehrmann | pmdomain: apple: pmp-report: Request T8122 devices by soc-device record | Not applicable — G15; `drivers/pmdomain/apple/pmp-report-test.c` | V | +| [3b9bb4563bb8](https://github.com/aurora-silicon/linux/commit/3b9bb4563bb8b9f16761fe5842ff3bb6a09d8661), [c7bbd7faf778](https://github.com/aurora-silicon/linux/commit/c7bbd7faf778be8ab87515e81caf06571e527ad8) | Chris Kearney | pmdomain: apple: pmp-report: start a handed-over T8122 or T6031 PMP | Not applicable — G15; `drivers/pmdomain/apple/pmp-report.c` | V | +| [3157c614a661](https://github.com/aurora-silicon/linux/commit/3157c614a66119b47100a6a8ac1ad9232bacbf81), [2b115cc04222](https://github.com/aurora-silicon/linux/commit/2b115cc04222553f4bcde86a70f5a2f9c8f5d086) | Ryan Murray | pmdomain: apple: pmp-report: document a failed PMP start and its recovery | Not applicable — VERIFY; `drivers/pmdomain/apple/Kconfig` | V | +| [c332c6fbd56a](https://github.com/aurora-silicon/linux/commit/c332c6fbd56adddb1f6d5635b4010a940c780e7e), [54b90aaf82e9](https://github.com/aurora-silicon/linux/commit/54b90aaf82e9aa7241747d747a5e79c46841a2dd) | Ryan Murray | pmdomain: apple: pmp-report: add KUnit fault injection for the PMP start | Not applicable — VERIFY; `drivers/pmdomain/apple/.kunitconfig.apple-pmp-start` | V | +| [7d101f700326](https://github.com/aurora-silicon/linux/commit/7d101f7003268e2e459ed86b07c9bc3ec5a000f2), [ac234af7c851](https://github.com/aurora-silicon/linux/commit/ac234af7c851e08e6da06a4dffeb912786611ed8) | Chris Kearney | drm/asahi: uapi: add the M3 (G15S) runtime numbers | Not applicable — G15 | V | +| [70f4bebd69b0](https://github.com/aurora-silicon/linux/commit/70f4bebd69b070d37022bca55228879bb7ec4e32), [577249de3221](https://github.com/aurora-silicon/linux/commit/577249de322139ceb84aea9daffcf54d437da52b) | Eryk Wieliczko | drm/asahi: uapi: add the J613 25G83 runtime numbers | Not applicable — G15 | V | +| [f3de3ac043ac](https://github.com/aurora-silicon/linux/commit/f3de3ac043acd22bf4ded712f2999b5693d824ae), [744d0129b6c0](https://github.com/aurora-silicon/linux/commit/744d0129b6c008708c2d9d019ca0a95ee1c04874) | Ryan Murray | drm/asahi: reject timestamp copies on M1 and M2 | Not applicable — LEGACY; `drivers/gpu/drm/asahi/queue/mod.rs` | V | +| [d2cdf2ff6b35](https://github.com/aurora-silicon/linux/commit/d2cdf2ff6b353a72f5f33239a4a7f2b7d87e8cf4), [1e52a05edada](https://github.com/aurora-silicon/linux/commit/1e52a05edadad288cfc17120f37c721f506abcd5) | Chris Kearney | gpu: preserve compute overrides and limit automatic preemption to M2 Max | Not applicable — LEGACY; `drivers/gpu/drm/asahi/asahi_core.rs` | V | +| [a22a6bfc2fa0](https://github.com/aurora-silicon/linux/commit/a22a6bfc2fa049f249effd677a512d682870b171) | Ryan Murray | rust: drm: document Aurora ownership and simplify pointer conversions | Not applicable — VERIFY; `rust/kernel/drm/gpuvm/mod.rs` | V | +| [e8f94ff7af1c](https://github.com/aurora-silicon/linux/commit/e8f94ff7af1cd4104091ac851109fdae946aa585) | Ryan Murray | arm64: dts: apple: j700: Use the raw xHCI SuperSpeed port number | Not applicable — PERIPH | V | +| [58d75c388d55](https://github.com/aurora-silicon/linux/commit/58d75c388d55030f432e93f42ff8871ffe9390f3) | Chris Kearney | soc: apple: opt native T6030 GPUs into the admitted PMP supplier [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/pmdomain/apple/pmp-report.c` | V | +| [19538d922267](https://github.com/aurora-silicon/linux/commit/19538d9222679bc5ff3f17da845a10f60591e82c) | Chris Kearney | pmdomain: apple: read readiness from the passive T6030 report [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/pmdomain/apple/pmp-report.c` | V | +| [1633fb96515e](https://github.com/aurora-silicon/linux/commit/1633fb96515e3b733a0e3514e17f841ef344ab4c) | Eryk Wieliczko | drm/asahi: g15: Add the UAPI glue and register the G15 generation [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/g15/adt.rs` | V | +| [4d6d40767ae2](https://github.com/aurora-silicon/linux/commit/4d6d40767ae2a1cdbb697d3471dbb2c4a7ef9f9d) | Naeem Malik | drm/asahi: g15: Add the T6031 (M3 Max, G15C) configuration row [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/g15/adt.rs` | V | +| [99a4031d5828](https://github.com/aurora-silicon/linux/commit/99a4031d58282e9d2d0f9211b3ba50547d4fc643) | Naeem Malik | drm/asahi: g15: Pin the J516C GPU firmware build [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/g15/hw/t6031.rs` | V | +| [4abf7048277d](https://github.com/aurora-silicon/linux/commit/4abf7048277dd6ec8364811002adeafe326046d6) | Naeem Malik | drm/asahi: g15: Build the J516C initdata from its board images [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/g15/board.rs` | V | +| [5fc412af6085](https://github.com/aurora-silicon/linux/commit/5fc412af608586b6beb6a567544f3440f42fe7eb) | Naeem Malik | drm/asahi: g15: Encode compute commands with the G15C register values [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/g15/hw/t6031.rs` | V | +| [377ce9d4f32a](https://github.com/aurora-silicon/linux/commit/377ce9d4f32a42a3b87e0885f48777140c4a2488) | Naeem Malik | drm/asahi: g15: Admit and map UAT regions that are reserved System RAM [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/g15/adt.rs` | V | +| [3a5c855a7219](https://github.com/aurora-silicon/linux/commit/3a5c855a7219aaf45d18a3449bfd4a263ed781c2) | Naeem Malik | drm/asahi: g15: Accept the J516C 14.8 GPU firmware build [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/g15/hw/t6031.rs` | V | +| [f0d7c15b4769](https://github.com/aurora-silicon/linux/commit/f0d7c15b47696e3bbf168f1c8bce9c4817ea28da) | DjDeveloperr | drm/asahi: g15: Reject a scissor base that overflows the address space [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/g15/command.rs` | V | +| [2510cc337f15](https://github.com/aurora-silicon/linux/commit/2510cc337f1551b53e9de38b5c69d6d365e373f3) | DjDeveloperr | drm/asahi: g15: Do not fail a published prefix on a group allocation failure [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/g15/engine.rs` | V | +| [5a592f22b23a](https://github.com/aurora-silicon/linux/commit/5a592f22b23a83ec106b50d5f9ec88f9d885e980) | DjDeveloperr | drm/asahi: g15: Release a closed VM's state when its handle goes [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/g15/mod.rs` | V | +| [e2b962c42e40](https://github.com/aurora-silicon/linux/commit/e2b962c42e40c5ee92b81495436c11b5832d056c) | DjDeveloperr | drm/asahi: g15: Release the power hold and the PMP vote when a start fails early [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/g15/boot.rs` | V | +| [c4d01d363c1d](https://github.com/aurora-silicon/linux/commit/c4d01d363c1d25c33fee168114f369288e24e55d) | DjDeveloperr | drm/asahi: g15: Only mark used pass slots dirty on release [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/g15/slots.rs` | V | +| [35bbcb1bca7a](https://github.com/aurora-silicon/linux/commit/35bbcb1bca7a9dac71f9785aed900d4cf6665d9f) | DjDeveloperr | tools/asahi: Drop the removed T6031 knobs from the M3 runtime tests [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/g15/glue.c` | V | +| [f6da2c8fd38a](https://github.com/aurora-silicon/linux/commit/f6da2c8fd38a75723a7569ff6046d08572900559) | Chris Kearney | drm/asahi-m3: adapt the native G15 driver to the 7.2 driver core and DRM device API [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/g15/adt.rs` | V | +| [42f91c2f9eae](https://github.com/aurora-silicon/linux/commit/42f91c2f9eae615bc12335e37f2016ec09398f63) | Naeem Malik | drm/asahi: g15: Say what to report when the GPU firmware build is unknown [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/g15/identity.rs` | V | +| [f3b867b04c1a](https://github.com/aurora-silicon/linux/commit/f3b867b04c1a50f46d034844754e39f8031ffae3) | Naeem Malik | drm/asahi: g15: Move the board template fill into a core-only submodule [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/g15/board.rs` | V | +| [7375309d764f](https://github.com/aurora-silicon/linux/commit/7375309d764f899d8eb4328e30447ec2959efa02) | Naeem Malik | drm/asahi: g15: t6031: Build Globals and Power from the device tree [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/g15/board.rs` | V | +| [b840b9944ad9](https://github.com/aurora-silicon/linux/commit/b840b9944ad9e171cf5b13b4e0e0fe8d018778c9) | Naeem Malik | drm/asahi: g15: board: Review fixes for the device-tree Globals and Power [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/g15/board/words.rs` | V | +| [3f02f6c358ab](https://github.com/aurora-silicon/linux/commit/3f02f6c358abd1630f1342cbd4dbca755c78023c) | Naeem Malik | drm/asahi: g15: t6031: Build the hardware record from the device tree [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/g15/adt.rs` | V | +| [f1f5a81b756f](https://github.com/aurora-silicon/linux/commit/f1f5a81b756f99b30baefcd967a924167b2a0c12) | Naeem Malik | drm/asahi: t6031: take the idle standby time from the GPU node [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/g15/hw.rs` | V | +| [de646267a725](https://github.com/aurora-silicon/linux/commit/de646267a725f94ea67864c2695eb1082f60dc33) | Chris Kearney | drm/asahi: g15: t6031: refuse board-data values that do not fit in u32 [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/g15/adt.rs` | V | +| [a9a0e4be0546](https://github.com/aurora-silicon/linux/commit/a9a0e4be0546ee7876924cee8e3478727f89cb41) | Chris Kearney | selftests: asahi-g15: run the T6031 board-data and firmware-lookup tests [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/g15/board/record.rs` | V | +| [0b06992d812c](https://github.com/aurora-silicon/linux/commit/0b06992d812ce75667740514a5a472fc8d0e091b) | Chris Kearney | drm/asahi: g15: t6031: test that no GPU node value panics the record builder [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/g15/board/record.rs` | V | +| [68b780fc7ced](https://github.com/aurora-silicon/linux/commit/68b780fc7ced9e39a5d4573993564ebda26f3649) | Naeem Malik | drm/asahi: g15: Read back the GPU's performance state in debugfs [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/g15/board.rs` | V | +| [cd970562fa97](https://github.com/aurora-silicon/linux/commit/cd970562fa979059c6dea0e7f0a7f9df63c3b1c7) | Naeem Malik | drm/asahi: g15: t6031: Leave the 42 record words the firmware does not need zero [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/g15/board/record.rs` | V | +| [2a0319927108](https://github.com/aurora-silicon/linux/commit/2a0319927108ee823a82bb1884d2cc6512b515bb) | Chris Kearney | drm/asahi: g15: m3: complete the stats record loads before consuming them [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/g15/channel.rs` | V | +| [73047c7ee59e](https://github.com/aurora-silicon/linux/commit/73047c7ee59e45b8d16e8ec308fb15683caefc6e) | Naeem Malik | drm/asahi: g15: Seed the compute UMA freelist with every pool [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/g15/fw/graph.rs` | V | +| [345f1d8467a4](https://github.com/aurora-silicon/linux/commit/345f1d8467a46f679eb2f2fc2852adb844911033) | Naeem Malik | drm/asahi: g15: Leave PB growth requests unanswered after two refusals at the maximum [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/g15/params.rs` | V | +| [bd0201fad2e0](https://github.com/aurora-silicon/linux/commit/bd0201fad2e02429de6e265c51a223e4209920fe) | Naeem Malik | drm/asahi: g15: Keep PB blocks above the firmware's growth maximum for partial renders [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/g15/params.rs` | V | +| [ff0d1cd692b8](https://github.com/aurora-silicon/linux/commit/ff0d1cd692b838934972a646fa7d1411c54b4864) | Naeem Malik | drm/asahi: g15: Map a 512 MiB parameter buffer heap [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/g15/fw/buffer.rs` | V | +| [e1cdc12eeb97](https://github.com/aurora-silicon/linux/commit/e1cdc12eeb97fc9f4da88343054e6a964e8d2e40) | Chris Kearney | drm/asahi: g15: m3: keep every PB ledger below the block list's K [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/g15/pools.rs` | V | +| [b1adafb49e1c](https://github.com/aurora-silicon/linux/commit/b1adafb49e1c49b71221f7c069dde6045ccce32c) | Chris Kearney | drm/asahi: g15: m3: say what a PB refusal does and size the heap right [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/g15/fw/buffer.rs` | V | +| [3ea5cb6af82b](https://github.com/aurora-silicon/linux/commit/3ea5cb6af82bda7fca665b0902b22275ae02d996) | Chris Kearney | drm/asahi: g15: m3: check firmware-object backing geometry with checked arithmetic [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/g15/object.rs` | V | +| [1eced02cd6c3](https://github.com/aurora-silicon/linux/commit/1eced02cd6c3efe9c43cfc9cf0993ee427e849b9) | Chris Kearney | drm/asahi: g15: m3: map the PB backing into each VM when it first runs [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/g15/engine.rs` | V | +| [73b9ade12e01](https://github.com/aurora-silicon/linux/commit/73b9ade12e0123ef2168abd7eb4969d6865622a3) | Chris Kearney | drm/asahi: g15: m3: back the PB on demand, before an answer posts it [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/g15/pools.rs` | V | +| [716d2fbe9254](https://github.com/aurora-silicon/linux/commit/716d2fbe9254f5ba13a8bd6a6ac687f5904a2f8a) | Chris Kearney | drm/asahi: adapt retained-G15 hooks to the maintained shared APIs [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/file.rs` | V | +| [a4000925920a](https://github.com/aurora-silicon/linux/commit/a4000925920a0e338e0bfa2b726f3b4de90b8cd7) | Chris Kearney | drm/asahi: port the native G15 adapter onto maintained DRM registration [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/Kconfig` | V | +| [23ae3eaf8377](https://github.com/aurora-silicon/linux/commit/23ae3eaf8377dbbd5c51541d77fb4a542fdb384b) | Chris Kearney | drm/asahi: g15: allocate driver data before touching firmware [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/g15/mod.rs` | V | +| [a8289266db94](https://github.com/aurora-silicon/linux/commit/a8289266db94cf7069ab26420bd9d6b60a92dce6) | Chris Kearney | drm/asahi: g15: defer firmware start until module registration completes [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/asahi_core.rs` | V | +| [bc962412f16e](https://github.com/aurora-silicon/linux/commit/bc962412f16e1ca6d93ababf971c68011294c5e3) | Chris Kearney | drm/asahi: g15: make maintained loader contracts independent of SF profiles [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/g15/adt.rs` | V | +| [6c6f0e62156c](https://github.com/aurora-silicon/linux/commit/6c6f0e62156cd9e35f5db47af226dd9821a26b0f) | Chris Kearney | drm/asahi: use the native private wire in G15 command consumers [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/g15/boot.rs` | V | +| [627c501db65e](https://github.com/aurora-silicon/linux/commit/627c501db65ed9bbe22e853f37f247dc579db4af) | Chris Kearney | drm/asahi: isolate the native G15 command ABI behind a private facade [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/g15/wire.rs` | V | +| [9d5c0ee71512](https://github.com/aurora-silicon/linux/commit/9d5c0ee715129fda8cbb8a1420315cf6a7d25cc4) | Chris Kearney | drm/asahi: g15: bound device-wide live logical queue capacity [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/g15/mod.rs` | V | +| [4f6aba6a3598](https://github.com/aurora-silicon/linux/commit/4f6aba6a35980bf152ce5803d7bb5940a2cb11a2) | Chris Kearney | pmdomain: apple: pmgr-pwrstate: Test apple,no-auto-pm on power on and off [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — VERIFY; `drivers/pmdomain/apple/Kconfig` | V | +| [b9d3cf70f1e8](https://github.com/aurora-silicon/linux/commit/b9d3cf70f1e8b488a9ad032dadad7cbe432e164c) | Chris Kearney | drm/asahi: export a T6030 module alias for the native G15 driver [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/g15/mod.rs` | V | +| [af2c5c09ba9f](https://github.com/aurora-silicon/linux/commit/af2c5c09ba9fae7fbf9f165fc6aad73e9aadce0b) | Chris K | pmdomain: apple: restore gated T6030 managed PMP startup [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/pmdomain/apple/pmp-report.c` | V | +| [2a182741b7f7](https://github.com/aurora-silicon/linux/commit/2a182741b7f7d80a7a743eafbf57bd9dbca3c6b4) | Chris Kearney | test: exercise the actual T6030 halted-PMP consumer graph [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/pmdomain/apple/Kconfig` | V | +| [70f41a7944ee](https://github.com/aurora-silicon/linux/commit/70f41a7944eeaf9afdd63f9246ebc5bc41df23f1) | Chris Kearney | drm/apple: admit T6030 G15 only after native display registration [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/apple/apple_drv.c` | V | +| [9632ccd283f9](https://github.com/aurora-silicon/linux/commit/9632ccd283f96ad98321238b6da4718b62f7671a) | Chris Kearney | drm/asahi: require a callable native display provider for G15 [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/Kconfig` | V | +| [e9d92ce6a5c2](https://github.com/aurora-silicon/linux/commit/e9d92ce6a5c27b36266de99d252db029476cb3f1) | Chris Kearney | drm/asahi: retain live G15 storage after allocation failures [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/g15/boot.rs` | V | +| [fc13d05b1db9](https://github.com/aurora-silicon/linux/commit/fc13d05b1db9515fa9481d1362ff530fbeb01ca4) | DjDeveloperr | drm/asahi: g15: adapt the native G15 backend to the Linux 7.3 Rust APIs [#395](https://github.com/aurora-silicon/linux/pull/395) | Not applicable — G15; `drivers/gpu/drm/asahi/g15/boot.rs` | V | +| [0ffd22188c3d](https://github.com/aurora-silicon/linux/commit/0ffd22188c3dbb2e8bb2df216ee8e03d71e14bfb) | Ryan Murray | drm/asahi: Keep refusing HIGH queue priority on G13 and G14 [#399](https://github.com/aurora-silicon/linux/pull/399) | Not applicable — LEGACY; `drivers/gpu/drm/asahi/file.rs` | V | +| [1fac0605f061](https://github.com/aurora-silicon/linux/commit/1fac0605f061ce757917781940a5e0aec1225019) | Eryk Wieliczko | soc: apple: rtkit-helper: preserve the inherited J713 input session | Not applicable — G16; `drivers/soc/apple/rtkit-helper.c` | V | +| [751b9e08ca3d](https://github.com/aurora-silicon/linux/commit/751b9e08ca3d8fa673927984d45b22697cc26edf) | Eryk Wieliczko | iommu: apple-dart: retain J713 input and radio ownership [#400](https://github.com/aurora-silicon/linux/pull/400) | Not applicable — G16; `drivers/iommu/apple-dart.c` | V | +| [939a993d46dd](https://github.com/aurora-silicon/linux/commit/939a993d46dd36392155cc05f692472d2d1c881e) | Eryk Wieliczko | pmdomain: apple: pmc: support T8132 floor requests [#400](https://github.com/aurora-silicon/linux/pull/400) | Not applicable — G16; `drivers/pmdomain/apple/pmc-test.c` | V | +| [c26dfe75ac9b](https://github.com/aurora-silicon/linux/commit/c26dfe75ac9be0fb75dfe937a15246b4e12bc84a) | Eryk Wieliczko | soc: apple: clpc: retain qualified T8132 memory floors | Not applicable — G16; `drivers/soc/apple/clpc.c` | V | +| [15e0eb00b83b](https://github.com/aurora-silicon/linux/commit/15e0eb00b83b783d7fca9720530942020722cd6a) | Eryk Wieliczko | soc: apple: mcc: support the T8132 memory controller | Not applicable — G16; `drivers/soc/apple/mcc.c` | V | +| [df3e4970e686](https://github.com/aurora-silicon/linux/commit/df3e4970e68677d56eca3e9e9608c338abe52cdd) | Eryk Wieliczko | soc: apple: pmp: publish owner-managed T8132 telemetry [#400](https://github.com/aurora-silicon/linux/pull/400) | Not applicable — G16; `drivers/pmdomain/apple/pmp-report.c` | V | +| [f1b75932e61d](https://github.com/aurora-silicon/linux/commit/f1b75932e61d7d193801a7f22ea628cb94d9dd56) | Eryk Wieliczko | cpufreq: apple: track T8132 thermal ownership per policy | Not applicable — G16; `drivers/cpufreq/apple-soc-cpufreq.c` | V | +| [1e23ae3aee82](https://github.com/aurora-silicon/linux/commit/1e23ae3aee824c65cbb6d3c94780e44a2e3fc6c2) | Eryk Wieliczko | thermal: apple: add the J713 PMP CPU policy [#400](https://github.com/aurora-silicon/linux/pull/400) | Not applicable — G16; `drivers/thermal/Kconfig` | V | +| [332eea1e9005](https://github.com/aurora-silicon/linux/commit/332eea1e9005b0cf731a16695c9c87c34e8f13aa) | Eryk Wieliczko | arm64: apple: activate J713 with its PMGR ownership policy [#400](https://github.com/aurora-silicon/linux/pull/400) | Not applicable — G16; `drivers/pmdomain/apple/pmgr-pwrstate.c` | V | +| [3949e3640d94](https://github.com/aurora-silicon/linux/commit/3949e3640d94d2b5434c84e9e1f6d7b81f650b70) | Eryk Wieliczko | arm64: apple: enable qualified J713 SuperSpeed host support [#400](https://github.com/aurora-silicon/linux/pull/400) | Not applicable — G16; `drivers/phy/apple/atc.c` | V | +| [0877cd766126](https://github.com/aurora-silicon/linux/commit/0877cd766126503ad02cc11565b85be37517a62c) | Eryk Wieliczko | arm64: apple: enable J713 Bluetooth with scoped radio DMA [#400](https://github.com/aurora-silicon/linux/pull/400) | Not applicable — G16; `drivers/bluetooth/hci_bcm4377.c` | V | +| [1ae4d49f3592](https://github.com/aurora-silicon/linux/commit/1ae4d49f35921f356d831da974c43dd5fb67f5d7) | Eryk Wieliczko | soc: apple: expose retained PMP display and GPU ownership [#400](https://github.com/aurora-silicon/linux/pull/400) | Not applicable — G16; `drivers/pmdomain/apple/pmp-report.c` | V | +| [a5f2ee806da2](https://github.com/aurora-silicon/linux/commit/a5f2ee806da2931b10d69044e4a40609d4924201) | Eryk Wieliczko | thermal: apple: integrate J713 measured-rail GPU budgets [#400](https://github.com/aurora-silicon/linux/pull/400) | Not applicable — G16; `drivers/thermal/Kconfig` | V | +| [52d846f6b8d3](https://github.com/aurora-silicon/linux/commit/52d846f6b8d3664ea6135a660c0b3ece39937209) | Eryk Wieliczko | iommu/apple-dart: adopt J713 display streams with retained ownership [#400](https://github.com/aurora-silicon/linux/pull/400) | Not applicable — G16; `drivers/iommu/Kconfig` | V | +| [24f67337c9d5](https://github.com/aurora-silicon/linux/commit/24f67337c9d586100ca4a4f438e83310644c1903) | Eryk Wieliczko | drm/asahi: add the T8132 G16 firmware backend [#400](https://github.com/aurora-silicon/linux/pull/400) | Not applicable — G16; `drivers/gpu/drm/asahi/Kconfig` | V | +| [fb96882f6cb4](https://github.com/aurora-silicon/linux/commit/fb96882f6cb4ce464fcf3f8b3f27f0c3eba891ed) | Ryan Murray | soc: apple: rtkit-helper: Keep serving buffer requests on T8140 adopted sessions | Not applicable — PERIPH; `drivers/soc/apple/rtkit-helper.c` | V | +| [4d88194af46e](https://github.com/aurora-silicon/linux/commit/4d88194af46e4eec12e57c3e7ff8bbcf7ac5b856) | Ryan Murray | pmdomain: apple: pmgr-pwrstate: Honour apple,inherited-on only on T8132 [#400](https://github.com/aurora-silicon/linux/pull/400) | Not applicable — G16; `drivers/pmdomain/apple/pmgr-pwrstate.c` | V | +| [d14b566cc309](https://github.com/aurora-silicon/linux/commit/d14b566cc309cf6840199c6394dc8f5f75f82f54) | Ryan Murray | drm/asahi: g17: describe the 25G83 barrier and parameter block plainly [#420](https://github.com/aurora-silicon/linux/pull/420) | Not applicable — VERIFY; `drivers/gpu/drm/asahi/g17/buffer/target.rs` | V | +| [f0f00dca4b09](https://github.com/aurora-silicon/linux/commit/f0f00dca4b09c9caf6f379928827e0d10451c66b) | Ryan Murray | drm/asahi: mem: document that page-by-page invalidation is global [#420](https://github.com/aurora-silicon/linux/pull/420) | Not applicable — VERIFY; `drivers/gpu/drm/asahi/mem.rs` | V | +| [8eacef90ff89](https://github.com/aurora-silicon/linux/commit/8eacef90ff8952b6fc75a85cb20abce488bcd4ed) | Erik Bourget | drm/asahi: g16: accept the 8-core J713 loaded 25G83 image [#442](https://github.com/aurora-silicon/linux/pull/442) | Not applicable — G16; `drivers/gpu/drm/asahi/g16/hw.rs` | V | +| [da42c7cf4fc8](https://github.com/aurora-silicon/linux/commit/da42c7cf4fc83e68261f1c238b2a3b870f94a128) | Ryan Murray | drm/asahi: test shared firmware identity normalization [#428](https://github.com/aurora-silicon/linux/pull/428) | Not applicable — VERIFY; `drivers/gpu/drm/asahi/fwid.rs` | V | +| [b11a5e9e8cd8](https://github.com/aurora-silicon/linux/commit/b11a5e9e8cd83d4ac3ac40bc620de254b095c75d) | DjDeveloperr | cpufreq: apple: verify T8140 transitions on the next request [#443](https://github.com/aurora-silicon/linux/pull/443) | Not applicable — LAZY; `drivers/cpufreq/apple-soc-cpufreq.c` | C,I | +| [e2a2d8ecbb24](https://github.com/aurora-silicon/linux/commit/e2a2d8ecbb24146ff61e1649ff503f234e846b30) | DjDeveloperr | cpufreq: apple: verify T8140 fast-switch requests from a timer [#443](https://github.com/aurora-silicon/linux/pull/443) | Not applicable — LAZY; `drivers/cpufreq/apple-soc-cpufreq.c` | C,I | +| [b7ea3ca9de07](https://github.com/aurora-silicon/linux/commit/b7ea3ca9de073d66f0e031013b7853c3e5702479) | DjDeveloperr | cpufreq: apple: settle T8140 lazy requests before suspend [#443](https://github.com/aurora-silicon/linux/pull/443) | Not applicable — LAZY; `drivers/cpufreq/apple-soc-cpufreq.c` | C,I | +| [6f5d208b4ce0](https://github.com/aurora-silicon/linux/commit/6f5d208b4ce01ff8cfea04fa21f14ac9a7cb610c) | DjDeveloperr | cpufreq: apple: apply a lowered T8140 fast die limit at once [#443](https://github.com/aurora-silicon/linux/pull/443) | Not applicable — LAZY; `drivers/cpufreq/apple-soc-cpufreq.c` | P | +| [b884471f5b13](https://github.com/aurora-silicon/linux/commit/b884471f5b13e62de9105bb0f06362fe01663a75) | Ryan Murray | thermal: apple-smc-cpu-thermal: walk the OPP table by die temperature | Not applicable — OLDTHERMAL; `drivers/thermal/apple-smc-cpu-thermal.c` | P | +| [ca1dab3dde89](https://github.com/aurora-silicon/linux/commit/ca1dab3dde89c57cc0b111fd096478241e6565e5) | Ryan Murray | cpufreq: apple-soc: Drop the SMC CPU thermal policy hooks | Not applicable — OLDTHERMAL; `drivers/cpufreq/apple-soc-cpufreq.c` | P | +| [52006c8e0bc5](https://github.com/aurora-silicon/linux/commit/52006c8e0bc5750331643c2f36ebad06ef300079) | Ryan Murray | thermal: Remove the Apple SMC CPU thermal policy [#43](https://github.com/aurora-silicon/linux/pull/43) [#380](https://github.com/aurora-silicon/linux/pull/380) [#470](https://github.com/aurora-silicon/linux/pull/470) | Not applicable — OLDTHERMAL; `drivers/thermal/Kconfig` | P | +| [1ce75ee9e797](https://github.com/aurora-silicon/linux/commit/1ce75ee9e797c296f742abf955764fa00b77e03d) | Ryan Murray | treewide: apple: t8140: Describe observed behaviour, not other implementations | Not applicable — VERIFY; `drivers/media/platform/apple/isp/isp-cam.c` | V | +| [3b0af1da96c2](https://github.com/aurora-silicon/linux/commit/3b0af1da96c28777073ebf99d65f1498a5f9fe94) | Ace | PCI: apple: enable bounded J700 endpoint bootstrap and enumeration | Not applicable — PERIPH; `drivers/pci/controller/Kconfig` | V | +| [e179042ff067](https://github.com/aurora-silicon/linux/commit/e179042ff0673c4b5adc749cb0fc3fcb0f909106) | Acelogic | HID: dockchannel: support firmware-managed M5 trackpad power | Not applicable — OTHERCHIP; `drivers/hid/dockchannel-hid/dockchannel-hid.c` | V | +| [f19bcf1a1e37](https://github.com/aurora-silicon/linux/commit/f19bcf1a1e373fb5bae2c9a5a0153d7820b13645) | Joshua Warren | drm/asahi: stats: Add StatsSnapshot and firmware-stats decoder [#197](https://github.com/aurora-silicon/linux/pull/197) [#380](https://github.com/aurora-silicon/linux/pull/380) [#470](https://github.com/aurora-silicon/linux/pull/470) | Not applicable — STAT; `drivers/gpu/drm/asahi/stats.rs` | T | +| [72d21d654534](https://github.com/aurora-silicon/linux/commit/72d21d654534decaecc58af0f8fc813064e6f500) | Joshua Warren | drm/asahi: Wire StatsSnapshot into StatsChannel and GpuManager [#197](https://github.com/aurora-silicon/linux/pull/197) [#380](https://github.com/aurora-silicon/linux/pull/380) [#470](https://github.com/aurora-silicon/linux/pull/470) | Not applicable — STAT; `drivers/gpu/drm/asahi/channel.rs` | T | +| [e8c789f9f77b](https://github.com/aurora-silicon/linux/commit/e8c789f9f77baf73f21857ca29a527930ed12aab) | Joshua Warren | drm/asahi: Add sysfs shim for agx_stats [#197](https://github.com/aurora-silicon/linux/pull/197) [#380](https://github.com/aurora-silicon/linux/pull/380) [#470](https://github.com/aurora-silicon/linux/pull/470) | Not applicable — STAT; `drivers/gpu/drm/asahi/Makefile` | T | +| [1b02b0b40af3](https://github.com/aurora-silicon/linux/commit/1b02b0b40af3822e16b5842e21964be40a42025f) | Joshua Warren | drm/asahi: Register agx_stats sysfs file on probe and unregister on drop [#197](https://github.com/aurora-silicon/linux/pull/197) [#380](https://github.com/aurora-silicon/linux/pull/380) [#470](https://github.com/aurora-silicon/linux/pull/470) | Not applicable — STAT; `drivers/gpu/drm/asahi/driver.rs` | T | +| [742f54c4855d](https://github.com/aurora-silicon/linux/commit/742f54c4855d0e3b4db54da4663d421f63d1165a) | Joshua Warren | drm/asahi: count completed submissions in agx_stats jobs [#197](https://github.com/aurora-silicon/linux/pull/197) [#380](https://github.com/aurora-silicon/linux/pull/380) [#470](https://github.com/aurora-silicon/linux/pull/470) | Not applicable — STAT; `drivers/gpu/drm/asahi/gpu.rs` | T | +| [bbfb96f309c8](https://github.com/aurora-silicon/linux/commit/bbfb96f309c8dd264540281469bba6c4a48ee789) | Joshua Warren | drm/asahi: link the sysfs shim into the driver module correctly [#197](https://github.com/aurora-silicon/linux/pull/197) [#380](https://github.com/aurora-silicon/linux/pull/380) [#470](https://github.com/aurora-silicon/linux/pull/470) | Not applicable — STAT; `drivers/gpu/drm/asahi/Makefile` | T | +| [01d19b8a4f55](https://github.com/aurora-silicon/linux/commit/01d19b8a4f55bb202b5c9d85a36996a90537602c) | Joshua Warren | drm/asahi: propagate the stats snapshot allocation result [#197](https://github.com/aurora-silicon/linux/pull/197) [#380](https://github.com/aurora-silicon/linux/pull/380) [#470](https://github.com/aurora-silicon/linux/pull/470) | Not applicable — STAT; `drivers/gpu/drm/asahi/gpu.rs` | T | +| [91c24dc11b09](https://github.com/aurora-silicon/linux/commit/91c24dc11b093be4868234ac2e00eed670d1e741) | Joshua Warren | drm/asahi: decode the stats messages where StatsMsg is nameable [#197](https://github.com/aurora-silicon/linux/pull/197) [#380](https://github.com/aurora-silicon/linux/pull/380) [#470](https://github.com/aurora-silicon/linux/pull/470) | Not applicable — STAT; `drivers/gpu/drm/asahi/channel.rs` | T | +| [dbc8b48aeb06](https://github.com/aurora-silicon/linux/commit/dbc8b48aeb06f85c9e7f9b63be9db7afef074580) | Joshua Warren | drm/asahi: qualify the versioned StatsMsg variant patterns [#197](https://github.com/aurora-silicon/linux/pull/197) [#380](https://github.com/aurora-silicon/linux/pull/380) [#470](https://github.com/aurora-silicon/linux/pull/470) | Not applicable — STAT; `drivers/gpu/drm/asahi/channel.rs` | T | +| [4189501b643f](https://github.com/aurora-silicon/linux/commit/4189501b643f404faa0ea37fc3bb379226300aa9) | Joshua Warren | drm/asahi: fix the agx_stats readout layout and the off-arm string [#197](https://github.com/aurora-silicon/linux/pull/197) [#380](https://github.com/aurora-silicon/linux/pull/380) [#470](https://github.com/aurora-silicon/linux/pull/470) | Not applicable — STAT; `drivers/gpu/drm/asahi/driver.rs` | T | +| [0b923fb29c84](https://github.com/aurora-silicon/linux/commit/0b923fb29c840b9bb675ba8ad9e81c513903b516) | Joshua Warren | drm/asahi: integrate utilization-weighted time into agx_stats busy_ns [#197](https://github.com/aurora-silicon/linux/pull/197) [#380](https://github.com/aurora-silicon/linux/pull/380) [#470](https://github.com/aurora-silicon/linux/pull/470) | Not applicable — STAT; `drivers/gpu/drm/asahi/channel.rs` | T | +| [9250872b1f33](https://github.com/aurora-silicon/linux/commit/9250872b1f33f357c01ccd3bf79be7835fc5c982) | Joshua Warren | drm/asahi: convert stats timestamps from base-clock ticks to ns [#197](https://github.com/aurora-silicon/linux/pull/197) [#380](https://github.com/aurora-silicon/linux/pull/380) [#470](https://github.com/aurora-silicon/linux/pull/470) | Not applicable — STAT; `drivers/gpu/drm/asahi/channel.rs` | T | +| [993288b37ec4](https://github.com/aurora-silicon/linux/commit/993288b37ec4368bbf2debbbd73124d8a2652410) | Joshua Warren | drm/asahi: split the tick window to avoid 128-bit division [#197](https://github.com/aurora-silicon/linux/pull/197) [#380](https://github.com/aurora-silicon/linux/pull/380) [#470](https://github.com/aurora-silicon/linux/pull/470) | Not applicable — STAT; `drivers/gpu/drm/asahi/channel.rs` | T | +| [9aebc2952fb5](https://github.com/aurora-silicon/linux/commit/9aebc2952fb51f6b0b9c03222109fe7f0ac107c2) | Ryan Murray | arm64: dts: apple: retain the T8140 P-cluster domain cells [#179](https://github.com/aurora-silicon/linux/pull/179) | Not applicable — BOARD | B | +| [e3163ca622d6](https://github.com/aurora-silicon/linux/commit/e3163ca622d6c55979d963929edbdf47f2c4c38c) | Ryan Murray | soc: apple: expose the T8140 PMP DVFS report for diagnosis [#182](https://github.com/aurora-silicon/linux/pull/182) [#380](https://github.com/aurora-silicon/linux/pull/380) [#470](https://github.com/aurora-silicon/linux/pull/470) | Not applicable — RAWDIAG; `drivers/pmdomain/apple/pmp-report.c` | O | +| [14e101ee2b8b](https://github.com/aurora-silicon/linux/commit/14e101ee2b8b599cf627015368e16ff8b3d2b95f) | Ryan Murray | cpufreq: apple: retain the unmanaged T8152 P-state ceiling | Not applicable — OTHERCHIP; `drivers/cpufreq/apple-soc-cpufreq.c` | V | +| [19d6664ea33a](https://github.com/aurora-silicon/linux/commit/19d6664ea33affcceef42d5deef488ce1ba7d50d) | Ryan Murray | arm64: dts: apple: retain a single J700 PMP alias | Not applicable — BOARD | B | +| [98038492dc48](https://github.com/aurora-silicon/linux/commit/98038492dc481d784c76fb2c52f3dd030dfad8f5) | Ryan Murray | drm: apple: recognize the T8140 external binding in the display master | Not applicable — BOARD; `drivers/gpu/drm/apple/apple_drv.c` | B | +| [78fc11047efa](https://github.com/aurora-silicon/linux/commit/78fc11047efa814a238113c46b4bb07aa7d641fb) | Ryan Murray | arm64: dts: apple: give the external J700 trial its firmware DMA window | Not applicable — PERIPH | V | +| [bc3b8aaa560f](https://github.com/aurora-silicon/linux/commit/bc3b8aaa560f23e069a7ea2d774645ea5fec39cc) | Ryan Murray | arm64: dts: apple: select the second external DCP source on J700 | Not applicable — PERIPH | V | +| [d08fd52e1619](https://github.com/aurora-silicon/linux/commit/d08fd52e161953a481a464716dd68aae85338044) | DjDeveloperr | drm/apple: enable the validated J700 external display profile by default | Not applicable — BOARD; `drivers/gpu/drm/apple/dcp-external-bw.h` | B | +| [01c8fe5e98f4](https://github.com/aurora-silicon/linux/commit/01c8fe5e98f4ab8805957ca3206d6d46a65b62e7) | Ryan Murray | soc: apple: aop: Do not overrun unread entries in the transmit ring [#345](https://github.com/aurora-silicon/linux/pull/345) | Not applicable — PERIPH; `drivers/soc/apple/aop.rs` | V | +| [02694d65655b](https://github.com/aurora-silicon/linux/commit/02694d65655baae0536d47beef4f0356145069f4) | Ryan Murray | spi: apple-qspi: Resume the controller even if its reset fails [#345](https://github.com/aurora-silicon/linux/pull/345) | Not applicable — PERIPH; `drivers/spi/spi-apple-qspi.c` | V | +| [033fa5e3c095](https://github.com/aurora-silicon/linux/commit/033fa5e3c0959f772e8464a25b8b93773f39b5b9) | Ryan Murray | soc: apple: aop: Accept a transmit entry at a write pointer of zero [#345](https://github.com/aurora-silicon/linux/pull/345) | Not applicable — PERIPH; `drivers/soc/apple/aop_ring.rs` | V | +| [0790f8a546fe](https://github.com/aurora-silicon/linux/commit/0790f8a546fea7ae46871d857643568c1b3c0c64) | Ryan Murray | hwmon: macsmc: drop the check for the undefined apple,cpu-thermal-policy [#317](https://github.com/aurora-silicon/linux/pull/317) | Not applicable — PERIPH; `drivers/hwmon/macsmc-hwmon.c` | V | +| [0943ae25f9bd](https://github.com/aurora-silicon/linux/commit/0943ae25f9bd82917a7a4813962d624d22056fd3) | Ryan Murray | HID: dockchannel-hid: Reject unaligned packet lengths before checksumming [#345](https://github.com/aurora-silicon/linux/pull/345) | Not applicable — PERIPH; `drivers/hid/dockchannel-hid/dockchannel-hid-parser-test.c` | V | +| [0c55425a8157](https://github.com/aurora-silicon/linux/commit/0c55425a8157326aa0bafd20a1c30a0f34af8650) | Ryan Murray | dt-bindings: gpu: apple,agx: Require three cells in apple,firmware-version [#420](https://github.com/aurora-silicon/linux/pull/420) | Not applicable — VERIFY | V | +| [152cc6b4d737](https://github.com/aurora-silicon/linux/commit/152cc6b4d737c3523b56d1c4b0b4cd3928e514b5) | Ryan Murray | spi: apple-qspi: Do not print a banner on probe [#314](https://github.com/aurora-silicon/linux/pull/314) | Not applicable — PERIPH; `drivers/spi/spi-apple-qspi.c` | V | +| [1baee970e0a9](https://github.com/aurora-silicon/linux/commit/1baee970e0a9d5bf0343f72eed61badfc5918159) | Jonathan Goyvaerts | dt-bindings: mailbox: apple,ascwrap-v6: Add the T6040 [#420](https://github.com/aurora-silicon/linux/pull/420) | Not applicable — VERIFY | V | +| [1df5bd529669](https://github.com/aurora-silicon/linux/commit/1df5bd529669987e8804b6ab38dbd1d5175f508d) | Ryan Murray | HID: dockchannel-hid: Test MTP message bounds with KUnit [#345](https://github.com/aurora-silicon/linux/pull/345) | Not applicable — VERIFY; `drivers/hid/dockchannel-hid/Kconfig` | V | +| [1f7383d5a281](https://github.com/aurora-silicon/linux/commit/1f7383d5a2811b99a9f2950581ec392837bfc469) | DjDeveloperr | rust: check generated layouts and provide corrected private bindgen [#171](https://github.com/aurora-silicon/linux/pull/171) | Not applicable — VERIFY | V | +| [232faf36664c](https://github.com/aurora-silicon/linux/commit/232faf36664c7e23aa9950bc3019dbf6ba534ee8) | Ryan Murray | HID: dockchannel-hid: Keep parser tests in the subsystem menu [#345](https://github.com/aurora-silicon/linux/pull/345) | Not applicable — PERIPH; `drivers/hid/dockchannel-hid/Kconfig` | V | +| [2d5c7b6bc705](https://github.com/aurora-silicon/linux/commit/2d5c7b6bc705f0ba8eef8f3d1ea23a53d4248eba) | Ryan Murray | ASoC: apple: t8140-aop-audio: Refuse card names that do not fit [#345](https://github.com/aurora-silicon/linux/pull/345) | Not applicable — PERIPH | V | +| [3006ca1a3835](https://github.com/aurora-silicon/linux/commit/3006ca1a383525a092ad1cbe9c268786292a32de) | Ryan Murray | mfd: macsmc: Test key and fixed-width readers with KUnit [#345](https://github.com/aurora-silicon/linux/pull/345) | Not applicable — VERIFY; `drivers/mfd/Kconfig` | V | +| [3024185f2efa](https://github.com/aurora-silicon/linux/commit/3024185f2efa34c4ea8ac9eebebf06b921795f0b), [aaea0b3a4fcf](https://github.com/aurora-silicon/linux/commit/aaea0b3a4fcf5cb371b735c6b595a2919a6a4d07) | DjDeveloperr | dt-bindings: power: add Apple PMC voter interface [#212](https://github.com/aurora-silicon/linux/pull/212) [#219](https://github.com/aurora-silicon/linux/pull/219) | Not applicable — VERIFY | V | +| [3132675b256e](https://github.com/aurora-silicon/linux/commit/3132675b256e07ad782280c42c853c63f6d18ef1) | Ryan Murray | selftests: apple-dcp: Run the queue recovery control through the RX drain [#345](https://github.com/aurora-silicon/linux/pull/345) | Not applicable — VERIFY | V | +| [315f6dc9944d](https://github.com/aurora-silicon/linux/commit/315f6dc9944de65990476dae34079d3e1313d605) | Chris Kearney | soc: apple: mailbox: match the T8122 GPU mailbox without the v4 fallback | Not applicable — G15; `drivers/soc/apple/mailbox.c` | V | +| [3407efe83df6](https://github.com/aurora-silicon/linux/commit/3407efe83df680534e10c0e66ffa36b75cae69ba) | Jonathan Goyvaerts | dt-bindings: soc: apple,t8140-mcc: Add the T6040 MCC [#385](https://github.com/aurora-silicon/linux/pull/385) | Not applicable — VERIFY | V | +| [35d6b26e5c9d](https://github.com/aurora-silicon/linux/commit/35d6b26e5c9d8bf22ce6284bf0594084ffd2dcdf) | Ryan Murray | soc: apple: aop: Test that a wrapped entry is refused below the read pointer [#345](https://github.com/aurora-silicon/linux/pull/345) | Not applicable — VERIFY; `drivers/soc/apple/aop_ring.rs` | V | +| [4b8d29e13da6](https://github.com/aurora-silicon/linux/commit/4b8d29e13da638bf7ba7f532844c5e368505bf20) | Jonathan Goyvaerts | dt-bindings: gpu: apple,agx: allow a fw-text region on the T6040 [#428](https://github.com/aurora-silicon/linux/pull/428) | Not applicable — VERIFY | V | +| [4c5aad430f62](https://github.com/aurora-silicon/linux/commit/4c5aad430f6283d1d6b3b961a87d1629b397f22f) | DjDeveloperr | dt-bindings: mailbox: apple,mailbox: Add the T8140 GPU mailboxes [#171](https://github.com/aurora-silicon/linux/pull/171) | Not applicable — VERIFY | V | +| [543ac70dce46](https://github.com/aurora-silicon/linux/commit/543ac70dce46b749f1a446cb80d4e63cf08949cd) | Ryan Murray | drm: apple: parser: add KUnit tests for malformed dictionaries [#345](https://github.com/aurora-silicon/linux/pull/345) | Not applicable — VERIFY; `drivers/gpu/drm/apple/Makefile` | V | +| [5b62d0c81387](https://github.com/aurora-silicon/linux/commit/5b62d0c81387e8d0d68f2a88244eeb7a618f9ec1) | Ryan Murray | mfd: macsmc: Preserve the key output on lookup failure [#345](https://github.com/aurora-silicon/linux/pull/345) | Not applicable — PERIPH; `drivers/mfd/macsmc.c` | V | +| [5e03e15dae17](https://github.com/aurora-silicon/linux/commit/5e03e15dae1740326b41df4777a5ee4b7502c02e), [94f215eaa642](https://github.com/aurora-silicon/linux/commit/94f215eaa6424e8f077f1447e78e35b3ebeb6542) | DjDeveloperr | dt-bindings: power: apple,t8140-pmp-report: describe report entries [#212](https://github.com/aurora-silicon/linux/pull/212) [#219](https://github.com/aurora-silicon/linux/pull/219) | Not applicable — VERIFY | V | +| [66db357bd614](https://github.com/aurora-silicon/linux/commit/66db357bd614c7f8e25dab080574c31dab6c0ed2) | Ryan Murray | tools/asahi: measure CPU cost of small render submissions [#201](https://github.com/aurora-silicon/linux/pull/201) | Not applicable — VERIFY | V | +| [6e2835ae7bd6](https://github.com/aurora-silicon/linux/commit/6e2835ae7bd6d16570464106bdfb740b6fed27c4) | Naeem Malik | soc: apple: mailbox: match the T6031 GPU mailbox without the v4 fallback | Not applicable — G15; `drivers/soc/apple/mailbox.c` | V | +| [712530b9753f](https://github.com/aurora-silicon/linux/commit/712530b9753fd8692b31382b3e9c6aaaa720ffe0) | Ryan Murray | gpio: macsmc: tidy the J700 pcIO helpers [#345](https://github.com/aurora-silicon/linux/pull/345) | Not applicable — PERIPH; `drivers/gpio/gpio-macsmc.c` | V | +| [7a6e327251cc](https://github.com/aurora-silicon/linux/commit/7a6e327251cc51d0a483483f0b81dda99a811791) | Ryan Murray | soc: apple: aop: Validate the receive ring before taking entries [#345](https://github.com/aurora-silicon/linux/pull/345) | Not applicable — PERIPH; `drivers/soc/apple/aop.rs` | V | +| [807cb8976697](https://github.com/aurora-silicon/linux/commit/807cb8976697ff272433278b272444ced7e91d57) | Ryan Murray | hwmon: macsmc: account for temperature channels in one helper [#317](https://github.com/aurora-silicon/linux/pull/317) | Not applicable — PERIPH; `drivers/hwmon/macsmc-hwmon.c` | V | +| [83039e319faf](https://github.com/aurora-silicon/linux/commit/83039e319faf3e49ceabc424c7c69180ac997dfd) | Ryan Murray | dt-bindings: apple: use the canonical SPDX form in PMP, PMC and report bindings [#317](https://github.com/aurora-silicon/linux/pull/317) | Not applicable — VERIFY | V | +| [87314d48cc08](https://github.com/aurora-silicon/linux/commit/87314d48cc080bcf535291dc636f1ec6164a06f9) | Ryan Murray | drm/apple: Test firmware message bounds with KUnit [#345](https://github.com/aurora-silicon/linux/pull/345) | Not applicable — VERIFY; `drivers/gpu/drm/apple/Kconfig` | V | +| [910327707f85](https://github.com/aurora-silicon/linux/commit/910327707f852c060b5ada81e64a2a4563cb64b4) | Ryan Murray | ASoC: apple: Test T8140 AOP producer reports with KUnit [#345](https://github.com/aurora-silicon/linux/pull/345) | Not applicable — VERIFY | V | +| [95b26591a1c4](https://github.com/aurora-silicon/linux/commit/95b26591a1c4b446656c3bc8a257a17792917a78) | Ryan Murray | spi: apple: Reword the hardware CS delay comment [#314](https://github.com/aurora-silicon/linux/pull/314) | Not applicable — VERIFY; `drivers/spi/spi-apple.c` | V | +| [966e777fd568](https://github.com/aurora-silicon/linux/commit/966e777fd568565b88c5f8ba4acb36463ae31e47) | Ryan Murray | MAINTAINERS: keep T8140 PMP coverage under Aurora [#179](https://github.com/aurora-silicon/linux/pull/179) | Not applicable — VERIFY | V | +| [a1f9aabbe6a9](https://github.com/aurora-silicon/linux/commit/a1f9aabbe6a9771a4dbf3259bc808713f0daac46) | Ryan Murray | dt-bindings: apple: describe PMP fast-die reporting on T8140 [#179](https://github.com/aurora-silicon/linux/pull/179) | Not applicable — VERIFY | V | +| [a239464d233d](https://github.com/aurora-silicon/linux/commit/a239464d233d2e94b89dea159f298b70e015b6a6) | Ryan Murray | kunit: Add an Apple firmware parser test configuration [#345](https://github.com/aurora-silicon/linux/pull/345) | Not applicable — VERIFY | V | +| [a27007e6ace3](https://github.com/aurora-silicon/linux/commit/a27007e6ace3815d11c340c25a3ee90cfb2981b0) | Ryan Murray | HID: dockchannel: Log routine interface set-up at debug level [#345](https://github.com/aurora-silicon/linux/pull/345) | Not applicable — PERIPH; `drivers/hid/dockchannel-hid/dockchannel-hid.c` | V | +| [a494d618f5a9](https://github.com/aurora-silicon/linux/commit/a494d618f5a9af40ae466166176ae36476b4638a) | Ryan Murray | mfd: macsmc: Hold the mutex while decoding key metadata [#345](https://github.com/aurora-silicon/linux/pull/345) | Not applicable — PERIPH; `drivers/mfd/macsmc.c` | V | +| [b8f892b79561](https://github.com/aurora-silicon/linux/commit/b8f892b79561ea085c0ab2e44368f873f15b9183) | Ryan Murray | soc: apple: aop: Decode ring entry headers without pointer casts [#345](https://github.com/aurora-silicon/linux/pull/345) | Not applicable — PERIPH; `drivers/soc/apple/aop.rs` | V | +| [ba6eb20e2ed9](https://github.com/aurora-silicon/linux/commit/ba6eb20e2ed9065a2925d0f16ae030acfaeceefe) | Ryan Murray | soc: apple: mailbox: Describe the ASCWrap v6 CPU start and stop in hardware terms | Not applicable — VERIFY; `drivers/soc/apple/mailbox.c` | V | +| [bc7394673cbe](https://github.com/aurora-silicon/linux/commit/bc7394673cbed4ef22971a49926be6070a7312be) | DjDeveloperr | tools: preserve bindgen recipe validation across Python modes [#171](https://github.com/aurora-silicon/linux/pull/171) | Not applicable — VERIFY | V | +| [bd7739d6232e](https://github.com/aurora-silicon/linux/commit/bd7739d6232e818ef7cf30acbcc50d24723e061c) | Ryan Murray | mfd: macsmc: Reject short fixed-width replies [#345](https://github.com/aurora-silicon/linux/pull/345) | Not applicable — PERIPH; `drivers/mfd/macsmc.c` | V | +| [c49ab96deb3c](https://github.com/aurora-silicon/linux/commit/c49ab96deb3c378015d1db27bf2122c837fd3bc0) | DjDeveloperr | dt-bindings: gpu: apple,agx: Add apple,agx-t8140 [#171](https://github.com/aurora-silicon/linux/pull/171) | Not applicable — VERIFY | V | +| [c9c779d6a2ba](https://github.com/aurora-silicon/linux/commit/c9c779d6a2bae406a9d48b7f733b16fd253730c7) | Jonathan Goyvaerts | dt-bindings: soc: apple,pmp: Add the T6040 PMP [#374](https://github.com/aurora-silicon/linux/pull/374) | Not applicable — VERIFY | V | +| [cbd157ef2a12](https://github.com/aurora-silicon/linux/commit/cbd157ef2a12a46e3caf5367de912a525d8d4874) | Ryan Murray | hwmon: macsmc: keep the channel of a temperature sensor that fails to set up [#317](https://github.com/aurora-silicon/linux/pull/317) | Not applicable — PERIPH; `drivers/hwmon/Kconfig` | V | +| [cf4ce5f41f4b](https://github.com/aurora-silicon/linux/commit/cf4ce5f41f4b86c4084b49ec6585377e16fd53b7) | Chris Kearney | thunderbolt, phy, soc: apple: T6030 USB4/Thunderbolt, Type-C and DP tunnel support | Not applicable — G15; `drivers/iommu/of_iommu.c` | V | +| [d36196335065](https://github.com/aurora-silicon/linux/commit/d36196335065346ef57a323e483002151670320c) | Ryan Murray | arm64: dts: apple: t6040-j616s: Keep the GPU disabled [#420](https://github.com/aurora-silicon/linux/pull/420) | Not applicable — SUPERSEDED | B | +| [d717a601430a](https://github.com/aurora-silicon/linux/commit/d717a601430aea6711ffc7486f5edb16ca7e7f66) | Jonathan Goyvaerts | dt-bindings: gpu: apple,agx: Add apple,agx-t6040 [#420](https://github.com/aurora-silicon/linux/pull/420) | Not applicable — VERIFY | V | +| [d90daf04de84](https://github.com/aurora-silicon/linux/commit/d90daf04de8431f70eed8b590a532f38a6249095) | Ryan Murray | media: apple: isp: Do not trust the batch count when a batch fails [#345](https://github.com/aurora-silicon/linux/pull/345) | Not applicable — PERIPH; `drivers/media/platform/apple/isp/isp-v4l2.c` | V | +| [d9aface7ea85](https://github.com/aurora-silicon/linux/commit/d9aface7ea8533965e33476631c8dbc44dfeb439) | Ryan Murray | HID: dockchannel: Build the protocol KUnit test only with the driver [#345](https://github.com/aurora-silicon/linux/pull/345) | Not applicable — PERIPH; `drivers/hid/dockchannel-hid/Kconfig` | V | +| [e65283c42678](https://github.com/aurora-silicon/linux/commit/e65283c426783b12298f72896ef6f0dc33c4222c) | Ryan Murray | ASoC: apple: t8140-aop-audio: Let power request sequences wrap [#345](https://github.com/aurora-silicon/linux/pull/345) | Not applicable — PERIPH | V | +| [e666e50ccfe8](https://github.com/aurora-silicon/linux/commit/e666e50ccfe8c58e8fa72bdf7ef6bedffec6551a) | Ryan Murray | rtc: macsmc: correct the comment on the whole-second offset [#317](https://github.com/aurora-silicon/linux/pull/317) | Not applicable — PERIPH; `drivers/rtc/rtc-macsmc.c` | V | +| [e99a6adbeba7](https://github.com/aurora-silicon/linux/commit/e99a6adbeba77b7ed032ef61f864afb3da8fe3a9) | Ryan Murray | dt-bindings: hwmon: apple,smc-hwmon: drop apple,cpu-thermal-policy [#317](https://github.com/aurora-silicon/linux/pull/317) | Not applicable — VERIFY | V | +| [ec34fb1f2835](https://github.com/aurora-silicon/linux/commit/ec34fb1f2835b291f978bd091ed73347cba7ceae) | Ryan Murray | mfd: macsmc: Reject out-of-range key indices [#345](https://github.com/aurora-silicon/linux/pull/345) | Not applicable — PERIPH; `drivers/mfd/macsmc.c` | V | +| [ed0925cdf091](https://github.com/aurora-silicon/linux/commit/ed0925cdf091083ba1efee97bbb288e9c331f812) | Ryan Murray | spi: apple-qspi: Restore the controller configuration after system sleep [#345](https://github.com/aurora-silicon/linux/pull/345) | Not applicable — PERIPH; `drivers/spi/spi-apple-qspi.c` | V | +| [ee6f48ce60bd](https://github.com/aurora-silicon/linux/commit/ee6f48ce60bdf68d596c4bedd31e35f55cd6bb03) | Ryan Murray | MAINTAINERS: Cover Apple firmware parser tests [#345](https://github.com/aurora-silicon/linux/pull/345) | Not applicable — VERIFY | V | +| [f3324e0de0e2](https://github.com/aurora-silicon/linux/commit/f3324e0de0e26bd43df402001c2e3f0f16821520) | Jonathan Goyvaerts | dt-bindings: power: apple,pmc: Add the T6040 PMC and CLPC [#385](https://github.com/aurora-silicon/linux/pull/385) | Not applicable — VERIFY | V | +| [f52e01d6925d](https://github.com/aurora-silicon/linux/commit/f52e01d6925de5022176c5038ff13cc9ad17559b) | Ryan Murray | dt-bindings: gpu: apple,agx: Match the T8140 register layout [#314](https://github.com/aurora-silicon/linux/pull/314) | Not applicable — VERIFY | V | +| [fdd138bbc722](https://github.com/aurora-silicon/linux/commit/fdd138bbc7228b8b1a232268e2a02cc126b24d95) | Ryan Murray | hwmon: macsmc: move the module help text back to its option [#345](https://github.com/aurora-silicon/linux/pull/345) | Not applicable — PERIPH; `drivers/hwmon/Kconfig` | V | +| [fec5e40ebade](https://github.com/aurora-silicon/linux/commit/fec5e40ebade9f7cf3684acfbb9d603d5b82127a) | Jonathan Goyvaerts | dt-bindings: power: apple,t8140-pmp-report: Add the T6040 aperture [#374](https://github.com/aurora-silicon/linux/pull/374) | Not applicable — VERIFY | V | +| [ff3ebdbad58c](https://github.com/aurora-silicon/linux/commit/ff3ebdbad58c1c50dadbf9cecd7f67aada516dc1) | Ryan Murray | nvme-apple: Reword two post-M4 handoff comments [#345](https://github.com/aurora-silicon/linux/pull/345) | Not applicable — VERIFY; `drivers/nvme/host/apple.c` | V | + +### Merge/provenance records + +Parent mechanisms are listed above; no isolated speedup is assigned here. + +| Commits / encountered aliases | Author | Improvement / record | T6040 after ports — evidence key and changed source | Measurement | +| --- | --- | --- | --- | --- | +| [00463d1d4031](https://github.com/aurora-silicon/linux/commit/00463d1d40317155246a13573aeab60ff773b51b), [f566e6098c67](https://github.com/aurora-silicon/linux/commit/f566e6098c676566b02a191eb58e5783fa7bf0b6) | Janne Grunau | Merge branch 'bits/010-soc' into asahi-wip | Not applicable — MERGE | V | +| [80390ef034fc](https://github.com/aurora-silicon/linux/commit/80390ef034fcfaf212674bc53b0a91e596409bcf), [5e35a79dac7f](https://github.com/aurora-silicon/linux/commit/5e35a79dac7f0958af6bd3ae9ea4517d22769cb2) | Janne Grunau | Merge branch 'bits/020-dart' into asahi-wip | Not applicable — MERGE | V | +| [4eea6c6d47ba](https://github.com/aurora-silicon/linux/commit/4eea6c6d47bab3a334b9c1633279c4392e2a4de7), [57710743dc8c](https://github.com/aurora-silicon/linux/commit/57710743dc8c0e9286115f38ad19a298cd36cc50) | Janne Grunau | Merge branch 'bits/030-misc' into asahi-wip | Not applicable — MERGE | V | +| [59f7e8be4e3b](https://github.com/aurora-silicon/linux/commit/59f7e8be4e3b693a96a1dd3b2355aa65dd784b0e), [466efd223bbf](https://github.com/aurora-silicon/linux/commit/466efd223bbf43f9e0f4f7065c67d6d4e97fc04c) | Janne Grunau | Merge branch 'bits/090-spi-hid' into asahi-wip | Not applicable — MERGE | V | +| [2871ed01752e](https://github.com/aurora-silicon/linux/commit/2871ed01752ef74924608f23b5edd7927ae4b71c), [f55090a2d12a](https://github.com/aurora-silicon/linux/commit/f55090a2d12a6fd3dad485a7621392b8f508f8a8) | Janne Grunau | Merge branch 'bits/190-rust' into asahi-wip | Not applicable — MERGE | V | +| [cd36abb1e1d2](https://github.com/aurora-silicon/linux/commit/cd36abb1e1d2bcead322aedaed3cab7cbbab5d4b), [3f3dfd6ef171](https://github.com/aurora-silicon/linux/commit/3f3dfd6ef171f4c3801e1bd8b7c880774e17f1ad) | Janne Grunau | Merge branch 'bits/210-gpu' into asahi-wip | Not applicable — MERGE | V | +| [d6c7409e354f](https://github.com/aurora-silicon/linux/commit/d6c7409e354fee96c74a0aa68c5cbd0cdc0b8d92), [39b401626267](https://github.com/aurora-silicon/linux/commit/39b4016262672c0f87d7e5af835f42b9632dcc6c) | Janne Grunau | Merge branch 'bits/250-aop' into asahi-wip | Not applicable — MERGE | V | +| [072195f87df8](https://github.com/aurora-silicon/linux/commit/072195f87df8ca94580cf5ca0d9a801e5bd217e4), [11ee430b3cf2](https://github.com/aurora-silicon/linux/commit/11ee430b3cf20a6611bea0462335f0ae975a40d7) | Janne Grunau | Merge branch 'bits/500-backports' into asahi-wip | Not applicable — MERGE | V | +| [fe8324184dd8](https://github.com/aurora-silicon/linux/commit/fe8324184dd8c6d3b09a532a9cb6a533c561a324) | Ryan Murray | Merge tag 'v7.3' into unified-7.3/base | Not applicable — MERGE | V | +| [cb8e5e495b88](https://github.com/aurora-silicon/linux/commit/cb8e5e495b88f638c22782136a8721a4f26f4346) | Ryan Murray | Merge branch 'unified/bits/010-soc' into rebase-wip/7.2.10-unified | Not applicable — MERGE | V | +| [6de20e534e47](https://github.com/aurora-silicon/linux/commit/6de20e534e473dd9efd9e14c727883df6036d60b) | Ryan Murray | Merge branch 'unified/bits/020-dart' into rebase-wip/7.2.10-unified | Not applicable — MERGE | V | +| [15fe1cbe72df](https://github.com/aurora-silicon/linux/commit/15fe1cbe72df8d8eb79b2dec500e58bbc5f57647) | Ryan Murray | Merge branch 'unified/bits/060-power-thermal' into rebase-wip/7.2.10-unified | Not applicable — MERGE | V | +| [939cccf33380](https://github.com/aurora-silicon/linux/commit/939cccf333802c32a319f0ddb4b0d01c85aab990) | Ryan Murray | Merge branch 'unified/bits/070-audio' into rebase-wip/7.2.10-unified | Not applicable — MERGE | V | +| [2409e9da1efb](https://github.com/aurora-silicon/linux/commit/2409e9da1efb9494fc9437ce9f3bd909c0f7dd36) | Ryan Murray | Merge branch 'unified/bits/100-sep' into rebase-wip/7.2.10-unified | Not applicable — MERGE | V | +| [2bc8aa67f178](https://github.com/aurora-silicon/linux/commit/2bc8aa67f17817c22c6bacaecd7ecf4df8e8c63d) | Ryan Murray | Merge branch 'unified/bits/200-dcp' into rebase-wip/7.2.10-unified | Not applicable — MERGE | V | +| [c00586c09e2b](https://github.com/aurora-silicon/linux/commit/c00586c09e2bab26f92ddd5b943cbcc66c047ff5) | Ryan Murray | Merge branch 'unified/bits/210-gpu' into rebase-wip/7.2.10-unified | Not applicable — MERGE | V | +| [14ca49f98175](https://github.com/aurora-silicon/linux/commit/14ca49f98175b54c37fa16af766e11f163d3a80f) | Ryan Murray | Merge branch 'unified-7.3/bits/010-soc' into unified-7.3/canonical | Not applicable — MERGE | V | +| [665a5ca84508](https://github.com/aurora-silicon/linux/commit/665a5ca845081049335f69d0bf27c66c90898b7b) | Ryan Murray | Merge branch 'unified-7.3/bits/020-dart' into unified-7.3/canonical | Not applicable — MERGE | V | +| [c217f4f47bf0](https://github.com/aurora-silicon/linux/commit/c217f4f47bf003c8fb39400d1af8179b552c497e) | Ryan Murray | Merge branch 'unified-7.3/bits/060-power-thermal' into unified-7.3/canonical | Not applicable — MERGE | V | +| [5bdd6c5b2c80](https://github.com/aurora-silicon/linux/commit/5bdd6c5b2c80a6f16c8835a57a5d56d26af67839) | Ryan Murray | Merge branch 'unified-7.3/bits/070-audio' into unified-7.3/canonical | Not applicable — MERGE | V | +| [f3c8cf25dd5d](https://github.com/aurora-silicon/linux/commit/f3c8cf25dd5d01f3871267c1b77df343dbef045d) | Ryan Murray | Merge branch 'unified-7.3/bits/100-sep' into unified-7.3/canonical | Not applicable — MERGE | V | +| [c15790e20e01](https://github.com/aurora-silicon/linux/commit/c15790e20e01e8562e0b54a00bee8a8ef9ded84b) | Ryan Murray | Merge branch 'unified-7.3/bits/200-dcp' into unified-7.3/canonical | Not applicable — MERGE | V | +| [ea36d4240e40](https://github.com/aurora-silicon/linux/commit/ea36d4240e40fc7607d568a8fa56f1fa5cf01c03) | Ryan Murray | Merge branch 'unified-7.3/bits/210-gpu' into unified-7.3/canonical | Not applicable — MERGE | V | +| [94141d397eae](https://github.com/aurora-silicon/linux/commit/94141d397eaede2ba95eab7279f4bc18f75bb9d1) | Ryan | Merge pull request #374 from jgoyvaerts/m4pro/pmp-t6041 | Not applicable — MERGE | V | +| [4e5eea54ce7e](https://github.com/aurora-silicon/linux/commit/4e5eea54ce7e1c9d21867d81c5e5672e1cbd1c34) | Ryan | Merge pull request #368 from jgoyvaerts/m4pro/small-fixes | Not applicable — MERGE | V | +| [815438428a9e](https://github.com/aurora-silicon/linux/commit/815438428a9e45f0facbb10c4f9bd77047ef7282) | Ryan | Merge pull request #385 from jgoyvaerts/m4pro/pmc-clpc-mcc-t6040 | Not applicable — MERGE | V | +| [49273fd6921c](https://github.com/aurora-silicon/linux/commit/49273fd6921c9b39101cd1ee8c8d2f2ca0153440) | Ryan | Merge pull request #386 from jgoyvaerts/m4pro/apple-dart-t6040 | Not applicable — MERGE | V | +| [50a912fe81fc](https://github.com/aurora-silicon/linux/commit/50a912fe81fcdbd770d231db8eda856a1678c194) | Ryan | Merge pull request #391 from jgoyvaerts/m4pro/g17-generic | Not applicable — MERGE | V | +| [311471f73e99](https://github.com/aurora-silicon/linux/commit/311471f73e9919764bfb24acfe7eeb7c5230795a) | Ryan | Merge pull request #402 from aurora-silicon/j616s/pmp-ep7-20261024 | Not applicable — MERGE | V | +| [1aa2857509d5](https://github.com/aurora-silicon/linux/commit/1aa2857509d50f7f787541280aba7b274721919c) | Ryan | Merge pull request #403 from aurora-silicon/j616s/usb-20261023 | Not applicable — MERGE | V | +| [04231230fd76](https://github.com/aurora-silicon/linux/commit/04231230fd76fadd12a8b44707159f37f0e498b1), [e9a9b647d0f3](https://github.com/aurora-silicon/linux/commit/e9a9b647d0f36e222f16de732f78f2d77ea2d661), [b9a73d410ff4](https://github.com/aurora-silicon/linux/commit/b9a73d410ff48c65624c9283c634b4d06f4dfc6a), [4433181e4eb6](https://github.com/aurora-silicon/linux/commit/4433181e4eb649033f07751001fbb72621752647) | Jonathan Goyvaerts, Ryan Murray | Merge integration/aurora into the T6040 GPU series [#420](https://github.com/aurora-silicon/linux/pull/420) | Not applicable — MERGE | V | +| [2a868cf22d90](https://github.com/aurora-silicon/linux/commit/2a868cf22d90f0de72b2ad3c339b93460fdb9927) | Ryan | Merge pull request #413 from jgoyvaerts/m4pro/g17-robustness | Not applicable — MERGE | V | +| [2e9e457c922d](https://github.com/aurora-silicon/linux/commit/2e9e457c922d799dcc27975ff6f5d114d2719f3f) | Ryan | Merge pull request #418 from aurora-silicon/ryan/g17-innocent-clients-413 | Not applicable — MERGE | V | +| [127df1be31af](https://github.com/aurora-silicon/linux/commit/127df1be31af90613079dcedaeaf0a2aa9ae7d10) | Ryan Murray | Merge integration/aurora into aurora-wip-sync-177 | Not applicable — MERGE | V | +| [e5dad6df06f6](https://github.com/aurora-silicon/linux/commit/e5dad6df06f66b944037770e81e6a481a0b8302f), [d1b62dc403c6](https://github.com/aurora-silicon/linux/commit/d1b62dc403c6f9ed534850f204f069befca149f1) | Ryan Murray | Merge integration/aurora into m4pro/g17-t6040-text-identity [#428](https://github.com/aurora-silicon/linux/pull/428) | Not applicable — MERGE | V | +| [a1b269f79e35](https://github.com/aurora-silicon/linux/commit/a1b269f79e35844660892b9779adb2f68abc38b7) | Ryan | Merge pull request #395: sync omacom #177 (G15, T6030 dock, platform fixes) onto 7.3 | Not applicable — MERGE | V | +| [8bc8f12e95ed](https://github.com/aurora-silicon/linux/commit/8bc8f12e95edbbd5b7d5431973eb1f08c872e87c) | Ryan | Merge pull request #427 from jgoyvaerts/m4pro/asahi-pgtable-checked-borrow | Not applicable — MERGE | V | +| [18012befd69c](https://github.com/aurora-silicon/linux/commit/18012befd69ceed9892c28291132ebd4c05a6076) | Ryan | Merge pull request #428 from jgoyvaerts/m4pro/g17-t6040-text-identity | Not applicable — MERGE | V | +| [41ef596ab373](https://github.com/aurora-silicon/linux/commit/41ef596ab3731ef695fcce2fdfb666bc47da21b7) | Dj | Merge pull request #443 from aurora-silicon/fix/t8140-cpufreq-lazy-verify-20261025 | Not applicable — MERGE | V | +| [af80fbe9094b](https://github.com/aurora-silicon/linux/commit/af80fbe9094b631c5d734883bd259ad0925e88ba) | Dj | Merge pull request #447 from aurora-silicon/fix/aic2-effective-affinity-20261025 | Not applicable — MERGE | V | +| [67fdc27bdadb](https://github.com/aurora-silicon/linux/commit/67fdc27bdadb58520839376a2b85d1265c3fa78f), [b4e5e4cff710](https://github.com/aurora-silicon/linux/commit/b4e5e4cff710c9961a05d4ba6c16f0f8c47c3776) | Chris Kearney | Merge current aurora-wip into the combined Thunderbolt series | Not applicable — MERGE | V | +| [99a554596d0a](https://github.com/aurora-silicon/linux/commit/99a554596d0a4960120b78f9a09ab30ffd4d0fc1) | Chris Kearney | Merge aurora-wip 3bb0a6104a11 into tb-dp-tunnel-t8103 | Not applicable — MERGE | V | +| [5b55206ca89b](https://github.com/aurora-silicon/linux/commit/5b55206ca89bbeb8bb54a208a9b9a36b80d6360b) | Acelogic | Merge M5 CPU support while retaining per-chip frequency safeguards | Not applicable — MERGE | V | +| [95f70d759093](https://github.com/aurora-silicon/linux/commit/95f70d7590935fdfb63c889d73598996dbf6c695) | Ryan Murray | Merge pull request #172 from aurora-silicon/j700-dcp-foundation | Not applicable — MERGE | V | +| [b37df4bdd552](https://github.com/aurora-silicon/linux/commit/b37df4bdd552dca6fa180e7826d0ec22b5e264f4) | Ryan Murray | Merge pull request #8 from iconidentify/tb-dp-tunnel-t8103 | Not applicable — MERGE | V | +| [c9a55efdff8f](https://github.com/aurora-silicon/linux/commit/c9a55efdff8f9505114fab2c89e934ce363e9b3b) | Ryan Murray | Merge pull request #42 from aurora-silicon/feat/sep-neo-rebase | Not applicable — MERGE | V | +| [026b4131cf05](https://github.com/aurora-silicon/linux/commit/026b4131cf0506b0974e6a0ad0b13cfa15f80cc6) | Ryan Murray | Merge pull request #43 from aurora-silicon/feat/j700-thermal-framework-v2 | Not applicable — MERGE | V | +| [e9c631c62252](https://github.com/aurora-silicon/linux/commit/e9c631c6225261125aae251b6a0b743e0663ed37) | Ryan Murray | Merge pull request #46 from aurora-silicon/fix/j414s-dual-dock-dp | Not applicable — MERGE | V | +| [b8fef4b5b0b5](https://github.com/aurora-silicon/linux/commit/b8fef4b5b0b5d1ed5401db1249fc230fc9ce3f60) | Ryan Murray | Merge pull request #156 from aurora-silicon/codex/neo-ave-bringup | Not applicable — MERGE | V | +| [527179cf0856](https://github.com/aurora-silicon/linux/commit/527179cf085663164d17e4d8e8abf2c3e33b5070) | Ryan Murray | Merge pull request #168 from joshuaswarren/agent/jw16-agx-stats4 | Not applicable — MERGE | V | +| [918c0ff6270c](https://github.com/aurora-silicon/linux/commit/918c0ff6270c673e67067453a2e727b5f3e26331) | Ryan Murray | Merge pull request #55 from aurora-silicon/t8140-cpu-capacity | Not applicable — MERGE | V | +| [db4752825c93](https://github.com/aurora-silicon/linux/commit/db4752825c935257fb77f6b8aacf3b4ee003e9fa) | Ryan Murray | Merge pull request #174 from aurora-silicon/j700/t8140-dt-complete | Not applicable — MERGE | V | +| [d8ad51f6b8c8](https://github.com/aurora-silicon/linux/commit/d8ad51f6b8c809444fe4e3f1ef0a1ebb3ba6af5e) | Ryan Murray | Merge pull request #179 from aurora-silicon/j700/pmp-mcc-thermal | Not applicable — MERGE | V | +| [5de43d36cd52](https://github.com/aurora-silicon/linux/commit/5de43d36cd520eaf5df6fc16d047ee413ac54968) | Ryan Murray | Merge pull request #181 from aurora-silicon/j700/t8140-ane-topology | Not applicable — MERGE | V | +| [564976d8e4a6](https://github.com/aurora-silicon/linux/commit/564976d8e4a60dd64a4dc77b2b82f2a0f25f3f45) | Ryan Murray | Merge pull request #182 from aurora-silicon/j700/mcc-telemetry | Not applicable — MERGE | V | +| [4f3a64e7baa9](https://github.com/aurora-silicon/linux/commit/4f3a64e7baa91c6616ae955695e8a5c847dff968) | Ryan | Merge pull request #215 from aurora-silicon/j700/t8140-slc-allocation | Not applicable — MERGE | V | +| [ccedac0c6a30](https://github.com/aurora-silicon/linux/commit/ccedac0c6a30b11b9b1926522a98d4079c9acbf1) | Ryan | Merge pull request #221 from aurora-silicon/j700/display-handoff-race | Not applicable — MERGE | V | +| [123c0d909a8a](https://github.com/aurora-silicon/linux/commit/123c0d909a8af07363e3e885691f0e9feb10c9bc) | Ryan | Merge pull request #223 from aurora-silicon/apple/sashiko-pmp-entry-unbind | Not applicable — MERGE | V | +| [1702229de78f](https://github.com/aurora-silicon/linux/commit/1702229de78f1acd92808b228034839fb7e6c0bc) | Ryan | Merge pull request #232 from aurora-silicon/j700/aop-dart-invalidate | Not applicable — MERGE | V | +| [5c06b1998b8e](https://github.com/aurora-silicon/linux/commit/5c06b1998b8eefdf785b5320a6f8cec31f8da29a) | Ryan | Merge pull request #272 from aurora-silicon/j700/pmgr-failed-power-on | Not applicable — MERGE | V | +| [92ad2a772af8](https://github.com/aurora-silicon/linux/commit/92ad2a772af82a81fc3c73c915a48b9b611db55b) | Ryan Murray | Merge public J700 DP Alt qualification stack into current aurora-wip | Not applicable — MERGE | V | +| [f5afac94ca29](https://github.com/aurora-silicon/linux/commit/f5afac94ca2999c858fb61ebab0bf4b4c168fc6d) | Ryan Murray | drm/apple: compose lean J700 display and automatic recovery candidate | Not applicable — MERGE | V | +| [08e0d0a90be3](https://github.com/aurora-silicon/linux/commit/08e0d0a90be3bb6d777cc1a285ba22bfce12a4d9) | Ryan Murray | drm/apple: integrate reviewed fixed-host lifecycle and scoped boot fixes | Not applicable — MERGE | V | +| [47c99edf4878](https://github.com/aurora-silicon/linux/commit/47c99edf48782ec6078624f579108e243f8cbb4c) | Ryan | Merge pull request #342 from aurora-silicon/cleanup/rust-clippy-20261017 | Not applicable — MERGE | V | +| [3abf4a56b59e](https://github.com/aurora-silicon/linux/commit/3abf4a56b59e323026f9a6e2fe9a0d4307c251d5) | Ryan | Merge pull request #345 from aurora-silicon/fix/harden-all-20261017 | Not applicable — MERGE | V | +| [c20b9d96ba77](https://github.com/aurora-silicon/linux/commit/c20b9d96ba77da9a32c751ab0e63f283d10eea4a) | Ryan Murray | Merge branch 'integration/aurora' into aurora-wip | Not applicable — MERGE | V | +| [8080772b8377](https://github.com/aurora-silicon/linux/commit/8080772b83773a6ca224718de3fdb8bddc1b8511) | Ryan Murray | Merge branch 'integration/aurora' at a1b269f7 into aurora-wip | Not applicable — MERGE | V | +| [42c59170fe98](https://github.com/aurora-silicon/linux/commit/42c59170fe98eb0266c4ca5ea5e1abd0cbac97aa) | Ryan Murray | Merge integration/aurora at 3b77f0f9 into aurora-wip | Not applicable — MERGE | V | +| [1a3c5ca41f8f](https://github.com/aurora-silicon/linux/commit/1a3c5ca41f8f0215a355625ad7c4d456c78f2802) | Ryan Murray | Merge branch 'unified-7.3/bits/170-usb' into unified-7.3/canonical | Not applicable — MERGE | V | +| [2e1656f93c27](https://github.com/aurora-silicon/linux/commit/2e1656f93c27f76171fa265dadcb5ba019bce421) | Ryan Murray | Merge pull request #65 from joshuaswarren/ane-dt-polltx-aurora | Not applicable — MERGE | V | +| [9dbd46baf1b2](https://github.com/aurora-silicon/linux/commit/9dbd46baf1b289a30b5f9be6c4b5f6341135d7ce) | Ryan | Merge pull request #198 from aurora-silicon/fix/pr171-review [#171](https://github.com/aurora-silicon/linux/pull/171) | Not applicable — MERGE | V | +| [f1f334c2ec72](https://github.com/aurora-silicon/linux/commit/f1f334c2ec724b8c96f534a561df9480aa4310ee) | Ryan Murray | Merge branch 'backlog/kunit-parsers-fixes-20261017' into backlog/harden-all-20261017 [#345](https://github.com/aurora-silicon/linux/pull/345) | Not applicable — MERGE | V | + +### Public measured follow-ups outside the audited branch heads + +[#464](https://github.com/aurora-silicon/linux/pull/464), +[#468](https://github.com/aurora-silicon/linux/pull/468) and +[#494](https://github.com/aurora-silicon/linux/pull/494) were also read. Their +heads are not in either pinned branch snapshot; they are not silently counted +as inherited or qualified ports. They remain separate proposals for review: + +- #464 limits the T8140 GPU floor to streams and yields to PMP demand. Its + reported video power is 5.151 to 4.975 W, with light-submit gains retained. + Check the same video/browsing/heavy-load failure modes on g2, including actual + T6040 PMP voter behavior, before accepting its unconditional powered floor. +- #468 reports 7409 to 4625 doorbells in a traced hop, slow hops 19.6% to 13.9%, + and C-d64 13.89 to 13.11 us. Its shared compute-doorbell mechanism would need + no T6040 feature gate; it is an additional unmerged source dependency. +- #494 reports event passes 8.2 to 6.2 us and submit.empty d2 1.78 to 1.69 us. + Its shared event housekeeping change is another unmerged source dependency; + it is not evidence that the audited branches already provide those gains. + +No register, voltage, slot-key address, power curve, module parameter or new +platform option is invented to close an evidence gap. Applicability of dormant +25G83-specific feature/identity paths must come from validated facts; successful +compilation is not that qualification. From 3db24da5c830344e1c1449d38ec9ae0de1396a3d Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Mon, 26 Oct 2026 09:28:38 -0400 Subject: [PATCH 212/388] drm/asahi: keep G17 long jobs alive beside other long jobs A published job's deadline is renewed, up to thirty seconds, while some command retired within the last five seconds: the GPU is then shared, not hung. Two ray traversal dispatches side by side retire nothing until they end, though both progress. Alone each took about 6.3 s; side by side they took 10 to 17 s, past the ten seconds a long job gets without that renewal, so the watchdog failed most of them with ETIMEDOUT and their clients lost the device. Also count the GPU as shared while a long job of another queue runs beside a long job. The thirty-second bound still ends a pair that never finishes. --- drivers/gpu/drm/asahi/g17/job.rs | 12 +++--- drivers/gpu/drm/asahi/g17/queue/compute.rs | 10 +++++ drivers/gpu/drm/asahi/g17/runtime/backend.rs | 10 +++++ drivers/gpu/drm/asahi/g17/timeout.rs | 42 ++++++++++++++------ 4 files changed, 57 insertions(+), 17 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/job.rs b/drivers/gpu/drm/asahi/g17/job.rs index 8175c992b8a62..61033aa9e52b4 100644 --- a/drivers/gpu/drm/asahi/g17/job.rs +++ b/drivers/gpu/drm/asahi/g17/job.rs @@ -293,6 +293,8 @@ pub(crate) trait Backend: Send + Sync { fn cancel(&self, packet: Arc); /// Transfer a timeout to an outstanding off-lock graph transaction, if any. fn defer_timeout(&self, packet: &Arc, renewals: u8) -> bool; + /// A long job of another queue is running beside `packet`. + fn long_job_beside(&self, _packet: &Arc) -> bool { false } /// Return true only after taking publication ownership and recording its result. fn try_early(&self, packet: &Arc) -> bool; fn count_backlog(&self, packet: &Packet); @@ -346,11 +348,11 @@ impl sched::JobImpl for Job { ) { return true; } - // Other work retiring means the GPU is shared, not hung. - if super::timeout::renew_shared( - job.packet.is_published(), super::recovery::since_retired(super::now_ns()), - &mut job.shared_renewals, - ) { + // Other work retiring means the GPU is shared, not hung. So does another long job + // running beside a long job, though neither retires until it ends. + let shared = super::timeout::retiring(super::recovery::since_retired(super::now_ns())) + || (job.packet.command.long_job() && job.backend.long_job_beside(&job.packet)); + if super::timeout::renew_shared(job.packet.is_published(), shared, &mut job.shared_renewals) { return true; } let deferred = job.backend.defer_timeout(&job.packet, job.victim_renewals); diff --git a/drivers/gpu/drm/asahi/g17/queue/compute.rs b/drivers/gpu/drm/asahi/g17/queue/compute.rs index 9c4c5c92d049d..23dc1200f3545 100644 --- a/drivers/gpu/drm/asahi/g17/queue/compute.rs +++ b/drivers/gpu/drm/asahi/g17/queue/compute.rs @@ -747,6 +747,16 @@ impl Queue { pub(crate) fn in_flight(&self) -> bool { self.active.len != 0 && !self.quarantined } + /// The oldest command is a published long job (ray traversal or AS build) whose + /// completion record the driver has not seen yet. + pub(crate) fn long_job_running(&self) -> bool { + !self.quarantined + && self.active.front().is_some_and(|active| { + active.observed.is_none() + && active.packet.is_published() + && active.packet.command.long_job() + }) + } /// Quarantines without a pending witness no longer describe live pipe order. pub(crate) fn timeout_head(&self) -> Option { if self.quarantined && !self.awaiting_witness { diff --git a/drivers/gpu/drm/asahi/g17/runtime/backend.rs b/drivers/gpu/drm/asahi/g17/runtime/backend.rs index d19f51c08dd29..bf0f989f042a5 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/backend.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/backend.rs @@ -297,6 +297,16 @@ impl job::Backend for Backend { } } + fn long_job_beside(&self, _packet: &Arc) -> bool { + let state = self.shared.state.lock(); + let Some(firmware) = (*state).as_deref() else { + return false; + }; + firmware.queues.compute.iter().flatten() + .filter_map(|entry| entry.queue.as_deref()) + .any(|other| other.owner() != Some(self.owner) && other.long_job_running()) + } + fn defer_timeout(&self, packet: &Arc, renewals: u8) -> bool { if packet.engine() == Engine::Compute { return self.defer_compute_timeout(packet, renewals); diff --git a/drivers/gpu/drm/asahi/g17/timeout.rs b/drivers/gpu/drm/asahi/g17/timeout.rs index 7b4ef4137b795..b83ea5d07f406 100644 --- a/drivers/gpu/drm/asahi/g17/timeout.rs +++ b/drivers/gpu/drm/asahi/g17/timeout.rs @@ -66,13 +66,18 @@ const SHARED_RENEWALS: u8 = 14; /// nothing for this long has stopped. pub(crate) const LIVENESS_NS: u64 = 5_000_000_000; -/// Renews a published job's deadline while some command retired within -/// [`LIVENESS_NS`] (`since_retired`), at most [`SHARED_RENEWALS`] times. -pub(crate) fn renew_shared(published: bool, since_retired: Option, renewals: &mut u8) -> bool { - if !published - || !since_retired.is_some_and(|since| since < LIVENESS_NS) - || *renewals >= SHARED_RENEWALS - { +/// Whether some command retired within [`LIVENESS_NS`] (`since_retired`). +pub(crate) fn retiring(since_retired: Option) -> bool { + since_retired.is_some_and(|since| since < LIVENESS_NS) +} + +/// Renews a published job's deadline while the GPU is `shared`, at most +/// [`SHARED_RENEWALS`] times. It is shared while some command retires +/// ([`retiring`]), or while a long job of another queue runs beside a long job: +/// two ray traversal dispatches of about 6 s each took 10 to 17 s side by side, and +/// neither retires anything until it ends. +pub(crate) fn renew_shared(published: bool, shared: bool, renewals: &mut u8) -> bool { + if !published || !shared || *renewals >= SHARED_RENEWALS { return false; } *renewals += 1; @@ -244,21 +249,34 @@ mod tests { fn a_live_gpu_renews_a_published_job_for_thirty_seconds() { let mut renewals = 0; for poll in 1..=14u8 { - assert!(renew_shared(true, Some(4_900_000_000), &mut renewals)); + assert!(renew_shared(true, retiring(Some(4_900_000_000)), &mut renewals)); assert_eq!(renewals, poll); } - assert!(!renew_shared(true, Some(0), &mut renewals)); + assert!(!renew_shared(true, true, &mut renewals)); assert_eq!(POLL_MS * (u32::from(renewals) + 1), 30_000); } #[test] fn a_stopped_gpu_or_unpublished_job_keeps_the_deadline() { let mut renewals = 0; - assert!(!renew_shared(true, Some(LIVENESS_NS), &mut renewals)); - assert!(!renew_shared(true, None, &mut renewals)); - assert!(!renew_shared(false, Some(0), &mut renewals)); + assert!(!renew_shared(true, retiring(Some(LIVENESS_NS)), &mut renewals)); + assert!(!renew_shared(true, retiring(None), &mut renewals)); + assert!(!renew_shared(false, true, &mut renewals)); assert_eq!(renewals, 0); } #[test] + fn a_long_job_beside_another_shares_the_gpu_without_retirements() { + // Two traversal dispatches side by side: nothing retires, yet both progress. + let mut renewals = 0; + let long_job_beside = true; + let shared = retiring(Some(LIVENESS_NS + 1)) || long_job_beside; + for poll in 1..=14u8 { + assert!(renew_shared(true, shared, &mut renewals)); + assert_eq!(renewals, poll); + } + // The thirty-second bound still ends a pair that never finishes. + assert!(!renew_shared(true, true, &mut renewals)); + } + #[test] fn exhausted_or_invalid_credit_never_wraps() { for value in 4..=u8::MAX { let mut renewals = value; From 347831e44afd4edc71850d47b89c6a22f9673d80 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Mon, 26 Oct 2026 09:44:36 -0400 Subject: [PATCH 213/388] drm/asahi: let a G17 render queue keep 32 commands in flight A logical render queue admitted 16 commands at a time, although its pair has command records (TA and fragment descriptors, configuration and timestamp slots) for 32. At depth the firmware runs the renders already announced when it starts a batch, then writes their completions one after another before it starts the next, so the window bounded every batch: at render depth 32 the median render ran in a batch of 8, at most 14, and the fixed cost of each batch edge fell on few renders. Admit as many commands as a pair has records; the scheduler's credits follow. At depth 32 and 64 the median render now runs in a batch of 11 and 15, at most 32. Each pair keeps a Generation per command in flight, 16 more of them, about 1.75 MiB more per render queue. A build-time check keeps the depth within the records. Measured on Neo in one boot, 16 against 32 commands, alternating, three rounds (us per submission, medians): render.light tri, depth 32 30.41 -> 29.50 render.light tri, depth 64 30.34 -> 29.38 Shallower depths, several queues or processes and the GPU time of each render are unchanged within run-to-run spread. --- drivers/gpu/drm/asahi/g17/queue/render/memory.rs | 2 ++ drivers/gpu/drm/asahi/hw/t8140.rs | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/queue/render/memory.rs b/drivers/gpu/drm/asahi/g17/queue/render/memory.rs index 756f96e3fd048..884537b99b6d8 100644 --- a/drivers/gpu/drm/asahi/g17/queue/render/memory.rs +++ b/drivers/gpu/drm/asahi/g17/queue/render/memory.rs @@ -528,6 +528,8 @@ impl Memory { } const _: () = { + // Every unretired render of a pair holds its own command record. + assert!(DEPTH <= RECORDS); assert!(abi::TA_DESCRIPTOR_SIZE * RECORDS <= TA_BYTES); assert!(abi::FRAGMENT_SLOT_SIZE * RECORDS <= FRAGMENT_BYTES); assert!(RECORDS * CONFIG_STRIDE <= 0x4000); diff --git a/drivers/gpu/drm/asahi/hw/t8140.rs b/drivers/gpu/drm/asahi/hw/t8140.rs index 75925713bf10b..47dc4d583a5fc 100644 --- a/drivers/gpu/drm/asahi/hw/t8140.rs +++ b/drivers/gpu/drm/asahi/hw/t8140.rs @@ -417,9 +417,10 @@ pub(crate) mod scheduling { pub(crate) mod queues { pub(crate) const RENDER_SLOTS: usize = 64; // One physical pair per logical owner leaves room for every execution - // context in the shared 128-QID namespace. Commands queue at RENDER_DEPTH. + // context in the shared 128-QID namespace. Commands queue at RENDER_DEPTH, + // as many as a pair has command records (the render memory's RECORDS). pub(crate) const RENDER_PAIRS_PER_OWNER: usize = 1; - pub(crate) const RENDER_DEPTH: u8 = 16; + pub(crate) const RENDER_DEPTH: u8 = 32; pub(crate) const COMPUTE_DEPTH: usize = 16; } From 8ee2bb20060eda7804910afb0bbf5a77f3a45680 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Bal=C3=A1zsy?= Date: Sun, 25 Oct 2026 17:13:26 +0100 Subject: [PATCH 214/388] dt-bindings: power: apple: add apple,irq-safe to the PMP report entries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit apple,irq-safe on a PMP report entry marks an entry that parents the IRQ-safe PMGR power states of a coprocessor: genpd requires the parent of an IRQ-safe subdomain to be IRQ safe as well. Signed-off-by: FrantiÅ¡ek Balázsy --- .../devicetree/bindings/power/apple,t8140-pmp-report.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Documentation/devicetree/bindings/power/apple,t8140-pmp-report.yaml b/Documentation/devicetree/bindings/power/apple,t8140-pmp-report.yaml index e8eac9e5411e3..e79ea331865b5 100644 --- a/Documentation/devicetree/bindings/power/apple,t8140-pmp-report.yaml +++ b/Documentation/devicetree/bindings/power/apple,t8140-pmp-report.yaml @@ -93,6 +93,13 @@ patternProperties: holds this entry's device ID, instead of by reg. Only valid on a report whose PMP the host starts. + apple,irq-safe: + type: boolean + description: + The entry is the parent of IRQ-safe PMGR power states and is + switched with interrupts disabled. Setting the state takes a + spinlock and polls the acknowledgment atomically. + required: - compatible - reg From c5d290f16dd278d19524ebdcdfec3de6dc3c87bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Bal=C3=A1zsy?= Date: Sun, 25 Oct 2026 01:25:45 +0200 Subject: [PATCH 215/388] pmdomain: apple: pmp-report: IRQ-safe entries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A PMP device-state entry that parents PMGR power states (which are IRQ safe) must be IRQ safe too, or genpd refuses the subdomain. Setting the state takes a spinlock and polls the acknowledgment atomically, so the entry may run with interrupts disabled. Opt in with apple,irq-safe. Tested on a MacBook Pro M4 Pro (J616s): the DISPEXT0 and DISPEXT1 device-state entries parent the dispext0/1 PMGR chains that power the HDMI and USB-C DisplayPort controllers. Signed-off-by: FrantiÅ¡ek Balázsy --- drivers/pmdomain/apple/pmp-report.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/drivers/pmdomain/apple/pmp-report.c b/drivers/pmdomain/apple/pmp-report.c index 823669e27ad74..aaa38c06daaaf 100644 --- a/drivers/pmdomain/apple/pmp-report.c +++ b/drivers/pmdomain/apple/pmp-report.c @@ -1473,6 +1473,13 @@ static int apple_pmp_report_entry_probe(struct platform_device *pdev) ent->genpd.power_on = apple_pmp_report_entry_power_on; ent->genpd.power_off = apple_pmp_report_entry_power_off; + /* + * An entry that is the parent of PMGR power states (which are IRQ safe) + * must be IRQ safe too. Setting the state takes a spinlock and polls + * the acknowledgment atomically, so it may be called with IRQs off. + */ + if (of_property_read_bool(node, "apple,irq-safe")) + ent->genpd.flags |= GENPD_FLAG_IRQ_SAFE; ret = pm_genpd_init(&ent->genpd, NULL, true); if (ret) { From 474fae2d8234780652534ac729689cb812b707b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Bal=C3=A1zsy?= Date: Mon, 26 Oct 2026 15:55:27 +0100 Subject: [PATCH 216/388] dt-bindings: display: apple,dcp: add apple,t6041-dcpext MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The M4 Pro (T6041) external display processors use the generic dcpext fallback like the other dcpext variants; the driver already matches apple,t6041-dcpext. Signed-off-by: FrantiÅ¡ek Balázsy --- Documentation/devicetree/bindings/display/apple,dcp.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/Documentation/devicetree/bindings/display/apple,dcp.yaml b/Documentation/devicetree/bindings/display/apple,dcp.yaml index d79ad457f7703..3d691822c50b1 100644 --- a/Documentation/devicetree/bindings/display/apple,dcp.yaml +++ b/Documentation/devicetree/bindings/display/apple,dcp.yaml @@ -33,6 +33,7 @@ properties: - apple,t8112-dcpext - apple,t6000-dcpext - apple,t6020-dcpext + - apple,t6041-dcpext - const: apple,dcpext - items: - const: apple,t8140-dcpext From 4ba86fff016009e8864eaad051683d83c2111800 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Bal=C3=A1zsy?= Date: Mon, 26 Oct 2026 15:52:02 +0100 Subject: [PATCH 217/388] arm64: dts: apple: t6040-j616s: external display chain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Describe the J616s external display chain, all disabled: dcpext0 behind the HDMI port (atc3 display crossbar, atcphy3, the DP-to-HDMI converter), dcpext1 behind the three Type-C ports (display fabric routes, atc0..2 crossbars), their DISPEXT power domains with the PMP acknowledgement entries, the display DARTs and mailboxes, and the dcpext firmware memory as disabled placeholders that the boot loader fills from the ADT. Nothing is enabled by the device tree: powering the DISPEXT chain without the PMP acknowledgement hangs the machine before root, and a boot loader that does not provide the firmware memory leaves the placeholders disabled. A gate enables the chain (next commit). Signed-off-by: FrantiÅ¡ek Balázsy --- .../dts/apple/t6040-j616s-extdisp-hdmi.dtsi | 308 ++++++++++++++++ .../apple/t6040-j616s-extdisp-mem-loader.dtsi | 72 ++++ .../dts/apple/t6040-j616s-extdisp-usbc.dtsi | 183 ++++++++++ .../boot/dts/apple/t6040-j616s-extdisp.dtsi | 345 ++++++++++++++++++ arch/arm64/boot/dts/apple/t6040-j616s.dts | 6 + 5 files changed, 914 insertions(+) create mode 100644 arch/arm64/boot/dts/apple/t6040-j616s-extdisp-hdmi.dtsi create mode 100644 arch/arm64/boot/dts/apple/t6040-j616s-extdisp-mem-loader.dtsi create mode 100644 arch/arm64/boot/dts/apple/t6040-j616s-extdisp-usbc.dtsi create mode 100644 arch/arm64/boot/dts/apple/t6040-j616s-extdisp.dtsi diff --git a/arch/arm64/boot/dts/apple/t6040-j616s-extdisp-hdmi.dtsi b/arch/arm64/boot/dts/apple/t6040-j616s-extdisp-hdmi.dtsi new file mode 100644 index 0000000000000..b414a0bddee7d --- /dev/null +++ b/arch/arm64/boot/dts/apple/t6040-j616s-extdisp-hdmi.dtsi @@ -0,0 +1,308 @@ +// SPDX-License-Identifier: GPL-2.0+ OR MIT +/* + * The fixed HDMI path of t6040-j616s-extdisp.dtsi (dcpext0 -> atc3 crossbar + * -> atcphy3 -> DP-to-HDMI converter) with every node left DISABLED. The + * nodes must not be enabled at boot in the device tree: powering the DISPEXT + * chain without the PMP acknowledgement hangs the machine before root. The + * enable gate (drivers/soc/apple/t6041-display-gate.c) switches them on in + * order once the PMP is bound. + * + * dispext0's DART is not in the display node's iommus: the gate appends it + * when it enables the chain, so the internal display's DART domain stays as + * it is without the external displays. + */ + +/* + * DISPEXT0 may only be powered once the PMP has acknowledged it in its + * device-state table (SOC-DEV-PS-REQ/ACK, PTD entries 480/488 on T6041): the + * soc-device record of DISPEXT0 has ID 18 (ADT iop-pmp-nub soc-device, ack + * flag set), so its request bit is 17. Without it, enabling the DISPEXT0 PMGR + * chain hung the machine before root. + * Same contract as the T6030 PMP device-state provider of + * aurora-silicon/linux#358, through the existing pmp-report entries here. + */ +&pmp_report { + pmp_dispext0: report@11 { + compatible = "apple,t6041-pmp-v2-report-entry", + "apple,t6000-pmp-v2-report-entry"; + reg = <0x11>; + label = "pmp-dispext0"; + #power-domain-cells = <0>; + /* parent of the IRQ-safe PMGR chain */ + apple,irq-safe; + status = "disabled"; + }; +}; + +&ps_dispext0_sys { + power-domains = <&pmp_dispext0>; + status = "disabled"; +}; + +&ps_dispext0_fe { + status = "disabled"; +}; + +&ps_dispext0_cpu { + /* + * NEVER enable: the ADT marks DISPEXT0_CPU no_ps (no PMGR power-state + * register, like the internal DISP_CPU). Its probe reads offset 0x350, + * and that read resets the SoC. DCPEXT0's own power gate (ADT power-gates 431) is the + * virtual DISPEXT0-CPU-V; the last real register of the chain is FE. + */ + /delete-property/ apple,always-on; + status = "disabled"; +}; + +&ps_atc3_common { + status = "disabled"; +}; + +&ps_atc3_common_dp { + status = "disabled"; +}; + +&ps_atc3_usb_aon { + status = "disabled"; +}; + +&ps_atc3_phymxwrap { + status = "disabled"; +}; + +&pmgr_mini { + /* ADT ATC3_USB: +0x148, parents ATC3_COMMON and ATC3_USB_AON; the crossbar's domain as on T6030 */ + ps_atc3_usb: power-controller@148 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x148 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc3_usb"; + power-domains = <&ps_atc3_common>, <&ps_atc3_usb_aon>; + status = "disabled"; + }; +}; + +&pinctrl_aop { + status = "disabled"; +}; + +&dcpext0_dart { + /* in the DISPEXT0 block: powered (after the PMP request) before probing */ + power-domains = <&ps_dispext0_fe>; + status = "disabled"; +}; + +&dispext0_dart { + /* in the DISPEXT0 block: powered (after the PMP request) before probing */ + power-domains = <&ps_dispext0_fe>; + status = "disabled"; +}; + +&dcpext0_mbox { + power-domains = <&ps_dispext0_fe>; + status = "disabled"; +}; + +&dcpext0 { + power-domains = <&ps_dispext0_fe>; + /* + * The IOMFB surface layout of the internal DCP; + * without apple,iomfb-surfaces apple-dcp's probe fails with -EINVAL. + */ + apple,iomfb-surfaces = <1 1 0 0>; + status = "disabled"; +}; + +&atcphy3_mbox { + status = "disabled"; +}; + +&atcphy3_xbar { + power-domains = <&ps_atc3_usb>; + status = "disabled"; +}; + +/* + * atcphy3 apply_tunables tables (aciophy_phy_init mode 3), from this machine's ADT /arm-io/atc-phy3 + * (privateartifacts/omarchy-m4-unit/adt.bin): AXI2AF converted entry by entry; every other table is + * byte-identical to atc-phy0 in the ADT, so the atcphy0 values of t6040-j616s-usb3.dtsi are reused + * (AUS40CMN_SHM differs: 0x50). Only the first six (axi2af..lane1) are applied in DP mode. + */ +&atcphy3 { + /* ATC3 ADT tunable_ATC0AXI2AF (port-specific), entries */ + apple,tunable-axi2af = + <0x0 0x800000ff 0x80000025>, + <0x4 0x80ff00ff 0x80250003>, + <0x8 0x80ff00ff 0x80250003>, + <0xc 0x80ff00ff 0x80250005>, + <0x10 0x80ff00ff 0x80250003>, + <0x14 0x80ff00ff 0x80250003>, + <0x18 0x80ff00ff 0x80250003>, + <0x1c 0x80ff00ff 0x80250003>, + <0x20 0x800000ff 0x8000001e>, + <0x24 0x800000ff 0x8000004a>, + <0x28 0x80ff00ff 0x804a0006>, + <0x2c 0x80ff00ff 0x804a0006>, + <0x30 0x80ff00ff 0x804a0006>, + <0x34 0x80ff00ff 0x804a0006>, + <0x38 0x80ff00ff 0x804a0006>, + <0x3c 0x80ff00ff 0x804a0006>, + <0x40 0x800000ff 0x8000001e>, + <0x44 0x800000ff 0x8000001e>, + <0x48 0x800000ff 0x8000001e>, + <0x100 0x000000ff 0x000000ff>, + <0x108 0xffff0006 0x00100000>, + <0x304 0x3f3f0000 0x04040000>, + <0x308 0x000fffff 0x00001681>, + <0x30c 0x000fffff 0x00001680>, + <0x310 0x70000fff 0x000001e0>, + <0x314 0xf0000fff 0x800001e0>, + <0x318 0x70000fff 0x000001e0>, + <0x31c 0xf0000fff 0x800001e0>, + <0x320 0x70000fff 0x000005a0>, + <0x324 0xf0000fff 0x800005a0>, + <0x328 0x70000fff 0x000005a0>, + <0x32c 0xf0000fff 0x800005a0>, + <0x330 0x70000fff 0x000005a0>, + <0x334 0xf0000fff 0x800005a0>, + <0x338 0x70000fff 0x000005a0>, + <0x33c 0x70000fff 0x000005a0>, + <0x340 0x70000fff 0x000005a0>, + <0x344 0x70000fff 0x000005a0>, + <0x348 0x70000fff 0x000005a0>, + <0x34c 0x70000fff 0x000005a0>, + <0x350 0x70000fff 0x000005a0>, + <0x358 0xffffffff 0x0000ffff>, + <0x35c 0xffffffff 0x0000ffff>, + <0x408 0xffff0020 0x00210020>, + <0x40c 0x000fffff 0x00001681>, + <0x410 0x000fffff 0x00001680>, + <0x414 0x70000fff 0x000001e0>, + <0x418 0x70000fff 0x000001e0>, + <0x41c 0x70000fff 0x000001e0>, + <0x420 0x70000fff 0x000001e0>, + <0x424 0x70000fff 0x000005a0>, + <0x428 0x70000fff 0x000005a0>, + <0x42c 0x70000fff 0x000005a0>, + <0x430 0x70000fff 0x000005a0>, + <0x434 0x70000fff 0x000005a0>, + <0x438 0x70000fff 0x000005a0>, + <0x43c 0x70000fff 0x000005a0>, + <0x440 0x70000fff 0x000005a0>, + <0x444 0x70000fff 0x000005a0>, + <0x448 0x70000fff 0x000005a0>, + <0x44c 0x70000fff 0x000005a0>, + <0x450 0x70000fff 0x000005a0>, + <0x454 0x70000fff 0x000005a0>, + <0x948 0x07ff07ff 0x00280000>, + <0xa48 0x07ff07ff 0x00400040>, + <0xb48 0x07ff07ff 0x00b8010e>, + <0xc48 0x07ff07ff 0x00400020>, + <0xd48 0x07ff07ff 0x008a008a>, + <0x1000 0xffffffff 0x7fffffff>, + <0x4008 0x00000001 0x00000001>, + <0x400c 0x00000001 0x00000001>, + <0x4014 0x00000001 0x00000001>, + <0x4018 0x00000001 0x00000000>, + <0x401c 0x00000001 0x00000001>; + /* the remaining tables equal atc-phy0 in the ADT, except AUS40CMN_SHM (0x50) */ + apple,tunable-fabric = + <0x4 0x003f003f 0x00200020>, + <0xc 0x000000f1 0x000000a1>, + <0x10 0x0000801f 0x00000004>, + <0x18 0x003f003f 0x00200020>, + <0x20 0x000000f1 0x000000a1>, + <0x24 0x0000801f 0x00000004>, + <0x2c 0x00ff00ff 0x00960096>, + <0x34 0x000000f1 0x00000071>, + <0x38 0x0000801f 0x00000004>, + <0x40 0x0000003f 0x00000020>, + <0x48 0x0000801f 0x00000004>, + <0x50 0x007f007f 0x00450045>, + <0x58 0x0000801f 0x00000004>, + <0x60 0x0000003f 0x00000020>, + <0x68 0x0000801f 0x00000004>, + <0x70 0x00000001 0x00000001>, + <0x74 0x0000801f 0x00000004>, + <0x7c 0x007f007f 0x00400040>, + <0x80 0x000000f1 0x00000011>, + <0x84 0x00000001 0x00000001>, + <0x88 0x00000001 0x00000001>, + <0x8c 0x0000801f 0x00000004>, + <0x94 0x001f001f 0x00100010>, + <0x98 0x000000f1 0x00000011>, + <0x9c 0x0000801f 0x00000004>, + <0xa4 0x000000ff 0x000000e0>, + <0xa8 0x0000801f 0x00000004>, + <0xb0 0x0000003f 0x00000020>, + <0xb8 0x0000801f 0x00000004>, + <0xc0 0x0000003f 0x00000020>, + <0xc8 0x0000801f 0x00000004>, + <0xd0 0x001f001f 0x00100010>, + <0xd8 0x00000001 0x00000001>, + <0xdc 0x00000001 0x00000001>, + <0xe0 0x0000801f 0x00000004>, + <0xe8 0x0000000f 0x00000008>, + <0xec 0x0000801f 0x00000004>, + <0xf4 0xffffffff 0x01010101>, + <0xf8 0x0000801f 0x00000004>, + <0x100 0x00000001 0x00000001>, + <0x104 0x00000001 0x00000001>, + <0x108 0x0000801f 0x00000004>, + <0x110 0x0000801f 0x00000004>, + <0x118 0x0000801f 0x00000004>, + <0x120 0x00000001 0x00000001>, + <0x124 0x00000001 0x00000001>, + <0x128 0x0000801f 0x00000004>, + <0x130 0xffffffff 0x01010101>, + <0x134 0x0000801f 0x00000004>, + <0x13c 0x00000001 0x00000001>, + <0x140 0x00000001 0x00000001>, + <0x144 0x0000801f 0x00000004>, + <0x14c 0x00000001 0x00000001>, + <0x150 0x00000001 0x00000001>, + <0x154 0x0000801f 0x00000004>, + <0x15c 0x0000801f 0x00000004>, + <0x164 0xffffffff 0x01010101>, + <0x168 0x0000ffff 0x00000101>, + <0x16c 0x0000801f 0x00000004>, + <0x174 0x00000001 0x00000001>, + <0x178 0x00000001 0x00000001>, + <0x17c 0x0000801f 0x00000004>, + <0x184 0x00000001 0x00000001>, + <0x188 0x00000001 0x00000001>, + <0x18c 0x0000801f 0x00000004>; + apple,tunable-common-cfg = + <0x8 0x00000001 0x00000001>; + apple,tunable-axi2af-lioa = + <0x28 0x0000fe2f 0x00000000>, + <0x2c 0x00000007 0x00000000>, + <0x30 0x0000007f 0x00000000>, + <0x34 0x000000ff 0x00000000>, + <0x58 0x0000010b 0x00000000>; + apple,tunable-lane0 = + <0xd094 0x00080000 0x00080000>, + <0xd098 0x00080000 0x00080000>, + <0xd0a4 0x00800000 0x00800000>, + <0xd0a8 0x00200000 0x00200000>; + apple,tunable-lane1 = + <0x1d094 0x00080000 0x00080000>, + <0x1d098 0x00080000 0x00080000>, + <0x1d0a4 0x00800000 0x00800000>, + <0x1d0a8 0x00200000 0x00200000>; + apple,tunable-aus40cmn-shm = + <0xa04 0x0000007c 0x00000050>; + apple,tunable-auspll = + <0x3224 0x00000c00 0x00000800>, + <0x3230 0x0000001f 0x0000000f>, + <0x3240 0x07d8007f 0x04000059>, + <0x32b0 0x0b800000 0x01800000>, + <0x32e8 0x00060001 0x00040001>, + <0x3338 0x0f800000 0x07800000>; + apple,tunable-cio4pll = + <0x3a30 0x0000001f 0x0000000f>, + <0x3a4c 0x0000fe7f 0x0000501e>, + <0x3ab0 0x00700000 0x00300000>, + <0x3ae8 0x00060000 0x00020000>; +}; diff --git a/arch/arm64/boot/dts/apple/t6040-j616s-extdisp-mem-loader.dtsi b/arch/arm64/boot/dts/apple/t6040-j616s-extdisp-mem-loader.dtsi new file mode 100644 index 0000000000000..08d4768494634 --- /dev/null +++ b/arch/arm64/boot/dts/apple/t6040-j616s-extdisp-mem-loader.dtsi @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-2.0+ OR MIT +/* + * The firmware memory of the external DCPs, for a boot loader that fills it + * (m4-loader m4_dcpext_fixup()). + * + * The loader reads /arm-io/dcpextN/iop-dcpextN-nub segment-ranges of the + * booting Mac (__TEXT, __DATA and the heap; __OS_LOG is left out as for the + * internal DCP), gives each placeholder below its reg, iommu-addresses and + * name, enables it, names the regions in the node's memory-region-names and + * sets apple,firmware-version/compat. A placeholder it does not fill is + * deleted together with the node's memory-region. A boot loader without this + * fix-up leaves them disabled, and the enable gate then keeps the external + * displays off. + * + * Nothing here enables dcpext0/1 or their DART/mailbox. + */ + +/ { + reserved-memory { + dcpext0_text: dcpext0-text { + compatible = "apple,asc-mem"; + reg = <0x0 0x0 0x0 0x4000>; + no-map; + status = "disabled"; + }; + + dcpext0_data: dcpext0-data { + compatible = "apple,asc-mem"; + reg = <0x0 0x0 0x0 0x4000>; + no-map; + status = "disabled"; + }; + + dcpext0_heap: dcpext0-heap { + compatible = "apple,asc-mem"; + reg = <0x0 0x0 0x0 0x4000>; + no-map; + status = "disabled"; + }; + + dcpext1_text: dcpext1-text { + compatible = "apple,asc-mem"; + reg = <0x0 0x0 0x0 0x4000>; + no-map; + status = "disabled"; + }; + + dcpext1_data: dcpext1-data { + compatible = "apple,asc-mem"; + reg = <0x0 0x0 0x0 0x4000>; + no-map; + status = "disabled"; + }; + + dcpext1_heap: dcpext1-heap { + compatible = "apple,asc-mem"; + reg = <0x0 0x0 0x0 0x4000>; + no-map; + status = "disabled"; + }; + }; +}; + +&dcpext0 { + memory-region = <&dcpext0_text>, <&dcpext0_data>, <&dcpext0_heap>; + memory-region-names = "text", "data", "heap"; +}; + +&dcpext1 { + memory-region = <&dcpext1_text>, <&dcpext1_data>, <&dcpext1_heap>; + memory-region-names = "text", "data", "heap"; +}; diff --git a/arch/arm64/boot/dts/apple/t6040-j616s-extdisp-usbc.dtsi b/arch/arm64/boot/dts/apple/t6040-j616s-extdisp-usbc.dtsi new file mode 100644 index 0000000000000..493dcbd3a28a0 --- /dev/null +++ b/arch/arm64/boot/dts/apple/t6040-j616s-extdisp-usbc.dtsi @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: GPL-2.0+ OR MIT +/* + * USB-C DisplayPort: dcpext1 over any of the three Type-C ports. As for + * HDMI, the DISPEXT1 chain and dcpext1 stay DISABLED in the DTB and are + * enabled by the enable gate (drivers/soc/apple/t6041-display-gate.c). The + * routes go through the display fabric (typec-routes, end of this file): + * tipd's mode switch reaches dcpext1, which claims the port. Boot-time + * changes: mode-switch on atcphy0..2 (the PHY registers a Type-C mode + * switch; tipd drives it) and the port@3 route endpoints of the connectors. + */ + +/* PMP device-state acknowledgement for DISPEXT1 (soc-device 19, request bit 18), as DISPEXT0. */ +&pmp_report { + pmp_dispext1: report@12 { + compatible = "apple,t6041-pmp-v2-report-entry", + "apple,t6000-pmp-v2-report-entry"; + reg = <0x12>; + label = "pmp-dispext1"; + #power-domain-cells = <0>; + apple,irq-safe; + status = "disabled"; + }; +}; + +/* + * DISPEXT1_SYS sits on FAB7_SOC (ADT pmgr device id2 134, pmgr_aux 0x488, parent + * FAB6_SOC, off at boot), which t6040-pmgr.dtsi does not describe. m4.62 enabled + * DISPEXT1_SYS without it and the first DCPEXT1 register access (its DART) reset + * the SoC. DISPEXT0_SYS's parent FAB2_SOC is on anyway through ATC0..2_COMMON. + */ +&pmgr_aux { + ps_fab7_soc: power-controller@488 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x488 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "fab7_soc"; + power-domains = <&ps_fab6_soc>; + status = "disabled"; + }; +}; + +&ps_dispext1_sys { + power-domains = <&ps_fab7_soc>, <&pmp_dispext1>; + status = "disabled"; +}; + +&ps_dispext1_fe { + status = "disabled"; +}; + +&ps_dispext1_cpu { + /* NEVER enable: ADT no_ps, as DISPEXT0_CPU (its register read resets the SoC). */ + /delete-property/ apple,always-on; + status = "disabled"; +}; + +/* The per-ATC crossbars of the Type-C ports: the ATC USB domain, as atcphy3's. */ +&atcphy0_xbar { + power-domains = <&ps_atc0_usb>; + status = "disabled"; +}; + +&atcphy1_xbar { + power-domains = <&ps_atc1_usb>; + status = "disabled"; +}; + +&atcphy2_xbar { + power-domains = <&ps_atc2_usb>; + status = "disabled"; +}; + +/* tipd switches the PHY into DP alt mode through the connector graph. */ +&atcphy0 { + mode-switch; +}; + +&atcphy1 { + mode-switch; +}; + +&atcphy2 { + mode-switch; +}; + +/* + * Type-C display routes through the display fabric, as T6030 and J700: + * dcpext1 offers one route per port, each a mode switch the port controller + * drives over the connector graph (port@3). The gate enables the routes + * before devices exist, so the port controllers wait for dcpext1's mode + * switches, and probes the controllers again once dcpext1 is bound. A route + * without a registered mode switch would make the port wait for one forever. + */ +&typec0 { + ports { + port@3 { + reg = <3>; + + typec0_dcpext1: endpoint { + remote-endpoint = <&dcpext1_typec0>; + }; + }; + }; +}; + +&typec1 { + ports { + port@3 { + reg = <3>; + + typec1_dcpext1: endpoint { + remote-endpoint = <&dcpext1_typec1>; + }; + }; + }; +}; + +&typec2 { + ports { + port@3 { + reg = <3>; + + typec2_dcpext1: endpoint { + remote-endpoint = <&dcpext1_typec2>; + }; + }; + }; +}; + +&dcpext1 { + apple,connector-type = "USB-C"; + /* as dcpext0: without it apple-dcp's probe fails with -EINVAL (m4.63) */ + apple,iomfb-surfaces = <1 1 0 0>; + phys = <&atcphy0 PHY_TYPE_DP>, <&atcphy1 PHY_TYPE_DP>, <&atcphy2 PHY_TYPE_DP>; + phy-names = "typec0", "typec1", "typec2"; + mux-controls = <&atcphy0_xbar 0>, <&atcphy1_xbar 0>, <&atcphy2_xbar 0>; + mux-control-names = "typec0", "typec1", "typec2"; + /* ADT: atcN-dpphy dp-switch-dfp-endpoint N; dcpext1 ufp 1 port 0 = crossbar state 2 */ + apple,typec-dptx-phys = <0 1 2>; + apple,typec-mux-indices = <2 2 2>; + + typec-routes { + #address-cells = <1>; + #size-cells = <0>; + + route@0 { + reg = <0>; + mode-switch; + status = "disabled"; + + port { + dcpext1_typec0: endpoint { + remote-endpoint = <&typec0_dcpext1>; + }; + }; + }; + + route@1 { + reg = <1>; + mode-switch; + status = "disabled"; + + port { + dcpext1_typec1: endpoint { + remote-endpoint = <&typec1_dcpext1>; + }; + }; + }; + + route@2 { + reg = <2>; + mode-switch; + status = "disabled"; + + port { + dcpext1_typec2: endpoint { + remote-endpoint = <&typec2_dcpext1>; + }; + }; + }; + }; +}; diff --git a/arch/arm64/boot/dts/apple/t6040-j616s-extdisp.dtsi b/arch/arm64/boot/dts/apple/t6040-j616s-extdisp.dtsi new file mode 100644 index 0000000000000..affbad9ba51ad --- /dev/null +++ b/arch/arm64/boot/dts/apple/t6040-j616s-extdisp.dtsi @@ -0,0 +1,345 @@ +// SPDX-License-Identifier: GPL-2.0+ OR MIT +/* + * External display resources of the J616s. Everything here is disabled in + * the device tree: the enable gate (drivers/soc/apple/t6041-display-gate.c) + * switches the chain on once the boot loader has provided the firmware + * memory of the external display processors, so without that the booted + * hardware state is the same as without this file. + * + * Values come from this unit's ADT (25G83); CPU address = ADT address + + * 0x2_0000_0000. Path of the picture: + * HDMI: dcpext0 -> atc3 display crossbar -> atcphy3 (fixed DP PHY, no + * USB controller) -> on-board DP-to-HDMI converter + * USB-C: dcpext1 -> atcN display crossbar -> atcphyN (DP alt mode) + * + * Needs the DP mode of the T6040 Type-C PHY driver (phy-apple-t6040-usb2) + * with its probe path for a PHY without a USB controller (atcphy3), the + * atc-dpxbar,t604x display crossbar driver and the dcpext firmware memory + * (dcp-map style memory-region with iommu-addresses, t6040-j616s-extdisp- + * mem-loader.dtsi). + */ + +/ { + pinctrl_aop: pinctrl@510824000 { + compatible = "apple,t6041-pinctrl", "apple,t8103-pinctrl"; + reg = <0x5 0x10824000 0x0 0x4000>; + + gpio-controller; + #gpio-cells = <2>; + gpio-ranges = <&pinctrl_aop 0 0 85>; + apple,npins = <85>; + + interrupt-controller; + #interrupt-cells = <2>; + interrupt-parent = <&aic>; + interrupts = , + , + , + , + , + , + ; + status = "disabled"; + }; + + dispext0_dart: iommu@3a2300000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x3 0xa2300000 0x0 0xc000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + status = "disabled"; + }; + + dcpext0_dart: iommu@3a2340000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x3 0xa2340000 0x0 0x20000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + status = "disabled"; + }; + + dcpext0: dcp@3a2e00000 { + compatible = "apple,t6041-dcpext", "apple,dcpext"; + reg = <0x3 0xa2e00000 0x0 0x4000>, + <0x3 0xa0000000 0x0 0x4000000>, + <0x3 0xa2240000 0x0 0x10000>, + <0x3 0xa23a8000 0x0 0x4000>, + <0x3 0xa2800000 0x0 0x800000>; + reg-names = "coproc", "disp-0", "disp-1", "disp-2", "disp-3"; + mboxes = <&dcpext0_mbox>; + mbox-names = "mbox"; + iommus = <&dcpext0_dart 23>; + /* guessed: the internal DCP uses 0x20000, J700 DISPEXT0 is +0x8000 */ + apple,bw-scratch = <&pmgr_dcp 0 4 0x28000>; + status = "disabled"; + + dispext0_piodma: piodma { + iommus = <&dispext0_dart 16>; + }; + }; + + dcpext0_mbox: mailbox@3a2e08000 { + compatible = "apple,t6041-asc-mailbox", "apple,asc-mailbox-v4"; + reg = <0x3 0xa2e08000 0x0 0x4000>; + interrupt-parent = <&aic>; + interrupts = , + , + , + ; + interrupt-names = "send-empty", "send-not-empty", + "recv-empty", "recv-not-empty"; + #mbox-cells = <0>; + status = "disabled"; + }; + + dispext1_dart: iommu@602300000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x6 0x02300000 0x0 0xc000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + status = "disabled"; + }; + + dcpext1_dart: iommu@602340000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x6 0x02340000 0x0 0xc000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + status = "disabled"; + }; + + dcpext1: dcp@602e00000 { + compatible = "apple,t6041-dcpext", "apple,dcpext"; + reg = <0x6 0x02e00000 0x0 0x4000>, + <0x6 0x00000000 0x0 0x4000000>, + <0x6 0x02240000 0x0 0x10000>, + <0x6 0x023a8000 0x0 0x4000>, + <0x6 0x02800000 0x0 0x800000>; + reg-names = "coproc", "disp-0", "disp-1", "disp-2", "disp-3"; + mboxes = <&dcpext1_mbox>; + mbox-names = "mbox"; + iommus = <&dcpext1_dart 23>; + /* guessed, as dcpext0 */ + apple,bw-scratch = <&pmgr_dcp 0 4 0x30000>; + status = "disabled"; + + dispext1_piodma: piodma { + iommus = <&dispext1_dart 16>; + }; + }; + + dcpext1_mbox: mailbox@602e08000 { + compatible = "apple,t6041-asc-mailbox", "apple,asc-mailbox-v4"; + reg = <0x6 0x02e08000 0x0 0x4000>; + interrupt-parent = <&aic>; + interrupts = , + , + , + ; + interrupt-names = "send-empty", "send-not-empty", + "recv-empty", "recv-not-empty"; + #mbox-cells = <0>; + status = "disabled"; + }; + + /* ATC3: the HDMI converter's fixed DP PHY; no USB controller, no pipehandler. */ + atcphy3: phy@39aa90000 { + compatible = "apple,t6040-atcphy"; + reg = <0x3 0x9aa90000 0x0 0x4000>, + <0x3 0x9a800000 0x0 0x4000>, + <0x3 0x9b000000 0x0 0x30000>, + <0x3 0x9b200000 0x0 0x4000>, + <0x3 0x9b140000 0x0 0x4000>, + <0x3 0x9b0c0000 0x0 0x10000>, + <0x3 0x98000000 0x0 0x8000>, + <0x3 0x98020000 0x0 0x4000>, + <0x3 0x9b244000 0x0 0x4000>, + <0x3 0x9b250000 0x0 0x4000>; + reg-names = "bank0", "bank1", "core", "cfg", "cpu", "cpu-data", + "axi2af", "axi2af-lioa", "fabric", "common-cfg"; + #phy-cells = <1>; + power-domains = <&ps_atc3_common_dp>; + mboxes = <&atcphy3_mbox>; + mbox-names = "phy-cpu"; + nonposted-mmio; + status = "disabled"; + }; + + atcphy3_mbox: mailbox@39b148000 { + compatible = "apple,t6041-asc-mailbox", "apple,asc-mailbox-v4"; + reg = <0x3 0x9b148000 0x0 0x4000>; + interrupt-parent = <&aic>; + interrupts = , + , + , + ; + interrupt-names = "send-empty", "send-not-empty", + "recv-empty", "recv-not-empty"; + #mbox-cells = <0>; + power-domains = <&ps_atc3_phymxwrap>; + status = "disabled"; + }; + + /* Display crossbars (ADT atcN-dpxbar, atc-dpxbar,t604x): no driver yet. */ + atcphy0_xbar: mux@38324c000 { + compatible = "apple,t6041-display-crossbar"; + reg = <0x3 0x8324c000 0x0 0x4000>; + #mux-control-cells = <1>; + status = "disabled"; + }; + + atcphy1_xbar: mux@38b24c000 { + compatible = "apple,t6041-display-crossbar"; + reg = <0x3 0x8b24c000 0x0 0x4000>; + #mux-control-cells = <1>; + status = "disabled"; + }; + + atcphy2_xbar: mux@39324c000 { + compatible = "apple,t6041-display-crossbar"; + reg = <0x3 0x9324c000 0x0 0x4000>; + #mux-control-cells = <1>; + status = "disabled"; + }; + + atcphy3_xbar: mux@39b24c000 { + compatible = "apple,t6041-display-crossbar"; + reg = <0x3 0x9b24c000 0x0 0x4000>; + #mux-control-cells = <1>; + status = "disabled"; + }; +}; + +/* + * PMGR domains from the ADT device table (+16 offset, +19 block; block 0 = + * pmgr_aux, block 3 = pmgr_mini; checked against the ATC0 nodes). Disabled + * so the pwrstate driver does not touch them yet: DISPEXT0/1 run the + * firmware iBoot loaded, and an always-on domain that happens to be off + * would be powered on by the driver. + */ +&pmgr_aux { + ps_dispext0_sys: power-controller@340 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x340 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dispext0_sys"; + status = "disabled"; + }; + + ps_dispext0_fe: power-controller@348 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x348 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dispext0_fe"; + power-domains = <&ps_dispext0_sys>; + status = "disabled"; + }; + + ps_dispext0_cpu: power-controller@350 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x350 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dispext0_cpu"; + power-domains = <&ps_dispext0_fe>; + apple,always-on; + status = "disabled"; + }; + + ps_atc3_common: power-controller@408 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x408 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc3_common"; + apple,always-on; + status = "disabled"; + }; + + ps_dispext1_sys: power-controller@490 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x490 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dispext1_sys"; + status = "disabled"; + }; + + ps_dispext1_fe: power-controller@498 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x498 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dispext1_fe"; + power-domains = <&ps_dispext1_sys>; + status = "disabled"; + }; + + ps_dispext1_cpu: power-controller@4a0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x4a0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dispext1_cpu"; + power-domains = <&ps_dispext1_fe>; + apple,always-on; + status = "disabled"; + }; +}; + +&pmgr_mini { + ps_atc3_common_dp: power-controller@b8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0xb8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc3_common_dp"; + power-domains = <&ps_atc3_phymxwrap>; + status = "disabled"; + }; + + ps_atc3_usb_aon: power-controller@e0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0xe0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc3_usb_aon"; + apple,always-on; + status = "disabled"; + }; + + ps_atc3_phymxwrap: power-controller@168 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x168 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc3_phymxwrap"; + power-domains = <&ps_atc3_common>, <&ps_atc3_usb_aon>; + status = "disabled"; + }; +}; + +/* + * HDMI port (disabled with dcpext0): converter power SMC gP0f/gP1a (as on + * the M3 Pro J514s), HPD on AOP GPIO 0x24. The converter's reset (gP10), + * force-dpfu (AOP GPIO 0x2a) and its I2C side channel (i2c8 0x08) are left + * undescribed, as on the J514s. + */ +&dcpext0 { + apple,connector-type = "HDMI-A"; + hdmi-hpd-gpios = <&pinctrl_aop 36 GPIO_ACTIVE_HIGH>; + hdmi-pwren-gpios = <&smc_gpio 15 GPIO_ACTIVE_HIGH>; + dp2hdmi-pwren-gpios = <&smc_gpio 26 GPIO_ACTIVE_HIGH>; + phys = <&atcphy3 PHY_TYPE_DP>; + phy-names = "dp-phy"; + mux-controls = <&atcphy3_xbar 0>; + mux-control-names = "dp-xbar"; + mux-index = <0>; + apple,dptx-phy = <3>; +}; diff --git a/arch/arm64/boot/dts/apple/t6040-j616s.dts b/arch/arm64/boot/dts/apple/t6040-j616s.dts index a40a3fa1e1dc0..bccb167340159 100644 --- a/arch/arm64/boot/dts/apple/t6040-j616s.dts +++ b/arch/arm64/boot/dts/apple/t6040-j616s.dts @@ -1086,3 +1086,9 @@ }; #include "t6040-j616s-usb3.dtsi" + +/* External display chain, all disabled: the gate in drivers/soc/apple/t6041-display-gate.c enables it. */ +#include "t6040-j616s-extdisp.dtsi" +#include "t6040-j616s-extdisp-mem-loader.dtsi" +#include "t6040-j616s-extdisp-hdmi.dtsi" +#include "t6040-j616s-extdisp-usbc.dtsi" From aea83d29bbcc31a7f0fcf9ca1a76e0b27e65f012 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Bal=C3=A1zsy?= Date: Mon, 26 Oct 2026 15:52:41 +0100 Subject: [PATCH 218/388] soc: apple: t6041-display-gate: power up the J616s external displays MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend the T6041 display gate to the external displays. When both external display processors have their firmware memory (the memory-region nodes of the dcpext are available: the boot loader filled the placeholders), enable the chain after the PMP firmware is bound: the Type-C routes before any device exists (so the port controllers wait for dcpext1's mode switches) with the display subsystem held back, then the HDMI chain, the Type-C chain, and last both processors and the display subsystem together, so the display master is created once with every DART in its iommus. The port controllers are probed again once dcpext1 is bound, as the Type-C core treats a mode switch below a disabled node as absent at their first probe. If the PMP does not come within 20 s, or a step fails, the routes are withdrawn and only the internal panel stays. The processors are enabled only once the infrastructure devices have their drivers (the crossbar and PHY are modules), as a processor that finds one missing is given up for good. Without the firmware memory, with nomodeset, or on another machine, nothing changes. Tested on a J616s with m4-loader rev19-j616s-1 (which fills the memory from the ADT): HDMI and USB-C DisplayPort from boot, eDP unchanged. Signed-off-by: FrantiÅ¡ek Balázsy --- drivers/soc/apple/Kconfig | 6 +- drivers/soc/apple/t6041-display-gate.c | 454 +++++++++++++++++++++++++ 2 files changed, 458 insertions(+), 2 deletions(-) diff --git a/drivers/soc/apple/Kconfig b/drivers/soc/apple/Kconfig index 9033a75140946..cbd8a0f0d430a 100644 --- a/drivers/soc/apple/Kconfig +++ b/drivers/soc/apple/Kconfig @@ -269,8 +269,10 @@ config APPLE_T6041_DISPLAY_GATE boot loader has handed the running display coprocessor over. This option enables those nodes before devices are created, only when the boot loader has published a complete T6041 handoff for the - 26.6.2 display firmware. Without it, or with nomodeset, it does - nothing. + 26.6.2 display firmware. It also powers up the J616s external + display chain (HDMI, Type-C DisplayPort) once the boot loader has + given the external display processors their firmware memory. + Without these, or with nomodeset, it does nothing. Say 'y' if you want the native display on M4 Pro MacBook Pros. diff --git a/drivers/soc/apple/t6041-display-gate.c b/drivers/soc/apple/t6041-display-gate.c index aa088ddb957de..eaa711247399c 100644 --- a/drivers/soc/apple/t6041-display-gate.c +++ b/drivers/soc/apple/t6041-display-gate.c @@ -156,3 +156,457 @@ static int __init apple_t6041_display_gate(void) /* Runs before of_platform_default_populate_init() at arch_initcall_sync. */ arch_initcall(apple_t6041_display_gate); + +/* + * External displays (HDMI behind dcpext0, Type-C behind dcpext1). The + * device tree describes the whole chain disabled; it is enabled here once the + * boot loader has given each external display processor its firmware memory + * (the memory-region nodes of the dcpext are available, or they stay disabled + * placeholders). Before any device exists only the Type-C routes are enabled + * (the port controllers then wait for dcpext1's mode switches) and the + * display subsystem is held back. The power-up waits for the PMP firmware, + * whose acknowledgement the DISPEXT domains need: HDMI chain, Type-C chain, + * then both processors and the display subsystem together, so the display + * master is created once with every DART in its iommus. + */ +#include +#include +#include +#include +#include +#include +#include + +/* + * Two stages, each with its own display DART (a DART still disabled in the + * display subsystem's iommus would make of_iommu drop its whole fwspec): the + * HDMI chain, then the Type-C chain. Run at the same time the two processors + * crashed (dcpext0: axi_rd_err), as they never did one after the other. + * Parents first; the genpd links in the tree do the real ordering. + */ +struct gate_stage { + const char *dart; /* display DART of this stage */ + const char *const *nodes; + unsigned int nr; +}; + +static const char *const gate_hdmi_nodes[] = { + /* ATC3 power, PMP DISPEXT0 request, DISPEXT0, AOP pins, PHY, crossbar, DARTs, mailbox, dcpext0 */ + "/power-management@502800000/power-controller@408", + "/power-management@508300000/power-controller@e0", + "/power-management@508300000/power-controller@168", + "/power-management@508300000/power-controller@b8", + "/power-management@508300000/power-controller@148", + "/pmp-report@5003c0000/report@11", + "/power-management@502800000/power-controller@340", + "/power-management@502800000/power-controller@348", + "/pinctrl@510824000", + "/mailbox@39b148000", + "/phy@39aa90000", + "/mux@39b24c000", + "/iommu@3a2340000", + "/iommu@3a2300000", + "/mailbox@3a2e08000", + "/dcp@3a2e00000", +}; + +static const char *const gate_usbc_nodes[] = { + /* PMP DISPEXT1 request, FAB7_SOC, DISPEXT1, crossbars, DARTs, mailbox, routes, dcpext1 */ + "/pmp-report@5003c0000/report@12", + "/power-management@502800000/power-controller@488", + "/power-management@502800000/power-controller@490", + "/power-management@502800000/power-controller@498", + "/mux@38324c000", + "/mux@38b24c000", + "/mux@39324c000", + "/iommu@602340000", + "/iommu@602300000", + "/mailbox@602e08000", + "/dcp@602e00000/typec-routes/route@0", + "/dcp@602e00000/typec-routes/route@1", + "/dcp@602e00000/typec-routes/route@2", + "/dcp@602e00000", +}; + +static const struct gate_stage gate_stages[] = { + { "/iommu@3a2300000", gate_hdmi_nodes, ARRAY_SIZE(gate_hdmi_nodes) }, + { "/iommu@602300000", gate_usbc_nodes, ARRAY_SIZE(gate_usbc_nodes) }, +}; + +/* A display processor (a direct child of the root) and a Type-C display route. */ +static bool gate_is_dcp(const char *path) +{ + return !strncmp(path, "/dcp@", 5) && !strchr(path + 5, '/'); +} + +static bool gate_is_route(const char *path) +{ + return strstr(path, "/typec-routes/"); +} + +static int gate_display_add_dart(struct of_changeset *cs, struct device_node *display, + struct device_node *dart) +{ + struct property *old, *prop; + __be32 *value; + + old = of_find_property(display, "iommus", NULL); + if (!old || !dart->phandle || old->length % (2 * sizeof(*value))) + return -EINVAL; + prop = kzalloc(sizeof(*prop), GFP_KERNEL); + value = kmalloc(old->length + 2 * sizeof(*value), GFP_KERNEL); + if (!prop || !value) { + kfree(prop); + kfree(value); + return -ENOMEM; + } + memcpy(value, old->value, old->length); + value[old->length / sizeof(*value)] = cpu_to_be32(dart->phandle); + value[old->length / sizeof(*value) + 1] = cpu_to_be32(0); + prop->name = kstrdup("iommus", GFP_KERNEL); + prop->length = old->length + 2 * sizeof(*value); + prop->value = value; + if (!prop->name) { + kfree(value); + kfree(prop); + return -ENOMEM; + } + return of_changeset_update_property(cs, display, prop); +} + +/* + * The processor's firmware memory must be described: every memory-region of + * the dcpext node (the stage's last node) must be an available reserved-memory + * node. A loader that fills the regions from the ADT (m4-loader + * m4_dcpext_fixup) leaves disabled placeholders when it refuses, and an older + * loader leaves them all disabled; enabling the processor then would only + * make its driver fail. /chosen m4,dcpext is that loader's record (0 filled, + * 2 static memory), printed when the check fails. + */ +static bool gate_firmware_memory(const char *dcp) +{ + struct device_node *np, *region; + int i, n; + u32 record[2]; + + np = of_find_node_by_path(dcp); + if (!np) + return false; + n = of_count_phandle_with_args(np, "memory-region", NULL); + for (i = 0; i < n; i++) { + region = of_parse_phandle(np, "memory-region", i); + if (region && of_device_is_available(region) && + of_device_is_compatible(region, "apple,asc-mem")) { + of_node_put(region); + continue; + } + of_node_put(region); + n = -ENODEV; + break; + } + of_node_put(np); + if (n > 0) + return true; + np = of_find_node_by_path("/chosen"); + if (np && !of_property_read_u32_array(np, "m4,dcpext", record, 2)) + pr_err("%s: no firmware memory (loader record <%u %u>), nothing enabled\n", dcp, + record[0], record[1]); + else + pr_err("%s: no firmware memory, nothing enabled\n", dcp); + of_node_put(np); + return false; +} + +/* Every node of @st is there and still disabled, and its processor has firmware memory. */ +static bool gate_stage_ready(const struct gate_stage *st) +{ + struct device_node *np; + unsigned int i; + + if (!gate_firmware_memory(st->nodes[st->nr - 1])) + return false; + for (i = 0; i < st->nr; i++) { + np = of_find_node_by_path(st->nodes[i]); + if (!np) { + pr_err("%s: no such node, nothing enabled\n", st->nodes[i]); + return false; + } + if (of_device_is_available(np)) { + pr_err("%s: already enabled, nothing changed\n", st->nodes[i]); + of_node_put(np); + return false; + } + of_node_put(np); + } + return true; +} + +/* Enable @st's nodes and put its display DART into the display subsystem's iommus. */ +static int gate_stage_apply(const struct gate_stage *st, struct device_node *display) +{ + struct device_node *np; + struct of_changeset *cs; + unsigned int i; + int ret; + + cs = kzalloc(sizeof(*cs), GFP_KERNEL); + if (!cs) + return -ENOMEM; + of_changeset_init(cs); + + np = of_find_node_by_path(st->dart); + ret = np ? gate_display_add_dart(cs, display, np) : -ENODEV; + of_node_put(np); + for (i = 0; !ret && i < st->nr; i++) { + if (gate_is_dcp(st->nodes[i]) || gate_is_route(st->nodes[i])) + continue; + np = of_find_node_by_path(st->nodes[i]); + ret = np ? of_changeset_update_prop_string(cs, np, "status", "okay") : -ENODEV; + of_node_put(np); + } + if (!ret) + ret = of_changeset_apply(cs); + if (ret) { + of_changeset_destroy(cs); + kfree(cs); + } + /* Applied, it stays: the live tree refers to it. */ + return ret; +} + +#define GATE_PMP_WAIT_MS 20000 + +/* Set the status of the processors (externals) or the routes, and the display subsystem, in one changeset. */ +static int gate_set(struct device_node *display, bool dcps, const char *dcp_status, bool routes, + const char *route_status, const char *display_status) +{ + struct device_node *np; + struct of_changeset *cs; + unsigned int i, j; + int ret = 0; + + cs = kzalloc(sizeof(*cs), GFP_KERNEL); + if (!cs) + return -ENOMEM; + of_changeset_init(cs); + for (i = 0; !ret && i < ARRAY_SIZE(gate_stages); i++) + for (j = 0; !ret && j < gate_stages[i].nr; j++) { + const char *path = gate_stages[i].nodes[j]; + + if (!(dcps && gate_is_dcp(path)) && !(routes && gate_is_route(path))) + continue; + np = of_find_node_by_path(path); + ret = np ? of_changeset_update_prop_string(cs, np, "status", + gate_is_dcp(path) ? dcp_status : + route_status) : -ENODEV; + of_node_put(np); + } + if (!ret && display_status) + ret = of_changeset_update_prop_string(cs, display, "status", display_status); + if (!ret) + ret = of_changeset_apply(cs); + if (ret) { + of_changeset_destroy(cs); + kfree(cs); + } + return ret; +} + +static struct device_node *gate_display; +static struct device_node *gate_pmp; +static atomic_t gate_started = ATOMIC_INIT(0); + +/* Internal panel only: the routes go again, so the port controllers probe without mode switches. */ +static void gate_fallback(const char *why) +{ + int ret = gate_set(gate_display, false, NULL, true, "disabled", "okay"); + + pr_err("%s: external displays stay off, internal panel only (%d)\n", why, ret); +} + +/* + * The Type-C core treats a mode switch below a disabled node as absent, so the + * port controllers, which probe before dcpext1 is enabled, only took the USB + * muxes. Once dcpext1 is bound (its route mode switches are registered), let + * each controller wired to a route probe again, so that it takes them too. + */ +static void gate_reprobe_port_controllers(void) +{ + struct device *done[ARRAY_SIZE(gate_usbc_nodes)] = {}; + struct device_node *dcp, *np, *ep, *connector, *ctrl; + struct device *dev = NULL; + unsigned int i, n = 0, tries; + + dcp = of_find_node_by_path("/dcp@602e00000"); + for (tries = 0; dcp && tries < 150; tries++) { + dev = dcp->fwnode.dev; + if (dev && READ_ONCE(dev->driver)) + break; + msleep(200); + } + of_node_put(dcp); + if (!dev || !READ_ONCE(dev->driver)) { + pr_err("dcpext1 not bound, port controllers left as they are\n"); + return; + } + for (i = 0; i < ARRAY_SIZE(gate_usbc_nodes); i++) { + unsigned int k; + + if (!gate_is_route(gate_usbc_nodes[i])) + continue; + np = of_find_node_by_path(gate_usbc_nodes[i]); + ep = np ? of_graph_get_next_endpoint(np, NULL) : NULL; + connector = ep ? of_graph_get_remote_port_parent(ep) : NULL; + ctrl = connector ? of_get_parent(connector) : NULL; + dev = ctrl ? get_device(ctrl->fwnode.dev) : NULL; + of_node_put(ctrl); + of_node_put(connector); + of_node_put(ep); + of_node_put(np); + if (!dev) + continue; + for (k = 0; k < n && done[k] != dev; k++) + ; + if (k < n || !READ_ONCE(dev->driver)) { + put_device(dev); + continue; + } + done[n++] = dev; + if (device_reprobe(dev)) + dev_err(dev, "probe again failed\n"); + else + dev_info(dev, "probed again for the display routes\n"); + } + while (n) + put_device(done[--n]); +} + +/* + * A processor that finds its crossbar or DP PHY without a driver is given up + * for good (the deferred probe timeout is 0 here), and those drivers are + * modules that load only once the stage has created their devices. Wait until + * every such device of the enabled infrastructure has its driver. Returns the + * first path still without one, or NULL. + */ +static const char *gate_unbound_infra(void) +{ + unsigned int i, j; + + for (i = 0; i < ARRAY_SIZE(gate_stages); i++) + for (j = 0; j < gate_stages[i].nr; j++) { + const char *path = gate_stages[i].nodes[j]; + struct device_node *np; + struct device *dev; + + /* Power domains are the genpd core's to wait for; the modules are the point here. */ + if (gate_is_dcp(path) || gate_is_route(path) || + !strncmp(path, "/power-management@", 18)) + continue; + np = of_find_node_by_path(path); + dev = np ? READ_ONCE(np->fwnode.dev) : NULL; + of_node_put(np); + if (dev && !READ_ONCE(dev->driver)) + return path; + } + return NULL; +} + +#define GATE_DRIVER_WAIT_MS 10000 + +static void gate_wait_for_drivers(void) +{ + const char *path = NULL; + unsigned int tries; + + for (tries = 0; tries < GATE_DRIVER_WAIT_MS / 100; tries++) { + path = gate_unbound_infra(); + if (!path) + return; + msleep(100); + } + pr_warn("%s has no driver after " __stringify(GATE_DRIVER_WAIT_MS) " ms, enabling the processors anyway\n", + path); +} + +static void gate_power_up(struct work_struct *work) +{ + unsigned int i; + int ret = 0; + + for (i = 0; !ret && i < ARRAY_SIZE(gate_stages); i++) { + ret = gate_stage_apply(&gate_stages[i], gate_display); + if (!ret) + pr_info("stage %u enabled\n", i); + } + if (!ret) + gate_wait_for_drivers(); + /* Both processors and the display subsystem together: the master binds once, with all DCPs. */ + if (!ret) + ret = gate_set(gate_display, true, "okay", false, NULL, "okay"); + if (ret) { + gate_fallback("could not enable the external displays"); + return; + } + pr_info("processors and display subsystem enabled\n"); + gate_reprobe_port_controllers(); +} +static DECLARE_WORK(gate_power_up_work, gate_power_up); + +static void gate_timeout(struct work_struct *work) +{ + if (!atomic_xchg(&gate_started, 1)) + gate_fallback("no PMP within " __stringify(GATE_PMP_WAIT_MS) " ms"); +} +static DECLARE_DELAYED_WORK(gate_timeout_work, gate_timeout); + +/* The PMP's driver is bound: DISPEXT can be acknowledged now. */ +static int gate_pmp_bound(struct notifier_block *nb, unsigned long action, void *data) +{ + struct device *dev = data; + + if (action != BUS_NOTIFY_BOUND_DRIVER || dev->of_node != gate_pmp || atomic_xchg(&gate_started, 1)) + return NOTIFY_DONE; + cancel_delayed_work(&gate_timeout_work); + queue_work(system_long_wq, &gate_power_up_work); + return NOTIFY_OK; +} +static struct notifier_block gate_pmp_nb = { .notifier_call = gate_pmp_bound }; + +static int __init apple_t6041_extdisp_gate(void) +{ + unsigned int i; + int ret; + + if (!of_machine_is_compatible("apple,j616s")) + return 0; + if (IS_ENABLED(CONFIG_VIDEO) && video_firmware_drivers_only()) + return 0; + + /* Everything must be there and disabled, with firmware memory, before anything is changed. */ + for (i = 0; i < ARRAY_SIZE(gate_stages); i++) + if (!gate_stage_ready(&gate_stages[i])) + return 0; + gate_display = of_find_node_by_path("/display-subsystem"); + gate_pmp = of_find_node_by_path("/pmp@500700000"); + if (!gate_display || !gate_pmp || !of_device_is_available(gate_display) || + !of_device_is_available(gate_pmp)) { + pr_err("no display subsystem or PMP, nothing enabled\n"); + return 0; + } + + /* No device exists yet: routes on, display subsystem held back until the power-up. */ + ret = gate_set(gate_display, false, NULL, true, "okay", "disabled"); + if (ret) { + pr_err("could not prepare the tree: %d, nothing enabled\n", ret); + return 0; + } + ret = bus_register_notifier(&platform_bus_type, &gate_pmp_nb); + if (ret) { + gate_fallback("no bus notifier"); + return 0; + } + schedule_delayed_work(&gate_timeout_work, msecs_to_jiffies(GATE_PMP_WAIT_MS)); + pr_info("routes enabled, waiting for the PMP\n"); + return 0; +} +arch_initcall(apple_t6041_extdisp_gate); From 64a2c8e9373b541b48dfb357631668bfc627e9de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Bal=C3=A1zsy?= Date: Mon, 26 Oct 2026 17:22:40 +0100 Subject: [PATCH 219/388] drm/asahi: admit a third loaded T6040 25G83 image MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A J616s with 26.6.2 boot firmware (25G83, iBoot mBoot-18000.161.10) can load a third normalized T6040 TEXT identity: TEXT 0x60000, DATA 0x138000, the same entropy offsets, sha256 d725264d4831b95a5b8a375abf2fb5cdfbd6d7cbc7f1baa2cc8b37ee5d6e5e98. Without it the GPU probe stops with "GPU firmware image not admitted" and the device has no render node. Add the digest to the 26.6.2 T6040 image list next to the two existing identities and to the KUnit test that exercises every listed image through the lookup used by identify(). Signed-off-by: FrantiÅ¡ek Balázsy --- drivers/gpu/drm/asahi/fwid.rs | 7 ++++++- drivers/gpu/drm/asahi/hw/t6040.rs | 11 +++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/asahi/fwid.rs b/drivers/gpu/drm/asahi/fwid.rs index bb1fd3c36f623..be81eac5dfb74 100644 --- a/drivers/gpu/drm/asahi/fwid.rs +++ b/drivers/gpu/drm/asahi/fwid.rs @@ -213,7 +213,7 @@ mod tests { } #[test] - fn t6040_accepts_both_loaded_images() { + fn t6040_accepts_all_loaded_images() { let crate::g17::fw::variant::Firmware::Identified(builds) = crate::hw::t6040::CONFIG.firmware else { @@ -231,6 +231,11 @@ mod tests { 0x31, 0xc3, 0xb2, 0x8d, 0x05, 0x09, 0xa1, 0x7e, 0xb7, 0x61, 0x08, 0xb7, 0xc9, 0xa5, 0x2a, 0xcc, 0xd9, 0xac, ], + [ + 0xd7, 0x25, 0x26, 0x4d, 0x48, 0x31, 0xb9, 0x5a, 0x5b, 0x8a, 0x37, 0x5a, 0xbf, 0x2f, + 0xb5, 0xcd, 0xfb, 0xd6, 0xd7, 0xcb, 0xc7, 0xf1, 0xba, 0xa2, 0xcc, 0x8b, 0x37, 0xee, + 0x5d, 0x6e, 0x5e, 0x98, + ], ]; for (i, digest) in digests.into_iter().enumerate() { let found = measured(build.images, 0x138000, [0x59839, 0x5991c], digest).unwrap(); diff --git a/drivers/gpu/drm/asahi/hw/t6040.rs b/drivers/gpu/drm/asahi/hw/t6040.rs index 0007fdf132949..c6ea928630e56 100644 --- a/drivers/gpu/drm/asahi/hw/t6040.rs +++ b/drivers/gpu/drm/asahi/hw/t6040.rs @@ -85,6 +85,17 @@ pub(crate) const CONFIG: Config = Config { 0xc9, 0xa5, 0x2a, 0xcc, 0xd9, 0xac, ], }, + crate::fwid::FirmwareBuild { + name: "Apple GFX 25G83 for T6040 (normalized loaded image, second alternate J616s fill)", + text_size: 0x60000, + data_size: 0x138000, + entropy: [0x59839, 0x5991c], + text_sha256: [ + 0xd7, 0x25, 0x26, 0x4d, 0x48, 0x31, 0xb9, 0x5a, 0x5b, 0x8a, 0x37, 0x5a, 0xbf, + 0x2f, 0xb5, 0xcd, 0xfb, 0xd6, 0xd7, 0xcb, 0xc7, 0xf1, 0xba, 0xa2, 0xcc, 0x8b, + 0x37, 0xee, 0x5d, 0x6e, 0x5e, 0x98, + ], + }, ], variant: &fw::variant::V25G83, }]), From 6477da65cee54d3519556b02358b8a9b3e37e99a Mon Sep 17 00:00:00 2001 From: fbalazsy Date: Tue, 27 Oct 2026 00:01:36 +0100 Subject: [PATCH 220/388] drm/apple: enable adaptive sync on the desktop 26.6 internal panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The desktop 26.6 mode set (get_mode_info, then set_digital_out_mode) never sent IOMFBParameter 14, so VRR on the J616s ProMotion panel was offered by nobody and the panel stayed at a fixed rate; the VRR modes were hidden because enabling them that way left swaps uncompleted. Send adaptive sync (minimum rate in 16.16 Hz, 0 when VRR_ENABLED is off) before the mode is selected, as the firmware wants it applied with a mode set, and turn a VRR_ENABLED change into a mode set. Offer VRR on the internal panel only; external pipes do not program it here. On the J616s (T6041, DCP firmware 26.6.2) the firmware logs "IOMFBParameter_adaptive_sync Req minRR = 0x180000" and "3456x2234@120 Hz DSC=NO VRR=YES"; swaps keep completing, VRR on/off toggles and DPMS cycles are clean. Signed-off-by: FrantiÅ¡ek Balázsy --- drivers/gpu/drm/apple/dcp.c | 9 +++++++++ drivers/gpu/drm/apple/iomfb.c | 8 ++++---- drivers/gpu/drm/apple/iomfb_template.c | 23 ++++++++++++++++++----- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index e7f7d662c7a78..82b33106d5012 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -1203,6 +1203,15 @@ int dcp_crtc_atomic_check(struct drm_crtc *crtc, struct drm_atomic_commit *commi crtc_state->vrr_enabled) return -EINVAL; + /* The desktop 26.6 firmware applies adaptive sync with a mode set. */ + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && crtc_state->enable) { + struct drm_crtc_state *old = + drm_atomic_get_old_crtc_state(commit, crtc); + + if (old && old->vrr_enabled != crtc_state->vrr_enabled) + crtc_state->mode_changed = true; + } + /* DRM core only caps the blob size; the gamma update copies a full table. */ if (crtc_state->color_mgmt_changed && crtc_state->gamma_lut && drm_color_lut_size(crtc_state->gamma_lut) != dcp_gamma_lut_size(pdev)) diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index 97e1c46342f1d..6d7a0ca997546 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -91,14 +91,14 @@ static inline u64 dcpep_ack(enum dcp_context_id id) } /* - * Adaptive sync through IOMFBParameter 14 black-screens the J616s panel on - * the desktop 26.6 firmware: the firmware accepts the VRR mode, then no swap - * completes. Do not offer it there. + * The desktop 26.6 modeset programs adaptive sync only for the internal + * ProMotion panel (see iomfb_modeset()); offer VRR there and nowhere else. */ static bool dcp_mode_vrr_supported(struct apple_dcp *dcp, const struct dcp_display_mode *mode) { - return dcp->fw_compat != DCP_FIRMWARE_V_26_6_DESKTOP || !mode->vrr; + return dcp->fw_compat != DCP_FIRMWARE_V_26_6_DESKTOP || !mode->vrr || + dcp->main_display; } /* diff --git a/drivers/gpu/drm/apple/iomfb_template.c b/drivers/gpu/drm/apple/iomfb_template.c index 246498a8e843c..1c9afe1b68550 100644 --- a/drivers/gpu/drm/apple/iomfb_template.c +++ b/drivers/gpu/drm/apple/iomfb_template.c @@ -2927,10 +2927,7 @@ int DCP_FW_NAME(iomfb_modeset)(struct apple_dcp *dcp, .timing_mode_id = mode->timing_mode_id }; - /* - * Suspected VRR modes need swap timestamps, see iomfb_flush(). - * Adaptive sync itself stays off, see dcp_mode_vrr_supported(). - */ + /* VRR modes need swap timestamps, see iomfb_flush(). */ dcp->use_timestamps = mode->vrr; /* @@ -2971,7 +2968,23 @@ int DCP_FW_NAME(iomfb_modeset)(struct apple_dcp *dcp, modeset->bootstrap.timing_mode_id); } - dcp_modeset_select(dcp, NULL, cookie); + /* + * The internal ProMotion panel takes adaptive sync (IOMFBParameter 14, + * minimum rate in 16.16 Hz, 0 = off) before the mode is selected; the + * firmware then reports the mode set with VRR on. + */ + if (mode->vrr && dcp->main_display) { + struct dcp_set_parameter_dcp param = { + .param = IOMFBPARAM_ADAPTIVE_SYNC, + .value = { crtc_state->vrr_enabled ? mode->min_vrr : 0 }, + .count = 3, + }; + + dcp_set_parameter_dcp(dcp, false, ¶m, dcp_modeset_select, + cookie); + } else { + dcp_modeset_select(dcp, NULL, cookie); + } /* * The DCP firmware has an internal timeout of ~8 seconds for From 562eb4c4087d9908aa164d203df1087f49e92240 Mon Sep 17 00:00:00 2001 From: fbalazsy Date: Tue, 27 Oct 2026 00:18:17 +0100 Subject: [PATCH 221/388] drm/apple: enable adaptive sync on desktop 26.6 external displays MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The desktop 26.6 mode set programs adaptive sync the same way on the external pipes (dcpext) as on the internal panel. Send IOMFBParameter 14 for every VRR mode and stop hiding the VRR range of external displays. Signed-off-by: FrantiÅ¡ek Balázsy --- drivers/gpu/drm/apple/iomfb.c | 13 ------------- drivers/gpu/drm/apple/iomfb_template.c | 8 ++++---- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index 6d7a0ca997546..b2a34c28f3008 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -90,17 +90,6 @@ static inline u64 dcpep_ack(enum dcp_context_id id) return dcpep_msg(id, 0, 0) | IOMFB_MSG_ACK; } -/* - * The desktop 26.6 modeset programs adaptive sync only for the internal - * ProMotion panel (see iomfb_modeset()); offer VRR there and nowhere else. - */ -static bool dcp_mode_vrr_supported(struct apple_dcp *dcp, - const struct dcp_display_mode *mode) -{ - return dcp->fw_compat != DCP_FIRMWARE_V_26_6_DESKTOP || !mode->vrr || - dcp->main_display; -} - /* * A channel is busy if we have sent a message that has yet to be * acked. The driver must not sent a message to a busy channel. @@ -1963,8 +1952,6 @@ int dcp_get_modes(struct drm_connector *connector) continue; published_180hz |= modes[i].automatic_rgb8_quirk; published++; - if (!dcp_mode_vrr_supported(dcp, &modes[i])) - continue; if (modes[i].vrr) { u16 lo = modes[i].min_vrr >> 16; u16 hi = modes[i].max_vrr >> 16; diff --git a/drivers/gpu/drm/apple/iomfb_template.c b/drivers/gpu/drm/apple/iomfb_template.c index 1c9afe1b68550..013e0073afeff 100644 --- a/drivers/gpu/drm/apple/iomfb_template.c +++ b/drivers/gpu/drm/apple/iomfb_template.c @@ -2969,11 +2969,11 @@ int DCP_FW_NAME(iomfb_modeset)(struct apple_dcp *dcp, } /* - * The internal ProMotion panel takes adaptive sync (IOMFBParameter 14, - * minimum rate in 16.16 Hz, 0 = off) before the mode is selected; the - * firmware then reports the mode set with VRR on. + * Adaptive sync (IOMFBParameter 14, minimum rate in 16.16 Hz, 0 = off) + * goes before the mode is selected; the firmware then reports the mode + * set with VRR on. */ - if (mode->vrr && dcp->main_display) { + if (mode->vrr) { struct dcp_set_parameter_dcp param = { .param = IOMFBPARAM_ADAPTIVE_SYNC, .value = { crtc_state->vrr_enabled ? mode->min_vrr : 0 }, From cf38d3a6c7190da84dd37ee705acea47fde2af54 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Mon, 26 Oct 2026 20:10:38 -0400 Subject: [PATCH 222/388] drm/asahi: observe and retire a G17 render pair in one device-mutex hold The event worker read a render pair's completion memory outside the device mutex. It took the pair out of its entry on loan under one hold, read the front's stamps and graph cursors unlocked, then took the mutex again to return the pair and retire what it had seen. The read is a short memory snapshot, so the loan bought no useful concurrency, and every observed pair cost two acquisitions of the mutex that submission also takes, with publishers able to slip in between. Select the pair, read its front and retire it in one hold. The pair stays in its entry, so observations no longer need the loan bookkeeping (the preparing marker and the borrowed dependency and switch snapshots) or the return path and its invariant-failure handling. Preparers keep using the loan. The read takes the context's work mutex for the shared list head, which now nests inside the device mutex; that mutex is a leaf, held only around accesses to the context's work storage. Measured on Neo across boots, with this change, without it, and with it again (3 rounds each, medians, us per submit): render.light tri queue d8 31.29 / 33.73 / 31.04 (-7.6%) render.light tri queue d1 105.49 / 105.75 / 105.53 render.light tri queue d32/d64 30.06 / 30.01 / 29.68 render.light tri procs d2 52.18 / 52.24 / 52.29 render.light tri queues d2 51.98 / 52.12 / 52.12 compute.light queue d1 103.61 / 103.73 / 104.34 sync.signal queues d2 timeline 22.74 / 22.67 / 22.69 sync.signal queues d2 binary 22.53 / 22.27 / 22.53 --- .../drm/asahi/g17/runtime/render/observe.rs | 99 ++++++------------- 1 file changed, 28 insertions(+), 71 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/runtime/render/observe.rs b/drivers/gpu/drm/asahi/g17/runtime/render/observe.rs index 5ae76639f6521..e6077624ba4f2 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render/observe.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render/observe.rs @@ -1,21 +1,22 @@ // SPDX-License-Identifier: GPL-2.0-only OR MIT -//! Event-driven render snapshots outside the device mutex. The preparation -//! epoch and exact pair owner survive every read, cancellation and retry. +//! Event-driven render snapshots. A pair's completion memory is read and the +//! pair retired in one device-mutex hold, inside the preparation epoch. use super::super::DeferredBatch; -use crate::g17::queue::render::Pair; use core::sync::atomic::Ordering; use kernel::{prelude::*, sync::Arc}; impl crate::g17::Firmware { - fn take_render_observation(&mut self, start: usize) -> Result)>> { + /// The first slot from `start` whose pair has news to observe: published work in flight, both + /// queues' notifications seen, and an observation the pair accepts. + fn next_render_observation(&self, start: usize) -> Result> { if self.recovery.pending() { return Ok(None); } let state = crate::g17::recovery::Memory::recovery_state(&self.init)?; for slot in start..self.queues.render.entries.len() { - let Some(entry) = self.queues.render.entries[slot].as_mut() else { + let Some(entry) = self.queues.render.entries[slot].as_ref() else { continue; }; if entry.preparing.is_some() || entry.growing { @@ -37,35 +38,13 @@ impl crate::g17::Firmware { self.init.stamp(qids[1])?.load(Ordering::Relaxed), ]; core::sync::atomic::fence(Ordering::Acquire); - let Some(packet) = pair.observation_packet(entry.event_masks, stamps, state)? else { - continue; - }; - entry.borrowed_active = pair.published_in_flight(); - entry.borrowed_dependencies = Some(pair.dependency_snapshot()); - entry.borrowed_switch = pair.switch_in_flight(); - entry.preparing = Some(packet); - return Ok(Some((slot, entry.pair.take().ok_or(EIO)?))); + if pair.observation_packet(entry.event_masks, stamps, state)?.is_some() { + return Ok(Some(slot)); + } } Ok(None) } - fn return_render_observation(&mut self, slot: usize, pair: &mut Option>) -> Result { - let returning = pair.as_ref().ok_or(EINVAL)?; - let entry = self.queues.render.entry_mut(slot as u8)?; - if entry.pair.is_some() - || entry.preparing.is_none() - || returning.slot() as usize != slot - || returning.qids() != entry.reservation.ids.map(crate::g17::kick::Id::qid) - { - return Err(EIO); - } - entry.pair = pair.take(); - entry.borrowed_dependencies = None; - entry.borrowed_switch = None; - entry.preparing = None; - Ok(()) - } - pub(super) fn settle_render_terminal( &mut self, slot: usize, @@ -122,59 +101,37 @@ impl crate::g17::Shared { Err(error) if error == EAGAIN || error == ENODEV => break, result => result?, }; - let loan = { - let mut state = self.state.lock(); - let Some(firmware) = (*state).as_deref_mut() else { - return Ok(()); - }; - if !epoch.is_current() || self.crashed.load(Ordering::Acquire) { - break; - } - firmware.take_render_observation(cursor)? + // Selecting, reading and retiring the front share one hold: a + // separate hold for the read would let publishers and the + // retirement contend twice per observed pair. + let mut state = self.state.lock(); + let Some(firmware) = (*state).as_deref_mut() else { + return Ok(()); }; - let Some((slot, pair)) = loan else { + if !epoch.is_current() + || firmware.recovery.pending() + || self.crashed.load(Ordering::Acquire) + { + break; + } + let Some(slot) = firmware.next_render_observation(cursor)? else { break; }; if slot != cursor { remaining = crate::hw::t8140::queues::RENDER_DEPTH; retries = 0; } - let observed = pair.observe_front(); - let mut pair = Some(pair); - let mut state = self.state.lock(); - // Return installed ownership before examining read errors or epoch. - let restored = (*state) - .as_deref_mut() - .ok_or(ENODEV) - .and_then(|firmware| firmware.return_render_observation(slot, &mut pair)); - if let Err(error) = restored { - // A failed invariant must not drop firmware-owned storage. Join - // the other preparers only after releasing our own epoch, and - // keep this detached owner until both processors have stopped. - drop(state); - drop(epoch); - self.lose_device_quietly(); - self.stop_runtime(); - if let Some(mut pair) = pair { - pair.stopped(&mut |event| { - event.finish(); - Ok(()) - })?; - } - return Err(error); - } - let firmware = (*state).as_deref_mut().ok_or(EIO)?; + let observed = firmware.queues.render.entries[slot] + .as_ref() + .and_then(|entry| entry.pair.as_ref()) + .ok_or(EIO)? + .observe_front(); + // Publishers waiting for render capacity recheck once this hold ends. self.note_changed(); if firmware.render_grow_pending() || firmware.queues.render.take_missed_event(slot as u8) { // Observation and the growth check run on every pass. self.queue_event_records(); } - if !epoch.is_current() - || firmware.recovery.pending() - || self.crashed.load(Ordering::Acquire) - { - break; - } let result = (|| { let state = crate::g17::recovery::Memory::recovery_state(&firmware.init)?; let progressed = match observed { From de0b22a8370e0dec3ffb080c173aea4aa4fc331b Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Mon, 26 Oct 2026 16:51:14 -0400 Subject: [PATCH 223/388] drm/asahi: retire a G17 render pass on its fragment notification An event pass considered a render pair only once both of its queues had posted a stamp notification, the tiling queue's and the fragment queue's. The tiling notification adds nothing a retirement needs: the fragment kick starts only after its fragment barrier saw the tiling stamp, and the observation still requires both stamps in memory to cover the pass. Recovery attribution reads the stamps, and parameter-buffer growth arrives as its own record. Select a pair, and decide that it needs a rescan, on the fragment queue's notification alone, through one helper with KUnit coverage. A pass whose only news is a tiling stamp no longer borrows the pair for an observation that cannot retire. --- drivers/gpu/drm/asahi/g17/queue/render.rs | 49 +++++++++++++++++-- drivers/gpu/drm/asahi/g17/runtime/render.rs | 8 +-- .../drm/asahi/g17/runtime/render/observe.rs | 4 +- 3 files changed, 49 insertions(+), 12 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/queue/render.rs b/drivers/gpu/drm/asahi/g17/queue/render.rs index 696a1486eb0ec..36b07caeb1495 100644 --- a/drivers/gpu/drm/asahi/g17/queue/render.rs +++ b/drivers/gpu/drm/asahi/g17/queue/render.rs @@ -1034,11 +1034,7 @@ impl Pair { ] } fn selected(&self, masks: Option<[u64; 2]>) -> bool { - masks.is_none_or(|masks| { - self.qids() - .into_iter() - .all(|qid| masks[usize::from(qid) / 64] & (1u64 << (qid % 64)) != 0) - }) + masks.is_none_or(|masks| completion_noted(self.qids(), masks)) } /// The oldest published group still running, for cross-VM timeout attribution /// (crate::g17::timeout). A quarantined pair's work holds the GPU until the firmware drains @@ -2200,3 +2196,46 @@ impl Pair { Ok(()) } } + +/// Whether `masks`, the stamp notifications collected for a pair whose tiling and fragment +/// queues are `qids`, can mean a finished pass: the fragment queue's bit. A fragment kick starts +/// only after its fragment barrier saw the tiling stamp, and retirement still checks both stamps +/// in memory, so the tiling notification is not needed to retire. +pub(crate) fn completion_noted(qids: [u8; 2], masks: [u64; 2]) -> bool { + let fragment = usize::from(qids[1]); + masks[fragment / 64] & (1u64 << (fragment % 64)) != 0 +} + +#[kunit_tests(asahi_g17_render_completion_noted)] +mod completion_noted_tests { + use super::completion_noted; + + fn bit(qid: u8) -> [u64; 2] { + let mut masks = [0; 2]; + masks[usize::from(qid) / 64] |= 1u64 << (qid % 64); + masks + } + + #[test] + fn fragment_notification_alone_selects() { + assert!(completion_noted([3, 4], bit(4))); + assert!(completion_noted([70, 100], bit(100))); + } + + #[test] + fn tiling_notification_alone_does_not_select() { + assert!(!completion_noted([3, 4], bit(3))); + assert!(!completion_noted([70, 100], bit(70))); + assert!(!completion_noted([3, 4], [0; 2])); + } + + #[test] + fn other_queues_do_not_select() { + assert!(!completion_noted([3, 4], bit(5))); + assert!(!completion_noted([3, 68], bit(4))); + let mut both = bit(3); + both[1] |= bit(68)[1]; + assert!(completion_noted([3, 68], both)); + } +} + diff --git a/drivers/gpu/drm/asahi/g17/runtime/render.rs b/drivers/gpu/drm/asahi/g17/runtime/render.rs index b5649f059f5d2..18292860811bc 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render.rs @@ -1031,10 +1031,10 @@ impl Registry { |pair| pair.published_in_flight(), ); active - && entry.reservation.ids.into_iter().all(|id| { - let qid = usize::from(id.qid()); - entry.event_masks[qid / 64] & (1u64 << (qid % 64)) != 0 - }) + && crate::g17::queue::render::completion_noted( + entry.reservation.ids.map(kick::Id::qid), + entry.event_masks, + ) }) } pub(in crate::g17) fn defer_render_timeout( diff --git a/drivers/gpu/drm/asahi/g17/runtime/render/observe.rs b/drivers/gpu/drm/asahi/g17/runtime/render/observe.rs index 52bdd964ca4ae..a0a92c2f21b89 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render/observe.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render/observe.rs @@ -26,9 +26,7 @@ impl crate::g17::Firmware { }; let qids = pair.qids(); if !pair.published_in_flight() - || !qids - .into_iter() - .all(|qid| entry.event_masks[usize::from(qid) / 64] & (1u64 << (qid % 64)) != 0) + || !crate::g17::queue::render::completion_noted(qids, entry.event_masks) { continue; } From 5073eac15192ff8aec118ed2dc85a3d632a6f845 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Mon, 26 Oct 2026 22:47:20 -0400 Subject: [PATCH 224/388] drm/asahi: set the G17 tiling first-kick word on a pair's first kick only The tiling descriptor has a word at the position of the fragment descriptor's first-kick word, which the fragment stage sets only on a queue pair's first kick. The tiling setup set it on every kick. With it set, the firmware also notifies the host of the tiling queue's completion on every pass, so each render cost two firmware notifications, two mailbox interrupts and two event passes, while retirement needs only the fragment queue's notification (previous commit). Name the word first_kick and set it for ordinal 0 only, as for the fragment stage, on the A18 Pro firmware (26A428). The M4 Pro firmware (25G83) keeps the old value on every kick until it is measured there. Measured on Neo in one boot through a diagnostic switch that toggled only this word (3 rounds each, medians, us per submit): GPU mailbox IRQs per submit (render.light d32) 2.18 -> 1.10 render.light tri queue d1 105.26 -> 104.12 render.light tri queue d8 30.98 -> 29.30 render.light tri queue d32 29.67 -> 27.80 render.light tri queue d64 29.67 -> 27.78 render.light tri procs d2 52.12 -> 49.73 render.light tri queues d2 52.16 -> 49.65 compute.light queue d1 103.39 -> 104.08 sync.signal queues d2 timeline / binary 22.79 / 22.37 -> 22.61 / 22.61 --- drivers/gpu/drm/asahi/g17/fw/render.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/fw/render.rs b/drivers/gpu/drm/asahi/g17/fw/render.rs index aa8693bf7140b..a6bc1704d1418 100644 --- a/drivers/gpu/drm/asahi/g17/fw/render.rs +++ b/drivers/gpu/drm/asahi/g17/fw/render.rs @@ -1023,7 +1023,11 @@ pub(crate) struct TaDescriptor { /// QID of the tiling queue. pub(crate) qid: u32, pub(crate) unk_8be: u32, - pub(crate) unk_8c2: u32, + /// Non-zero on the first kick of a queue pair, like the fragment descriptor's word at the + /// same position. Set on every kick, the firmware also notifies the host of each pass's + /// tiling completion, which no retirement needs. The 25G83 firmware keeps it set on every + /// kick until this is measured there. + pub(crate) first_kick: u32, /// Completion value of the tiling kick (see /// [`KickTimestamp::completion_value`]). pub(crate) completion: u64, @@ -1073,7 +1077,6 @@ impl TaDescriptor { const ARRAY_OFFSET: u64 = core::mem::offset_of!(Self, registers) as u64; const UNK_876: u32 = u32::MAX; const UNK_892: u32 = 1; - const UNK_8C2: u32 = 1; const UNK_932: u8 = 0x44; const UNK_93C: u8 = 1; const UNK_93E_FIRST_KICK: [u8; 2] = [0xd0, 0x91]; @@ -1127,7 +1130,10 @@ impl TaDescriptor { self.aux_stamp_va = args.ta.aux_stamp_va; self.stamp_value_2 = stamp_value; self.qid = args.ta.qid.into(); - self.unk_8c2 = Self::UNK_8C2; + self.first_kick = match args.programs == RegisterPrograms::V25G83 { + true => 1, + false => u32::from(args.ordinal == 0), + }; self.current_id = resources.ta_ids.current.into(); self.ordinal = args.ordinal as u32; self.timestamp_start_va = resources.timestamp_va; From fcd39d542f865e7a0ae744da44748897dfdee75d Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 14:45:56 +1100 Subject: [PATCH 225/388] drm/asahi: require a compute stamp for record fallback across recovery An exact completion record that has aged for 50 ms permits compute retirement without a stamp in the ordinary generation. The recovery path can consume queue items without completing command writes; those cursors and an old record alone must not release command mappings. Restrict the record-only fallback to the publication's generation. After a restart, retain the command until a covering stamp and consumed items provide the release witness, or the existing quarantine/stop path takes ownership. Keep interrupted fence classification independent. Exercise aged records with missing, stale, exact and later stamps across a restart, consumed and busy rings, and the ordinary fallback control. This closes a retention hazard; it does not establish the cause of the T6040 CTS page faults or qualify recovery on that firmware. Assisted-by: LLM Signed-off-by: Ryan Murray --- .../drm/asahi/g17/queue/compute/retirement.rs | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/queue/compute/retirement.rs b/drivers/gpu/drm/asahi/g17/queue/compute/retirement.rs index c2b49e7b436d7..3f0246047e845 100644 --- a/drivers/gpu/drm/asahi/g17/queue/compute/retirement.rs +++ b/drivers/gpu/drm/asahi/g17/queue/compute/retirement.rs @@ -106,7 +106,9 @@ pub(crate) fn note_stampless(qid: u8, kick: u64) { /// A zero-duration record fails before visibility polling. It proves execution did not occur; /// ownership still requires a separate retirement witness before mappings can be released. /// The command is complete when its stamp covers it, or when its exact completion record has -/// been present for [`STAMP_FALLBACK_NS`] without one (`record_aged`); in both cases its items +/// been present for [`STAMP_FALLBACK_NS`] without one (`record_aged`) in the same recovery +/// generation. Recovery may consume items without completing their writes, so an old record +/// and consumed items cannot replace the stamp across a restart. In both cases its items /// must be consumed. #[expect( clippy::too_many_arguments, @@ -125,7 +127,9 @@ pub(crate) fn poll( if observed.is_some_and(|record| record.timestamps[0] == record.timestamps[1]) { return Err(ENODATA); } - let completed = stamp_covers(stamp, ticket.kick) || (observed.is_some() && record_aged); + let same_generation = !matches!(recordless, Recordless::CrossedRecovery); + let completed = stamp_covers(stamp, ticket.kick) + || (same_generation && observed.is_some() && record_aged); if !completed || !cursors.consumed(ticket.item_producer, ticket.item_count)? { return Ok(None); } @@ -527,6 +531,30 @@ mod tests { assert_eq!(result, Ok(None)); } + #[test] + fn recovery_cannot_turn_an_aged_record_into_a_release_witness() { + let record = Some(Observation { timestamps: [10, 20] }); + // Recovery can advance the consumer without completing cache maintenance. + // Even a record observed before the restart and aged past the fallback + // must retain its command's mappings until a real stamp covers it. + for stamp in [0, own(KICK - 1)] { + assert_eq!(poll(&ticket(), record, consumed(), stamp, + Recordless::CrossedRecovery, 0, None, true), Ok(None)); + } + // A real stamp restores the release witness; completion's interrupted + // classification independently prevents false successful fences. + for stamp in [own(KICK), own(KICK + 1)] { + assert_eq!(poll(&ticket(), record, consumed(), stamp, + Recordless::CrossedRecovery, 0, None, true), Ok(Some([10, 20]))); + } + let busy = Cursors { producer: 4, consumer: 2, count: 64 }; + assert_eq!(poll(&ticket(), record, busy, own(KICK), + Recordless::CrossedRecovery, 0, None, true), Ok(None)); + // The existing same-generation fallback remains available. + assert_eq!(poll(&ticket(), record, consumed(), own(KICK - 1), + Recordless::Allowed, 0, None, true), Ok(Some([10, 20]))); + } + #[test] fn a_zero_duration_record_fails() { let record = Some(Observation { timestamps: [5, 5] }); From 90150b09b82066a3104df8bb7ec3a1dfeffc06c3 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 14:46:45 +1100 Subject: [PATCH 226/388] drm/asahi: retain recovery progress when slot keys are unknown The 25G83 path has no validated slot-key address. It consequently lost progress observations too, even though they come from a separately validated firmware-owned view. Read those observations with an unknown key represented by zero; preserve firmware blame when ownership is unknown or a read fails. Report the bounded halted-firmware snapshot with its generation, reason, slot mask, known-key count and per-slot progress. Identify stampless retirement counters explicitly as lifetime totals so they cannot be mistaken for commands retired by a single recovery. Read no new MMIO addresses and change no firmware timeout, feature or voltage policy. Test unknown ownership, incomplete progress and page-fault fallback. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/asahi/g17/initdata.rs | 5 +- .../drm/asahi/g17/queue/compute/retirement.rs | 6 +- drivers/gpu/drm/asahi/g17/recovery.rs | 1 + .../gpu/drm/asahi/g17/recovery/attribution.rs | 58 +++++++++++++++++++ 4 files changed, 68 insertions(+), 2 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/initdata.rs b/drivers/gpu/drm/asahi/g17/initdata.rs index 3b224e330cdcb..c8763f40871d1 100644 --- a/drivers/gpu/drm/asahi/g17/initdata.rs +++ b/drivers/gpu/drm/asahi/g17/initdata.rs @@ -1563,7 +1563,10 @@ impl super::recovery::Memory for InitData { .dword(self.status_b + abi::PrimaryStatusB::RECOVERY_SLOT_MASK)? .load(Ordering::Relaxed); let mut sources = super::recovery::Sources::sample(mask as u32, |slot| { - let key = super::recovery::slot_key(slot, self.recovery_slot_keys)?; + // Progress is independently available even when this firmware's key + // address is unknown. Zero keeps ownership unknown and cannot override + // firmware blame, but preserves the progress evidence for diagnostics. + let key = super::recovery::slot_key(slot, self.recovery_slot_keys).unwrap_or(0); let monitor = self.recovery_view_words::<6>(abi::recovery::PROGRESS_VIEW, abi::recovery::PROGRESS_START + slot * abi::recovery::PROGRESS_STRIDE) .ok().map(|words| words[0])?; diff --git a/drivers/gpu/drm/asahi/g17/queue/compute/retirement.rs b/drivers/gpu/drm/asahi/g17/queue/compute/retirement.rs index 3f0246047e845..c132b87a9860a 100644 --- a/drivers/gpu/drm/asahi/g17/queue/compute/retirement.rs +++ b/drivers/gpu/drm/asahi/g17/queue/compute/retirement.rs @@ -88,13 +88,17 @@ pub(crate) fn record_aged(observed_at: Option, now: u64) -> bool { static STAMPLESS: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0); +pub(crate) fn stampless_total() -> u64 { + STAMPLESS.load(core::sync::atomic::Ordering::Relaxed) +} + /// Counts a command that retired on its record because its completion stamp never arrived; /// logs the first and every 64th. pub(crate) fn note_stampless(qid: u8, kick: u64) { let count = STAMPLESS.fetch_add(1, core::sync::atomic::Ordering::Relaxed) + 1; if count == 1 || count % 64 == 0 { pr_info!( - "G17: {} compute commands retired on their completion record after {} ms without a completion stamp (latest: queue {} kick {:#x})\n", + "G17: lifetime total {} compute commands retired on their completion record after {} ms without a completion stamp (latest: queue {} kick {:#x})\n", count, STAMP_FALLBACK_NS / 1_000_000, qid, diff --git a/drivers/gpu/drm/asahi/g17/recovery.rs b/drivers/gpu/drm/asahi/g17/recovery.rs index 3bb9d20190b57..8bffec476c9ac 100644 --- a/drivers/gpu/drm/asahi/g17/recovery.rs +++ b/drivers/gpu/drm/asahi/g17/recovery.rs @@ -244,6 +244,7 @@ pub(super) fn service( host.clear_timestamps()?; } let mut sources = host.fault_sources()?; + sources.report(request.generation, super::queue::compute::retirement::stampless_total()); let target_error = host.publish_recovery_targets(); host.transition(1, 2)?; let mut recovered = false; diff --git a/drivers/gpu/drm/asahi/g17/recovery/attribution.rs b/drivers/gpu/drm/asahi/g17/recovery/attribution.rs index 1755484219187..be1c7ab9eb662 100644 --- a/drivers/gpu/drm/asahi/g17/recovery/attribution.rs +++ b/drivers/gpu/drm/asahi/g17/recovery/attribution.rs @@ -30,6 +30,23 @@ pub(super) enum Attribution { } impl Sources { + /// Bounded evidence from the existing halted-firmware snapshot. No guessed + /// MMIO/key addresses or additional firmware reads are used for diagnostics. + pub(super) fn report(&self, generation: u64, stampless_total: u64) { + kernel::pr_warn!( + "G17: recovery snapshot gen {} reason {:?} slot_mask {:#x} sampled {} known_keys {}; stampless lifetime total {} before restart\n", + generation, self.reason, self.mask, self.count, + self.records[..self.count].iter().filter(|source| source.qid.is_some()).count(), + stampless_total + ); + for source in &self.records[..self.count] { + kernel::pr_warn!( + "G17: recovery slot {} queue {:?} kick {:#x} monitor {:#x}\n", + source.slot, source.qid, source.stamp, source.monitor + ); + } + } + /// Firmware is halted for every selected key and progress-record read. /// Failed reads stay unknown; incomplete evidence cannot override firmware blame. pub(crate) fn sample(mask: u32, mut read: impl FnMut(usize) -> Option<(u64, u32)>) -> Self { @@ -103,3 +120,44 @@ impl Sources { } } } + +#[kernel::macros::kunit_tests(asahi_g17_recovery_sources)] +mod tests { + use super::*; + + #[test] + fn progress_without_keys_preserves_firmware_blame() { + let mut reads = 0; + let mut sources = Sources::sample(0b101, |slot| { + reads += 1; + Some((0, (slot + 1) as u32)) + }); + sources.reason = Some(2); + assert_eq!(reads, 2); + assert_eq!(sources.count, 2); + assert_eq!(sources.records[1].slot, 2); + assert_eq!(sources.records[1].monitor, 3); + assert!(sources.records[..sources.count].iter().all(|source| source.qid.is_none())); + assert_eq!(sources.attribute(Some(38), |_, _| panic!("unknown key")), + Attribution::Firmware); + } + + #[test] + fn incomplete_progress_cannot_clear_firmware_blame() { + let mut sources = Sources::sample(0b11, |slot| { + (slot == 0).then_some(((38 << 40) | 1, 0)) + }); + sources.reason = Some(2); + assert_eq!(sources.attribute(Some(38), |_, _| Some(false)), Attribution::Firmware); + } + + #[test] + fn page_fault_keeps_firmware_blame_with_complete_keys() { + let mut sources = Sources::sample(0b11, |slot| { + Some(((((38 + slot) as u64) << 40) | 1, 0)) + }); + sources.reason = Some(PAGE_FAULT); + assert_eq!(sources.attribute(Some(38), |qid, _| Some(qid == 39)), + Attribution::Firmware); + } +} From 0c857881725fd02b155c704e190e524183da5483 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 14:48:00 +1100 Subject: [PATCH 227/388] docs: enumerate the remaining T6040 firmware qualification facts Specify the exact firmware facts needed for each gated 25G83 path, including achieved-clock telemetry, recovery identity/table semantics, visibility witnesses, native features and MMIO registration. Distinguish advertised OPPs from sustained residency and retention safety from CTS root-cause qualification. Keep all unsupported paths gated by evidence. Assisted-by: LLM Signed-off-by: Ryan Murray --- GAPS.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/GAPS.md b/GAPS.md index cfa2140a890b4..dd5781b12df4d 100644 --- a/GAPS.md +++ b/GAPS.md @@ -47,6 +47,36 @@ parity. There are no J616s results for this series yet. | M5 Pro comparison | No M5 Pro hardware configuration exists in this public baseline | No T605x/G17S KMD path to compare | No claim of M5 parity | Pinned public tree contains T6040 and T8140; a licensed public M5 Pro reference is required | | CTS and Linux/macOS performance ratio | Hardware results belong to their exact kernel/Mesa pair | No result for this candidate | Pending hardware agent | Run gpubench with matching suite, configuration and conditions; review CTS failures and kernel faults before claiming parity | +## Remaining 25G83 facts and recovery qualification + +No firmware capability is inferred from another GPU's successful CTS result. +The following are the exact T6040/25G83 facts needed to open the remaining +paths. A reference capture of achieved frequency establishes a target, not +an offset, voltage, descriptor ABI, or Linux residency measurement. + +| Gated path | Exact facts needed for T6040/25G83 | Current evidence and disposition | +| --- | --- | --- | +| Sustained top clock | DT OPP count, both voltage lanes, hardware A/B selector maps, selected logical-state limit; independently decoded achieved frequency or residency, counter units/timebase/wrap, request versus achieved-state semantics and limiting reason under sustained load | Per-chip DT handoff and maps are already mandatory; both maps select the final raw OPP. `max_frequency_khz` is an advertised maximum, not achieved telemetry. G17 `stats_snapshot()` returns None; ring drain alone proves no residency. Keep voltages/maps unchanged and require a measured Linux load trace. | +| Raw versus logical state count | Meaning of the ADT count properties for this firmware, both SGX map banks and number of entries actually consumed by the 25G83 power tables | Public `HwData25G83`, calibration validation and loader handoff select eleven logical states from sixteen raw OPPs. Do not resize the firmware structure from the ADT property name or discard its unsorted OPPs. Compare actual top selection and achieved clock. | +| DVFS ramp / idle / fast-die | 25G83 meaning and units of ramp/effort controls, idle/off delays, temperature limits and hysteresis; PMP/AP/CLPC request ownership, achieved floors and release under idle and load | Existing shared submission/idle policy, DT suppliers and T6040 power-state graph are enabled. Power-state presence is not proof of load tracking. Require live requests, actual floor release and thermal traces; opaque power words are not tunable parameters. | +| Recovery slot attribution | Exact 25G83 slot-key storage range, lifetime, cache policy and slot stride; key QID/stamp bit fields; slot-mask meanings, progress monitor semantics and association with an actual started render | `recovery_slot_keys=None`; never copy T8140's physical address. Preserve firmware blame. Progress-view reads no longer depend on knowing the key address; bounded diagnostics report unknown ownership explicitly. | +| Recovery table / reason | 25G83 recovery-info offset/extent, valid/guilty/cancelled bits, stamp sequence encoding; reason-code semantics; ordering and reset of records relative to states 1/2/3/0; interrupted-command drain and visibility guarantees | Existing checked 256-entry table and handshake remain unchanged. Reason 3 is treated as page fault by existing attribution. New generation/slot diagnostics aid qualification; missing key evidence cannot clear blame. An aged compute record may no longer substitute for a stamp across recovery. | +| Compute completion / visibility | Exact compute record identity and timestamp fields, stamp write/cache-maintenance order, ring consumption during restart, late/missing-stamp behavior and a release witness for interrupted work | Same-generation 50 ms fallback retained. Crossing recovery now requires a covering stamp for ordinary retirement; quarantine, kill/drain and processor-stop ownership remain. A completion record plus elapsed time after restart is not independent visibility evidence. | +| MMIO pair registration | 25G83 MMIO port addresses and access-power witness; pair registration write order, ACK and coexistence with lead commands/bootstrap QID reservation | Keep disabled. The 26A428 sequence, POWER_ON_DONE message and recovery-target pointer are distinct ABI facts. | +| MCache | 25G83 table-entry format, sizes/alignment, descriptor pointers and enable bits, ownership/coherency rules, partial-render/restart behavior | Keep disabled until a licensed public implementation or independently supplied facts establish the whole contract and tests validate it. | +| Few-primitives | T6040 enable-bit semantics, required auxiliary buffer sizes/registers and partial-store/load/resume handling for all tiling/fragment paths | Keep disabled; previously observed hangs are not grounds to enable it for performance. | +| Ray traversal / preemption | T6040 register program, scratch sizing/alignment by enabled cores/clusters, saved operand-state layout, resume/checkpoint semantics, kill/restart contract, scheduler timer mask and priority profiles | Existing two-by-ten operand layout and priorities retained; periodic timer mask remains zero on both variants. Native traversal remains disabled pending bounded execution/fault/recovery qualification. | +| TLB range operations | T6040 opcode/operand encoding, VA/ASID granules, ordering, completion and negative-translation invalidation across every mapping alias | Existing page TLBI and PoC table cleaning retained. Copying Neo range encoding cannot close a performance gap safely. | +| Large vertex/barrier and tess/geometry faults | Faulting engine/access/VA/VM, exact bound intervals/backings and mapping lifetime; render/compute descriptor identity, TVB/GTP capacities, pass-start/stamp state and recovery reason before acknowledgment | Isolate exact public CTS names with one process and compare kernels with the same Mesa. Buffer size alone does not establish VM-bind, timeout or TVB causality. Retention fix is independently justified, not a claim to have cured a CTS fault. | + +The compute stamp-wait KUnit suite now checks that an aged exact record and +consumed items cannot retire a pre-recovery kick without its stamp, while an +exact/later stamp, busy-ring control and same-generation fallback retain their +expected behavior. Recovery-source tests cover unknown keys, incomplete reads +and page-fault fallback. Diagnostic counters explicitly identify lifetime +stampless totals; they are not a count of commands retired by one restart. + + ## Source and validation boundaries The implementation references are the licensed public Aurora/Asahi kernel and From 34665a1107eb74adb99f8724ec5a94370e2fe918 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 14:49:05 +1100 Subject: [PATCH 228/388] drm/asahi: preserve firmware blame when any selected slot lacks identity Readable progress is not complete ownership evidence. If even one selected slot lacks a valid key, do not use another slot's unstarted render to clear firmware blame. Require every sampled selected key to name a queue before attempting host attribution. Cover a known blamed queue with an unknown peer key, in addition to wholly unknown keys and failed progress reads. This also keeps a failed key read conservative on firmware builds with a known key address. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/asahi/g17/recovery/attribution.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/asahi/g17/recovery/attribution.rs b/drivers/gpu/drm/asahi/g17/recovery/attribution.rs index be1c7ab9eb662..38ad8612a0b99 100644 --- a/drivers/gpu/drm/asahi/g17/recovery/attribution.rs +++ b/drivers/gpu/drm/asahi/g17/recovery/attribution.rs @@ -81,7 +81,10 @@ impl Sources { blamed: Option, mut started: impl FnMut(u8, u64) -> Option, ) -> Attribution { - if self.reason.is_none() || self.count as u32 != self.mask.count_ones() { + if self.reason.is_none() + || self.count as u32 != self.mask.count_ones() + || self.records[..self.count].iter().any(|source| source.qid.is_none()) + { return Attribution::Firmware; } for source in &mut self.records[..self.count] { @@ -151,6 +154,16 @@ mod tests { assert_eq!(sources.attribute(Some(38), |_, _| Some(false)), Attribution::Firmware); } + #[test] + fn a_missing_peer_key_cannot_clear_a_known_queues_blame() { + let mut sources = Sources::sample(0b11, |slot| { + Some((if slot == 0 { (38 << 40) | 1 } else { 0 }, 0)) + }); + sources.reason = Some(2); + assert_eq!(sources.count, 2); + assert_eq!(sources.attribute(Some(38), |_, _| Some(false)), Attribution::Firmware); + } + #[test] fn page_fault_keeps_firmware_blame_with_complete_keys() { let mut sources = Sources::sample(0b11, |slot| { From 078d744a260cb923d0ddfcc2b58ce8903be5a96d Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Mon, 26 Oct 2026 23:58:01 -0400 Subject: [PATCH 229/388] drm/asahi: kill faulted G17 contexts before freeing their render work When a compute command fails, the firmware holds the commands its context published after it and never runs them; only a kill of the context returns them. The context's render work behind the same failure behaves differently. It runs about a second later, consumes its ring entries and returns its free-list blocks, but the firmware never stores its completion stamp and leaves the job linked in the context's work list until it processes a kill. None of that work could retire, so a closed context whose compute failed kept its pools, its render pairs, its memory and its scheduler teardown until the GPU firmware stopped. Let the kill proceed for such a context. A quarantined render pair whose commands all ran (both pass timestamps written, rings consumed, free list balanced) counts as idle for the kill and is held for it, and the kill no longer waits for render pools that are only released after it. A faulted compute queue with no commands behind the failure is a kill candidate as well. This applies only once the owner has closed; a live owner's quarantined pair stays quarantined. Free only after the firmware acknowledges the kill. The pair's unstamped work is retired once the context's scheduler page reports the kill, and its graph memory stays allocated and out of the reuse cache until then. The detach path enforces the same rule: a context whose work list still links a node and has no acknowledged kill never has its graphs freed or recycled. The firmware leaves a killed context's last job linked even after the kill, so the context's 32 KiB work object is kept out of reuse until the firmware stops; debugfs counts these as firmware_linked_objects and a warning is printed past 256. Reuse a killed context's render pairs last. The kill hold on them ends when the context's scheduler release is consumed. A new owner then takes a clean released pair first, then an unowned slot while the physical queue table still has room for both of its identities, and only then the killed pair released longest ago. Siblings and spares never take killed pairs, and a sibling no longer claims an unowned slot once the table is full. Admission is told whether the table has room whenever the runtime changes it. Publish a failed context's kill only while no other context has a compute command in flight, or once the GPU has retired nothing for the liveness interval. When GPU memory comes within an eighth of the device limit, at most one waiting kill per 250 ms skips that wait. debugfs reports the waits and the pressure kills, and a wait over 5 s is warned about once per kill. The order matters. Earlier versions retired and freed the pair first and killed afterwards, and the firmware then used memory that had been handed out again: GPU recoveries 4-7 ms after the kill and occasional firmware crashes when the kill followed within milliseconds, and a storm of compute watchdog timeouts when it followed minutes later. A run that handed killed pairs to new owners first stranded the teardowns of 24 contexts, and a render stall behind a failed context took the desktop down. Reusing them last without regard to the queue table ran out of physical queues after about 50 kills. A kill published beside other clients' running compute made the firmware recover the GPU twice in ten minutes of a five-lane stress load, which the wait avoids. Waiting without a bound let the dead clients' memory reach the device limit under sustained compute, and gnome-shell then crashed on ENOMEM, which the memory escape avoids. On Neo, ten minutes of a five-lane stress load (render, render2, long ALU, churn and global-priority CTS beside two ray query geometry-shader stress loops) had 52 faults and 15 kills (2 under memory pressure), all acknowledged and their pairs retired. There were 0 GPU recoveries and nothing was left pending afterwards, against 12 teardowns left pending without this change. Ten minutes of two ray query stress loops beside heavy geometry had 51 faults and 51 kills (3 under memory pressure), with 0 recoveries, the physical queue table full at 127 of 128 with no refusals, and 4 render-only teardowns left pending. In both, GPU memory peaked at 4.3 GiB of the 5.0 GiB device limit, no allocation was refused and gnome-shell stayed up. Known limits: - A killed context's 32 KiB work object stays allocated until the GPU firmware restarts. - While other clients keep compute running, a faulted client's memory stays held until its kill publishes: at the next moment without foreign compute, or through the memory escape near the device limit. - A closed context whose compute side is clean but whose render pair holds executed, unstamped work is not killed, since no compute queue is left to carry the kill. A kill that doesn't need a compute exit candidate would close this. - A kill that is safe beside running compute would remove the wait. It is not understood yet. An asahi_g17_teardown tracepoint records fault, unstamped retire, kill, acknowledgement, detach and slot-owner events. --- drivers/gpu/drm/asahi/g17/context.rs | 38 ++++ drivers/gpu/drm/asahi/g17/event.rs | 1 + drivers/gpu/drm/asahi/g17/kick.rs | 5 + drivers/gpu/drm/asahi/g17/object.rs | 87 ++++++++- drivers/gpu/drm/asahi/g17/queue/admission.rs | 173 ++++++++++++++++-- drivers/gpu/drm/asahi/g17/queue/compute.rs | 12 ++ .../gpu/drm/asahi/g17/queue/compute/exit.rs | 25 ++- drivers/gpu/drm/asahi/g17/queue/render.rs | 48 ++++- .../drm/asahi/g17/queue/render/retirement.rs | 28 +++ drivers/gpu/drm/asahi/g17/runtime.rs | 7 + .../gpu/drm/asahi/g17/runtime/compute_exit.rs | 138 +++++++++++++- drivers/gpu/drm/asahi/g17/runtime/render.rs | 28 ++- .../gpu/drm/asahi/g17/runtime/render/exit.rs | 13 ++ .../drm/asahi/g17/runtime/render/submit.rs | 3 +- .../drm/asahi/g17/runtime/render/teardown.rs | 35 +++- drivers/gpu/drm/asahi/g17/runtime/teardown.rs | 12 ++ drivers/gpu/drm/asahi/g17/trace.c | 6 + drivers/gpu/drm/asahi/g17/trace.h | 19 ++ drivers/gpu/drm/asahi/g17/trace.rs | 10 + drivers/gpu/drm/asahi/g17/trace_api.h | 1 + 20 files changed, 654 insertions(+), 35 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/context.rs b/drivers/gpu/drm/asahi/g17/context.rs index 0d3948a2b69ff..ba74140d4f243 100644 --- a/drivers/gpu/drm/asahi/g17/context.rs +++ b/drivers/gpu/drm/asahi/g17/context.rs @@ -280,6 +280,28 @@ impl Context { _work: self.work.clone(), } } + /// Called once the firmware consumed this context's scheduler release. If its work list + /// still links a node, the firmware keeps referencing that node: on Neo a render command + /// that ran behind a failed compute command is never unlinked, and a later context given + /// the same work storage (the object cache hands it out again) made the firmware recover + /// the GPU. Keep the storage mapped and out of reuse until firmware stop; returns whether + /// it was linked. + pub(crate) fn retain_linked_work(&self, pool: &Arc) -> bool { + // An unreadable head counts as linked: retaining is the safe side. + let linked = self.work_head().map_or(true, |head| head != [0, self.work_head_va()]); + if linked { + self.work.lock().object.retain_until_stop(pool); + } + linked + } + + /// The firmware may still walk this context's work list into memory it names: a node is + /// linked and the scheduler page shows no kill. Unreadable state counts as linked. + pub(crate) fn firmware_may_link(&self) -> bool { + let killed = super::queue::compute::exit::State::read(self).map_or(0, |state| state.killed); + must_retain(self.work_head().ok(), self.work_head_va(), killed) + } + pub(crate) fn work_head_va(&self) -> u64 { self.work_base + (UAT_PGSZ + 0x400) as u64 } @@ -463,10 +485,26 @@ impl QosIds { } } +/// Memory a context's work list may lead the firmware to stays allocated: a linked node (or +/// an unreadable head) and no acknowledged kill. +fn must_retain(head: Option<[u64; 2]>, head_va: u64, killed: u32) -> bool { + killed == 0 && head.is_none_or(|head| head != [0, head_va]) +} + #[kunit_tests(asahi_g17_qos_capacity)] mod capacity_tests { use super::*; + #[test] + fn linked_unkilled_work_is_retained() { + let va = 0xfffffc20_0000_4400; + assert!(!must_retain(Some([0, va]), va, 0)); + assert!(must_retain(Some([0xfffffc20_0000_0000, 0xfffffc20_0000_0098]), va, 0)); + assert!(must_retain(None, va, 0)); + assert!(!must_retain(Some([0xfffffc20_0000_0000, 0xfffffc20_0000_0098]), va, 1)); + assert!(!must_retain(None, va, 1)); + } + #[test] fn failed_client_construction_returns_every_qos_id() { let ids = QosIds::new().unwrap(); diff --git a/drivers/gpu/drm/asahi/g17/event.rs b/drivers/gpu/drm/asahi/g17/event.rs index e726a75602b86..a175e58392379 100644 --- a/drivers/gpu/drm/asahi/g17/event.rs +++ b/drivers/gpu/drm/asahi/g17/event.rs @@ -75,6 +75,7 @@ fn batch(host: &mut H, role: Role, masks: &mut [u64; 2]) -> Result { Event::FreeListGrow(completion) => host.complete_freelist_grow(completion)?, Event::ContextKilled(cookie) => host.context_killed(cookie), Event::ComputeFault { qid, stamp } => { + crate::g17::trace::teardown(1, u64::from(qid), u64::from(stamp), 0); host.compute_fault(qid, stamp); // Its queue retires or settles its quarantine on this pass. masks[usize::from(qid) / 64] |= 1 << (qid % 64); diff --git a/drivers/gpu/drm/asahi/g17/kick.rs b/drivers/gpu/drm/asahi/g17/kick.rs index a1f8f70f597d4..b5fcd560191d7 100644 --- a/drivers/gpu/drm/asahi/g17/kick.rs +++ b/drivers/gpu/drm/asahi/g17/kick.rs @@ -145,6 +145,11 @@ impl QueueIds { self.render_ids() + (QID_COUNT - self.floor - self.leased) >= 2 * queues + 2 } + /// Whether a render pair not yet built can still reserve both of its identities. + pub(crate) fn render_pair_fits(&self) -> bool { + QID_COUNT - self.floor - self.leased >= 2 + } + /// Whether a fresh compute identity fits beside the render reserve of /// `queues` open logical queues, without reserving it. pub(crate) fn compute_fits(&self, queues: usize) -> bool { diff --git a/drivers/gpu/drm/asahi/g17/object.rs b/drivers/gpu/drm/asahi/g17/object.rs index f1b22ad5d7331..7cc13dbba705e 100644 --- a/drivers/gpu/drm/asahi/g17/object.rs +++ b/drivers/gpu/drm/asahi/g17/object.rs @@ -466,8 +466,17 @@ struct PoolInner { reserved: [usize; POOL_FAMILIES], bytes: usize, stopped: bool, + /// Objects the firmware may still reference after their owner dropped them, kept mapped + /// and out of reuse until firmware stop (see [`PooledObject::retain_until_stop`]). + kept: KVec, + kept_bytes: usize, + /// Kept objects still owned by their users, whose list entries are reserved. + kept_reserved: usize, } +/// Retained objects past this many are still retained, with one warning. +const KEPT_WARN: usize = 256; + /// Per-device cache-safe object recycling. Every converted mapping reserves /// one return slot before its page-table attributes change. Active leases and /// poisoned mappings count against the same capacity as reusable objects. @@ -490,6 +499,7 @@ struct CacheLease { pub(crate) struct PooledObject { object: core::mem::ManuallyDrop, lease: Option, + keep: Option>, } impl core::ops::Deref for PooledObject { @@ -503,11 +513,28 @@ impl core::ops::DerefMut for PooledObject { &mut self.object } } +impl PooledObject { + /// The firmware may still reference this object after its owner drops it: keep it mapped + /// and never hand it out again before firmware stop. It leaves the reuse cache, so it + /// does not take cache capacity. Call from sleepable context: this reserves the list + /// entry, so the drop, wherever it happens, does not allocate. + pub(crate) fn retain_until_stop(&mut self, pool: &kernel::sync::Arc) { + if self.keep.is_none() { + pool.reserve_kept(); + self.keep = Some(pool.clone()); + } + } +} + impl Drop for PooledObject { fn drop(&mut self) { // SAFETY: This destructor is the only operation that moves `object` // out; ManuallyDrop prevents a second implicit drop of the field. let object = unsafe { core::mem::ManuallyDrop::take(&mut self.object) }; + if let Some(pool) = self.keep.take() { + pool.keep_object(object, self.lease.take()); + return; + } match self.lease.take() { Some(lease) => lease.pool.return_object(object, &lease), None => drop(object), @@ -523,7 +550,10 @@ impl Pool { } kernel::sync::Arc::pin_init( pin_init!(Self { - inner <- kernel::new_mutex!(PoolInner { returned, reserved: [0; POOL_FAMILIES], bytes: 0, stopped: false }, "g17::object::Pool"), + inner <- kernel::new_mutex!(PoolInner { + returned, reserved: [0; POOL_FAMILIES], bytes: 0, stopped: false, kept: KVec::new(), + kept_bytes: 0, kept_reserved: 0, + }, "g17::object::Pool"), }), GFP_KERNEL, ) @@ -572,6 +602,7 @@ impl Pool { return Ok(PooledObject { object: core::mem::ManuallyDrop::new(entry.object), lease: Some(lease()), + keep: None, }); } let object = alloc.kernel(size, mmu::UAT_PGSZ as u64, prot, cpu)?; @@ -596,6 +627,7 @@ impl Pool { let mut result = PooledObject { object: core::mem::ManuallyDrop::new(object), lease: reserved.then(lease), + keep: None, }; if !reserved { return Ok(result); @@ -655,10 +687,58 @@ impl Pool { core::mem::forget(object); } + /// Room for one more kept object, taken by the matching `keep_object`. A failed + /// allocation is reported by that drop, which then leaks the object instead. + fn reserve_kept(&self) { + let mut inner = self.inner.lock(); + let pending = inner.kept_reserved + 1; + if inner.kept.capacity() - inner.kept.len() < pending + && inner.kept.reserve(pending, GFP_KERNEL).is_err() + { + return; + } + inner.kept_reserved = pending; + } + + fn keep_object(&self, object: KernelObject, lease: Option) { + let mut inner = self.inner.lock(); + inner.kept_reserved = inner.kept_reserved.saturating_sub(1); + if let Some(lease) = &lease { + // The object leaves the reuse cache and its capacity. + inner.reserved[lease.family as usize] -= 1; + inner.bytes -= object.size(); + } + if inner.stopped { + drop(inner); + drop(object); + return; + } + let size = object.size(); + if inner.kept.len() == KEPT_WARN { + pr_warn!("G17 keeps {} firmware-linked objects until the GPU firmware restarts\n", KEPT_WARN); + } + // retain_until_stop reserved the entry; this never allocates. + match inner.kept.push_within_capacity(object) { + Ok(()) => inner.kept_bytes += size, + Err(error) => { + // Never unmap an object the firmware may still reference. + pr_err!("G17 could not record a firmware-linked object; leaking it\n"); + core::mem::forget(error.0); + } + } + } + /// Releases retained mappings after both firmware processors have stopped. /// Active objects returned later observe the same stop witness. pub(crate) fn stop_after_firmware(&self) { - self.inner.lock().stopped = true; + let kept = { + let mut inner = self.inner.lock(); + inner.stopped = true; + inner.kept_bytes = 0; + core::mem::take(&mut inner.kept) + }; + // Mapping teardown must not run while the pool mutex is held. + drop(kept); for index in 0..POOL_FAMILIES * POOL_SLOTS { let entry = { let mut inner = self.inner.lock(); @@ -684,6 +764,7 @@ impl Pool { inner.reserved[family], POOL_SLOTS, entries.iter().flatten().count(), entries.iter().flatten().filter(|entry| entry.poisoned).count())?; } - writeln!(f, "cached_object_bytes used={} total={}", inner.bytes, POOL_BYTES) + writeln!(f, "cached_object_bytes used={} total={}", inner.bytes, POOL_BYTES)?; + writeln!(f, "firmware_linked_objects kept={} bytes={}", inner.kept.len(), inner.kept_bytes) } } diff --git a/drivers/gpu/drm/asahi/g17/queue/admission.rs b/drivers/gpu/drm/asahi/g17/queue/admission.rs index e1e7552c243f2..b2083e50aef14 100644 --- a/drivers/gpu/drm/asahi/g17/queue/admission.rs +++ b/drivers/gpu/drm/asahi/g17/queue/admission.rs @@ -71,11 +71,19 @@ struct State { /// their own fence with EBUSY instead of a device-loss error, so the VM stays usable. spared: u64, kill_hold: u64, + /// Slots whose kill hold ended at their killed context's consumed scheduler release, + /// and when. A new owner takes such a pair last, the one released longest ago first. + killed: u64, + killed_at: [u64; cfg::RENDER_SLOTS], + /// The physical queue table still has room for a pair built in an unowned slot. The + /// runtime updates this whenever the table changes. + build_room: bool, released: u64, epoch: u64, footprints: KVec, next_ticket: u64, } + impl State { const MASK: u64 = (u64::MAX >> (64 - cfg::RENDER_SLOTS)) & !1; const fn new() -> Self { @@ -87,6 +95,9 @@ impl State { quarantined: 0, spared: 0, kill_hold: 0, + killed: 0, + killed_at: [0; cfg::RENDER_SLOTS], + build_room: true, released: 0, epoch: 0, footprints: KVec::new(), @@ -120,6 +131,15 @@ impl State { self.epochs[slot] = self.epoch; } fn take_idle(&mut self, slot: usize, owner: u64) -> u8 { + if self.owners[slot] != owner { + crate::g17::trace::teardown( + 6, + slot as u64, + self.owners[slot], + owner | u64::from(self.killed & (1u64 << slot) != 0) << 63, + ); + self.killed &= !(1u64 << slot); + } self.refs[slot] += 1; self.active |= 1u64 << slot; self.released &= !(1u64 << slot); @@ -165,13 +185,23 @@ impl State { } let free = !self.active & Self::MASK & !self.quarantined & !self.kill_hold; let eligible = |slot: usize| free & (1u64 << slot) != 0; - // Reuse a closed, exactly retired owner before consuming a new graph. + let released = |slot: usize| self.released & (1u64 << slot) != 0; + let unowned = || (0..cfg::RENDER_SLOTS).find(|slot| eligible(*slot) && self.owners[*slot] == 0); + // Reuse a closed, exactly retired owner before consuming a new graph. A killed + // context's pair comes after a new graph while one can still be built: on Neo, new + // owners of such pairs had renders that executed but never stored their stamps. + // Once the queue table is full it is reused, the one released longest ago first, + // so killed pairs never strand physical queues. let slot = (0..cfg::RENDER_SLOTS) - .filter(|slot| eligible(*slot) && self.released & (1u64 << slot) != 0) + .filter(|slot| eligible(*slot) && released(*slot) && self.killed & (1u64 << slot) == 0) .min_by_key(|slot| self.epochs[*slot]) + .or_else(|| if self.build_room { unowned() } else { None }) .or_else(|| { - (0..cfg::RENDER_SLOTS).find(|slot| eligible(*slot) && self.owners[*slot] == 0) - })?; + (0..cfg::RENDER_SLOTS) + .filter(|slot| eligible(*slot) && released(*slot)) + .min_by_key(|slot| self.killed_at[*slot]) + }) + .or_else(unowned)?; let first = self.take_idle(slot, owner); self.claim_sibling(owner); Some(first) @@ -180,7 +210,8 @@ impl State { if self.owners.iter().filter(|id| **id == owner).count() >= cfg::RENDER_PAIRS_PER_OWNER { return; } - let free = !self.active & Self::MASK & !self.quarantined & !self.kill_hold; + // A speculative sibling never takes a killed context's pair. + let free = !self.active & Self::MASK & !self.quarantined & !self.kill_hold & !self.killed; let eligible = |slot: usize| free & (1u64 << slot) != 0; let sibling = (0..cfg::RENDER_SLOTS) .filter(|slot| { @@ -190,6 +221,10 @@ impl State { }) .min_by_key(|slot| self.epochs[*slot]) .or_else(|| { + // Nor an unowned slot once no pair can be built there. + if !self.build_room { + return None; + } (0..cfg::RENDER_SLOTS).find(|slot| eligible(*slot) && self.owners[*slot] == 0) }); if let Some(slot) = sibling { @@ -223,6 +258,7 @@ impl State { let bit = 1u64 << slot; let old = self.owners[slot]; if old == 0 + || self.killed & bit != 0 || owner == Some(old) || expected.is_some_and(|expected| expected != old) || self.refs[slot] != 0 @@ -648,9 +684,27 @@ impl Pool { state.quarantined &= !released; state.spared &= !released; state.kill_hold = 0; + state.killed = 0; drop(state); self.returned(); } + + /// Whether the physical queue table can hold another pair (see `State::build_room`). + pub(crate) fn note_build_room(&self, room: bool) { + let mut state = self.state.lock(); + if state.build_room != room { + state.build_room = room; + drop(state); + self.returned(); + } + } + + /// A killed context's pair is free for a new owner. + pub(crate) fn killed_pair_free(&self) -> bool { + let state = self.state.lock(); + state.killed & state.released & !state.active & !state.quarantined & !state.kill_hold & State::MASK != 0 + } + } #[kunit_tests(asahi_g17_admission_accounting)] @@ -863,7 +917,7 @@ impl Drop for RenderLease { } impl State { - fn hold_for_exit_kill(&mut self, owner: u64, slots: u64) -> bool { + fn hold_for_exit_kill(&mut self, owner: u64, slots: u64, executed: u64) -> bool { if owner == 0 || slots & !Self::MASK != 0 { return false; } @@ -872,10 +926,10 @@ impl State { if slots & bit == 0 { continue; } + // A pair whose commands all ran is held with them (see `Pair::executed_for_kill`). if self.owners[slot] != owner - || self.refs[slot] != 0 - || self.active & bit != 0 - || self.released & bit == 0 + || (executed & bit == 0 + && (self.refs[slot] != 0 || self.active & bit != 0 || self.released & bit == 0)) { return false; } @@ -887,11 +941,11 @@ impl State { impl Pool { /// Hold the departed owner's idle pairs before publishing its exit record. /// Successful publication retains these bits until both processors stop. - pub(crate) fn hold_render_slots_for_exit_kill(&self, owner: u64, slots: u64) -> bool { - self.state.lock().hold_for_exit_kill(owner, slots) + pub(crate) fn hold_render_slots_for_exit_kill(&self, owner: u64, slots: u64, executed: u64) -> bool { + self.state.lock().hold_for_exit_kill(owner, slots, executed) } - /// Undo only a hold whose control record was not published. + /// Undo a hold whose control record was not published. pub(crate) fn release_exit_kill_hold(&self, slots: u64) { if slots == 0 { return; @@ -899,6 +953,26 @@ impl Pool { self.state.lock().kill_hold &= !slots; self.returned(); } + + /// End the kill hold of `slots` once their killed context's scheduler release was + /// consumed (the firmware no longer names these pairs). A new owner takes them last. + pub(crate) fn release_killed_slots(&self, slots: u64) { + let mut state = self.state.lock(); + let held = state.kill_hold & slots & State::MASK; + if held == 0 { + return; + } + let now = crate::g17::now_ns(); + state.kill_hold &= !held; + state.killed |= held; + for slot in 0..cfg::RENDER_SLOTS { + if held & (1u64 << slot) != 0 { + state.killed_at[slot] = now; + } + } + drop(state); + self.returned(); + } } impl Pool { @@ -972,13 +1046,86 @@ mod fault_capacity_tests { let bit = 1u64 << lease.slot(); pool.release_owner(1); drop(lease); - assert!(pool.hold_render_slots_for_exit_kill(1, bit)); + assert!(pool.hold_render_slots_for_exit_kill(1, bit, 0)); assert!(!pool.reassign_unused_spare(bit.trailing_zeros() as u8, Some(1), Some(2))); // A failed (unpublished) control may roll back its hold. Published kills // keep the existing processor-stop rule; this test must not weaken it. pool.release_retained_owners_after_processor_stop(); assert_eq!(pool.state.lock().kill_hold, 0); } + + #[test] + fn consumed_scheduler_release_ends_the_kill_hold() { + let pool = Pool::new(MAX_EXECUTION_CONTEXTS).unwrap(); + let lease = pool.acquire_render(1, None, u64::MAX, || Ok(())).unwrap(); + let slot = lease.slot(); + let bit = 1u64 << slot; + pool.release_owner(1); + drop(lease); + assert!(pool.hold_render_slots_for_exit_kill(1, bit, 0)); + assert!(!pool.reassign_unused_spare(slot, Some(1), Some(2))); + pool.release_killed_slots(bit); + assert_eq!(pool.state.lock().kill_hold, 0); + assert_eq!(pool.state.lock().killed, bit); + assert!(pool.killed_pair_free()); + // The killed context's pair is never reassigned as a spare, and a new owner takes + // a new graph before it. + assert!(!pool.reassign_unused_spare(slot, Some(1), Some(2))); + let next = pool.acquire_render(2, None, u64::MAX, || Ok(())).unwrap(); + assert_ne!(next.slot(), slot); + drop(next); + pool.release_retained_owners_after_processor_stop(); + assert_eq!(pool.state.lock().killed, 0); + } + + #[test] + fn killed_pairs_are_taken_last_oldest_first() { + let pool = Pool::new(MAX_EXECUTION_CONTEXTS).unwrap(); + { + let mut state = pool.state.lock(); + for slot in 1..cfg::RENDER_SLOTS { + state.owners[slot] = 100 + slot as u64; + state.released |= 1u64 << slot; + state.killed |= 1u64 << slot; + state.killed_at[slot] = 1000 - slot as u64; + } + // One pair released without a kill. + state.killed &= !(1u64 << 7); + } + let first = pool.acquire_render(1, None, u64::MAX, || Ok(())).unwrap(); + assert_eq!(first.slot(), 7); + let second = pool.acquire_render(2, None, u64::MAX, || Ok(())).unwrap(); + assert_eq!(usize::from(second.slot()), cfg::RENDER_SLOTS - 1); + assert_eq!(pool.state.lock().killed & (1u64 << second.slot()), 0); + } + + #[test] + fn a_full_queue_table_reuses_killed_pairs_before_unowned_slots() { + let pool = Pool::new(MAX_EXECUTION_CONTEXTS).unwrap(); + { + let mut state = pool.state.lock(); + for slot in 1..8 { + state.owners[slot] = 100 + slot as u64; + state.released |= 1u64 << slot; + state.killed |= 1u64 << slot; + state.killed_at[slot] = 1000 - slot as u64; + } + } + // With room in the queue table a new owner builds a pair in an unowned slot. + let built = pool.acquire_render(1, None, u64::MAX, || Ok(())).unwrap(); + assert!(usize::from(built.slot()) >= 8); + // Without room it takes the killed pair released longest ago. + pool.note_build_room(false); + let reused = pool.acquire_render(2, None, u64::MAX, || Ok(())).unwrap(); + assert_eq!(reused.slot(), 7); + assert_eq!(pool.state.lock().killed & (1u64 << 7), 0); + // It claims no sibling: neither another killed pair nor an unowned slot. + let state = pool.state.lock(); + for slot in 1..7 { + assert_eq!(state.owners[slot], 100 + slot as u64); + } + assert_eq!(state.owners.iter().filter(|owner| **owner == 2).count(), 1); + } } impl Pool { diff --git a/drivers/gpu/drm/asahi/g17/queue/compute.rs b/drivers/gpu/drm/asahi/g17/queue/compute.rs index 3d5e12be64842..68fd03c42b42b 100644 --- a/drivers/gpu/drm/asahi/g17/queue/compute.rs +++ b/drivers/gpu/drm/asahi/g17/queue/compute.rs @@ -566,6 +566,9 @@ pub(crate) struct Queue { replay_error: Option, replay_prepared: bool, exit: Option, + /// Since when the kill of this failed queue's context waits for other contexts' compute, + /// and whether that wait was reported. + kill_wait: Option<(u64, bool)>, installed: bool, /// Bit `p` set once a work slot of priority class `p` named this queue. outer_started: u8, @@ -691,6 +694,7 @@ impl Queue { replay_error: None, replay_prepared: false, exit: None, + kill_wait: None, installed: false, outer_started: 0, published_config: None, @@ -732,6 +736,14 @@ impl Queue { } } + /// The firmware failed a command of this quarantined queue, and the queue still waits for + /// its retirement witness: commands published after the failed one, which the firmware + /// never runs, or the failed command itself, whose ring entry it does not consume (a ray + /// traversal's, on Neo). Only a kill of the queue's context gives them back. + pub(crate) fn fault_pending(&self) -> bool { + self.fault_stamp.is_some() && self.quarantined && self.awaiting_witness && self.exit.is_none() + } + /// The stamp that proves retirement: the stored one, or a newer reported fault's. fn witness_stamp(&self) -> Result { let stored = self.graph.stamp()?; diff --git a/drivers/gpu/drm/asahi/g17/queue/compute/exit.rs b/drivers/gpu/drm/asahi/g17/queue/compute/exit.rs index d7386e335e167..ae812dd41fc12 100644 --- a/drivers/gpu/drm/asahi/g17/queue/compute/exit.rs +++ b/drivers/gpu/drm/asahi/g17/queue/compute/exit.rs @@ -83,11 +83,12 @@ impl Transaction { } impl Queue { - /// A command with ray traversal scratch is never killed. On Neo, killing a + /// A running command with ray traversal scratch is never killed. On Neo, killing a /// traversal that stopped making progress either crashed the GPU firmware /// when the queue's pool was then released, or stalled every later render /// when it was retained; left alone, the rest of the GPU kept working. Such - /// a queue stays quarantined and retained until its commands retire. + /// a queue stays quarantined and retained until its commands retire. A + /// traversal the firmware reported failed no longer runs (see `fault_pending`). fn active_traversal(&self) -> bool { self.active.iter().any(|active| { matches!( @@ -101,7 +102,7 @@ impl Queue { && self.released && self.active.len != 0 && self.exit.is_none() - && !self.active_traversal() + && (!self.active_traversal() || self.fault_pending()) && (self.quarantined || self.retired_by_teardown) && age >= if self.quarantined { @@ -124,6 +125,23 @@ impl Queue { .and_then(|active| active.ticket.map(|ticket| ticket.owner)) }) } + /// The kill of this queue's failed context waits; returns how long it has waited, and + /// whether this is the first time it crossed `slow`. + pub(crate) fn note_kill_wait(&mut self, now: u64, slow: u64) -> (u64, bool) { + let (since, warned) = self.kill_wait.get_or_insert((now, false)); + let waited = now.saturating_sub(*since); + let crossed = waited >= slow && !*warned; + *warned |= crossed; + (waited, crossed) + } + /// How long the kill of this queue's failed context has been waiting, if it is. + pub(crate) fn kill_waiting(&self, now: u64) -> Option { + self.kill_wait.map(|(since, _)| now.saturating_sub(since)) + } + /// The kill was published: how long it waited, if it did. + pub(crate) fn take_kill_wait(&mut self, now: u64) -> u64 { + self.kill_wait.take().map_or(0, |(since, _)| now.saturating_sub(since)) + } pub(crate) fn record_exit(&mut self, cookie: u64, now: u64) { self.exit = Some(Transaction { cookie, @@ -162,6 +180,7 @@ impl Queue { return false; } exit.phase = Phase::Acknowledged; + crate::g17::trace::teardown(4, cookie, u64::from(self.qid()), 0); true } pub(crate) fn settle_exit( diff --git a/drivers/gpu/drm/asahi/g17/queue/render.rs b/drivers/gpu/drm/asahi/g17/queue/render.rs index 696a1486eb0ec..bcf10f97fa2d7 100644 --- a/drivers/gpu/drm/asahi/g17/queue/render.rs +++ b/drivers/gpu/drm/asahi/g17/queue/render.rs @@ -1614,7 +1614,43 @@ impl Pair { let counters = self.free_list.counters()?; let stamps = host.stamps(self.qids())?; let (_, state) = host.epoch()?; - Ok(retirement::drained(self.ordinal, stamps, counters, state)) + Ok(retirement::drained(self.ordinal, stamps, counters, state) + || (state == 0 && self.executed_unstamped() && self.context_killed())) + } + + /// The firmware acknowledged a kill of this pair's context (scheduler page): it no longer + /// walks that context's work list, so the commands it never unlinked can be released. + fn context_killed(&self) -> bool { + crate::g17::queue::compute::exit::State::read(&self.memory.context).is_ok_and(|state| state.killed != 0) + } + + /// The pair's context may be killed although its commands were not retired: they all ran + /// (`executed_unstamped`), so the kill leaves nothing queued for the firmware to run. + /// Its memory stays until the kill is acknowledged. + pub(crate) fn executed_for_kill(&self, context: &Arc) -> bool { + Arc::ptr_eq(context, &self.memory.context) && self.executed_unstamped() + } + + /// Every command of this quarantined pair ran, its ring entries were consumed and its + /// free-list blocks returned, yet the firmware never stores the stamp of the last one. + /// On Neo this is the render work of a context whose compute command failed: it runs + /// about a second after the failure, but its stamp never arrives and it stays linked in + /// the firmware's job list, so neither the stamp proof nor the abandoned-group proof + /// ever holds and the pair would stay quarantined, keeping its context, until the + /// processors stop. + fn executed_unstamped(&self) -> bool { + self.quarantined + // Only for a closed owner: a live owner would go on publishing into a context + // whose work list still links the unstamped node. + && self.owner.is_none() + && self.prepared.is_none() + && self.active.len() != 0 + && self.free_list.counters().is_ok_and(|counters| counters[0] == counters[1]) + && self.active.iter().all(|active| { + active.outer_published + && observation::snapshot(&self.memory, active.ticket.ordinal) + .is_ok_and(retirement::Snapshot::executed_and_consumed) + }) } /// Account one witnessed tracker, preserving all completion ownership. @@ -1628,6 +1664,10 @@ impl Pair { let qids = self.qids(); let qos = self.qos_owners(); let buffer = self.buffer_id(); + // Commands the firmware never unlinked are released only after it acknowledged the + // kill of their context, on every path (see `firmware_drained`). + let unstamped = self.executed_unstamped(); + let killed = unstamped && self.context_killed(); let Some(active) = self.active.iter_mut().find(|a| !a.flist_completed) else { return Ok(false); }; @@ -1638,6 +1678,12 @@ impl Pair { }; let result = match retirement::poll(&mut active.ticket, &mut observer)? { retirement::Poll::Complete(ticks) => Ok(ticks), + // Its commands ran without a stamp (see `executed_unstamped`): they already failed + // with the quarantine, and the client sees them cancelled. + retirement::Poll::Pending | retirement::Poll::Retry if killed => { + crate::g17::trace::teardown(2, u64::from(self.slot), active.ticket.ordinal, 0); + Err(ECANCELED) + } retirement::Poll::Pending | retirement::Poll::Retry => { core::sync::atomic::fence(core::sync::atomic::Ordering::Acquire); let snapshot = observer.snapshot(active.ticket.ordinal)?; diff --git a/drivers/gpu/drm/asahi/g17/queue/render/retirement.rs b/drivers/gpu/drm/asahi/g17/queue/render/retirement.rs index 00e4997ef3661..7996970ee369f 100644 --- a/drivers/gpu/drm/asahi/g17/queue/render/retirement.rs +++ b/drivers/gpu/drm/asahi/g17/queue/render/retirement.rs @@ -127,6 +127,12 @@ impl Snapshot { pub(crate) fn untouched(self) -> bool { self.pass == [0, 0] } + + /// The command ran (tiler start and fragment end written) and the firmware consumed both + /// queues' ring entries. + pub(crate) fn executed_and_consumed(self) -> bool { + self.executed() && self.cursors.iter().all(|cursors| cursors.done == cursors.write) + } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -355,3 +361,25 @@ impl Tracker { .filter_map(Option::as_mut) } } + +#[kunit_tests(asahi_g17_render_retirement)] +mod executed_tests { + use super::*; + + fn snapshot(done: u32, write: u32, pass: [u64; 2]) -> Snapshot { + Snapshot { + cursors: [Cursors { done, read: done, write }; 2], + job_list_empty: false, + pass, + } + } + + #[test] + fn executed_and_consumed_needs_both_passes_and_consumed_rings() { + assert!(snapshot(3, 3, [1, 2]).executed_and_consumed()); + assert!(!snapshot(3, 3, [0, 0]).executed_and_consumed()); + assert!(!snapshot(3, 3, [1, 0]).executed_and_consumed()); + assert!(!snapshot(3, 3, [0, 2]).executed_and_consumed()); + assert!(!snapshot(2, 3, [1, 2]).executed_and_consumed()); + } +} diff --git a/drivers/gpu/drm/asahi/g17/runtime.rs b/drivers/gpu/drm/asahi/g17/runtime.rs index e04295d576606..9d7564600455e 100644 --- a/drivers/gpu/drm/asahi/g17/runtime.rs +++ b/drivers/gpu/drm/asahi/g17/runtime.rs @@ -541,6 +541,7 @@ impl Registry { .qids .reserve_for(identity, DataMaster::Compute, Some(FIRST_COMPUTE_QID), queues) .map_err(|error| self.compute_admission_error(error))?; + self.note_build_room(); let reservation = ComputeReservation { slot, id, owner }; self.compute[slot] = Some(ComputeEntry { reservation, @@ -584,6 +585,7 @@ impl Registry { self.reserved(reservation)?; self.qids.cancel_unpublished(reservation.id)?; self.compute[reservation.slot] = None; + self.note_build_room(); Ok(()) } @@ -601,6 +603,7 @@ impl Registry { if unpublished { self.qids.cancel_unpublished(entry.reservation.id)?; self.compute[slot] = None; + self.note_build_room(); } Ok(()) } @@ -2373,6 +2376,10 @@ impl Registry { writeln!(f, "qos_ids used={} total=126", self.qos_ids.used())?; writeln!(f, "metrics_ids used={} total={}", self.metrics.used(), super::fw::buffer::METRICS_SLOTS)?; self.render.write_capacity(f)?; + let now = now_ns(); + let waits = || self.compute.iter().flatten().filter_map(|entry| entry.queue.as_deref()) + .filter_map(|queue| queue.kill_waiting(now)); + compute_exit::write_kill_waits(f, waits().count(), waits().max().unwrap_or(0))?; writeln!(f, "compute_slots used={} total={} reusable={} building={}", self.compute.iter().flatten().count(), self.compute.len(), self.compute.iter().flatten().filter_map(|entry| entry.queue.as_deref()).filter(|queue| queue.reusable()).count(), diff --git a/drivers/gpu/drm/asahi/g17/runtime/compute_exit.rs b/drivers/gpu/drm/asahi/g17/runtime/compute_exit.rs index bf9d023038a51..791c1fccae6ae 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/compute_exit.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/compute_exit.rs @@ -12,9 +12,57 @@ use crate::g17::{ timeout::LIVENESS_NS, Firmware, MSG_CONTROL_NOTIFY, }; -use core::sync::atomic::{fence, Ordering}; +use core::sync::atomic::{fence, AtomicU32, AtomicU64, Ordering}; use kernel::{prelude::*, sync::Arc}; +/// Waits of failed contexts' kills for a moment without other contexts' compute in flight, +/// by bucket (under 10 ms, 100 ms, 1 s, 5 s, and longer) and the longest. +static KILL_WAITS: [AtomicU32; 5] = [const { AtomicU32::new(0) }; 5]; +static KILL_WAIT_MAX_NS: AtomicU64 = AtomicU64::new(0); +const KILL_WAIT_BOUNDS_NS: [u64; 4] = [10_000_000, 100_000_000, 1_000_000_000, 5_000_000_000]; +/// A failed context's kill waiting this long is reported, once per kill and at most once per +/// this interval across kills. +const SLOW_KILL_WAIT_NS: u64 = 5_000_000_000; +static SLOW_KILL_WARNED_NS: AtomicU64 = AtomicU64::new(0); +/// Failed contexts' kills published beside other contexts' compute under GPU memory pressure, +/// and when the last one was: at most one per [`PRESSED_KILL_INTERVAL_NS`], so the memory of +/// one is freed before another is considered. +static PRESSED_KILLS: AtomicU32 = AtomicU32::new(0); +static PRESSED_KILL_NS: AtomicU64 = AtomicU64::new(0); +const PRESSED_KILL_INTERVAL_NS: u64 = 250_000_000; + +/// GPU memory is within an eighth of the device limit. A failed context holds its memory +/// until its kill is acknowledged, so kills waiting for other contexts' compute to pause can +/// push the device to its limit, and then every client's allocations fail: on Neo, +/// gnome-shell crashed on such an ENOMEM. A kill beside foreign compute risks one GPU +/// recovery instead. +fn memory_pressed() -> bool { + let ledger = &crate::memacct::LEDGER; + let limit = ledger.limit.load(Ordering::Relaxed); + limit != 0 + && ledger + .user_committed + .load(Ordering::Relaxed) + .saturating_add(ledger.kernel_resident.load(Ordering::Relaxed)) + > limit - limit / 8 +} + +fn record_kill_wait(waited: u64) { + let bucket = KILL_WAIT_BOUNDS_NS.iter().position(|bound| waited < *bound).unwrap_or(4); + KILL_WAITS[bucket].fetch_add(1, Ordering::Relaxed); + KILL_WAIT_MAX_NS.fetch_max(waited, Ordering::Relaxed); +} + +pub(in crate::g17) fn write_kill_waits(f: &mut kernel::fmt::Formatter<'_>, waiting: usize, longest: u64) + -> kernel::fmt::Result +{ + let count = |bucket: usize| KILL_WAITS[bucket].load(Ordering::Relaxed); + writeln!(f, "failed_kill_waits under_10ms={} under_100ms={} under_1s={} under_5s={} longer={} max_ms={} memory_pressed={}", + count(0), count(1), count(2), count(3), count(4), + KILL_WAIT_MAX_NS.load(Ordering::Relaxed) / 1_000_000, PRESSED_KILLS.load(Ordering::Relaxed))?; + writeln!(f, "failed_kills waiting={} longest_wait_ms={}", waiting, longest / 1_000_000) +} + impl super::Registry { /// ACK storage belongs to the already-retained transaction, so a full event /// batch cannot overflow a separate cache. No allocation or wait is needed. @@ -53,16 +101,22 @@ impl Firmware { let _ = queue.announce_exit(now, || self.primary.notify(MSG_CONTROL_NOTIFY)); } } + // Under memory pressure one kill skips the wait, at most one per interval. + let last = PRESSED_KILL_NS.load(Ordering::Relaxed); + let mut pressed = + (last == 0 || now.saturating_sub(last) >= PRESSED_KILL_INTERVAL_NS) && memory_pressed(); for index in 0..self.queues.teardown.len() { let Some((context, age)) = self.queues.teardown.closed(index, now) else { continue; }; + // A failed context's executed pairs keep their pool leases until the kill is + // acknowledged (`Pair::firmware_drained`), so its kill cannot wait for them. if !self.queues.render.teardown_idle(&context)? - || !self.queues.render.teardown_pools_released(&context) + || (!self.compute_failed(&context) && !self.queues.render.teardown_pools_released(&context)) { continue; } - if let Err(error) = self.publish_compute_exit(&context, age, now) { + if let Err(error) = self.publish_compute_exit(&context, age, now, &mut pressed) { dev_err!( self.primary.state.shared.dev.as_ref(), "Scheduler {} exit request failed: {:?}\n", @@ -74,7 +128,7 @@ impl Firmware { Ok(()) } - fn publish_compute_exit(&mut self, context: &Arc, age: u64, now: u64) -> Result { + fn publish_compute_exit(&mut self, context: &Arc, age: u64, now: u64, pressed: &mut bool) -> Result { let Some(index) = self.queues.compute.iter().position(|entry| { entry .as_ref() @@ -83,7 +137,36 @@ impl Firmware { }) else { return Ok(()); }; - if self.render_control_backpressured() || self.kill_unsafe(context, now) { + let failed = self.compute_failed(context); + if self.render_control_backpressured() { + return Ok(()); + } + let beside = failed && self.foreign_compute_running(context, now); + if failed { + if beside && !*pressed { + let queue = self.queues.compute[index] + .as_mut() + .and_then(|entry| entry.queue.as_deref_mut()); + if let Some(mut queue) = queue { + let (waited, crossed) = queue.note_kill_wait(now, SLOW_KILL_WAIT_NS); + let last = SLOW_KILL_WARNED_NS.load(Ordering::Relaxed); + if crossed + && (last == 0 || now.saturating_sub(last) >= SLOW_KILL_WAIT_NS) + && SLOW_KILL_WARNED_NS + .compare_exchange(last, now.max(1), Ordering::Relaxed, Ordering::Relaxed) + .is_ok() + { + dev_warn!( + self.primary.state.shared.dev.as_ref(), + "Scheduler {} kill has waited {} ms for other contexts' compute to pause\n", + context.id(), + waited / 1_000_000 + ); + } + } + return Ok(()); + } + } else if self.kill_unsafe(context, now) { return Ok(()); } let (owner, target) = { @@ -122,7 +205,7 @@ impl Firmware { && !self .queues .admission - .hold_render_slots_for_exit_kill(owner, hold) + .hold_render_slots_for_exit_kill(owner, hold, self.queues.render.executed_slots(context) & hold) { return Ok(()); } @@ -131,13 +214,23 @@ impl Firmware { self.queues.admission.release_exit_kill_hold(hold); return if error == EAGAIN { Ok(()) } else { Err(error) }; } - // Publication is irrevocable even if its doorbell fails. Keep both - // the killed graph and the held render identities until processor stop. + // Publication is irrevocable even if its doorbell fails. Keep the killed + // graph until processor stop, and the held render identities until this + // context's scheduler release is consumed or processors stop. let mut queue = self.queues.compute[index] .as_mut() .and_then(|entry| entry.queue.as_deref_mut()) .ok_or(EIO)?; + if failed { + record_kill_wait(queue.take_kill_wait(now)); + } + if beside { + *pressed = false; + PRESSED_KILLS.fetch_add(1, Ordering::Relaxed); + PRESSED_KILL_NS.store(now.max(1), Ordering::Relaxed); + } queue.record_exit(cookie, now); + crate::g17::trace::teardown(3, cookie, u64::from(context.id()), hold | u64::from(beside)); fence(Ordering::SeqCst); if let Err(error) = queue.announce_exit(now, || self.primary.notify(MSG_CONTROL_NOTIFY)) { dev_warn!( @@ -150,12 +243,39 @@ impl Firmware { Ok(()) } + /// Another context's compute command is in flight. On Neo, kills of failed contexts beside + /// other clients' compute occasionally made the firmware recover the GPU (twice in ten + /// minutes of a five-lane load with 67 faults), even with nothing freed before the kill's + /// acknowledgement; such a kill takes the next moment without foreign compute instead. + /// Render work is not waited for. A GPU that retired nothing for [`LIVENESS_NS`] has + /// stopped. + fn foreign_compute_running(&self, context: &Arc, now: u64) -> bool { + let stalled = crate::g17::recovery::since_retired(now).is_none_or(|since| since >= LIVENESS_NS); + !stalled + && self.queues.compute.iter().flatten().filter_map(|entry| entry.queue.as_deref()).any(|queue| { + queue.in_flight() && !queue.context().is_some_and(|bound| Arc::ptr_eq(bound, context)) + }) + } + + /// A compute command of `context` failed and the firmware holds commands behind it that it + /// never runs. `publish_compute_exit` is only reached once `teardown_idle` found all of + /// the context's render pairs idle; the pairs behind such a failure become idle when + /// their commands have executed (`Pair::firmware_drained`), so by then nothing of the + /// context runs any more. + fn compute_failed(&self, context: &Arc) -> bool { + self.queues.compute.iter().flatten().filter_map(|entry| entry.queue.as_deref()).any(|queue| { + queue.context().is_some_and(|bound| Arc::ptr_eq(bound, context)) && queue.fault_pending() + }) + } + /// A kill while other contexts' commands run makes the firmware restart the /// GPU, and repeated ones crash it; on an otherwise idle GPU it is safe. A /// closed context's commands left alone retire like any others once the GPU /// reaches them, so its queues stay quarantined and retained until then. Only /// a GPU that retired nothing for [`LIVENESS_NS`] has stopped, and then the - /// kill proceeds. + /// kill proceeds. A context whose compute failed ([`Self::compute_failed`]) + /// is not left alone, since its held commands never retire: its kill waits + /// only for other contexts' compute ([`Self::foreign_compute_running`]). fn kill_unsafe(&self, context: &Arc, now: u64) -> bool { let stalled = crate::g17::recovery::since_retired(now).is_none_or(|since| since >= LIVENESS_NS); if stalled { diff --git a/drivers/gpu/drm/asahi/g17/runtime/render.rs b/drivers/gpu/drm/asahi/g17/runtime/render.rs index b5649f059f5d2..22cd1f18876ca 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render.rs @@ -76,6 +76,9 @@ pub(super) struct RenderState { pools: pools::Pools, grow: grow::Grow, retired: KVec, + /// Graphs of released contexts the firmware may still walk (see `detach_context`): kept + /// until the firmware stops, when this state is dropped. + kept_graphs: KVec, next_pm_generation: u64, } @@ -102,6 +105,7 @@ impl RenderState { pools: pools::Pools::new()?, grow: grow::Grow::new()?, retired: KVec::with_capacity(128, GFP_KERNEL)?, + kept_graphs: KVec::new(), next_pm_generation: 1, }) } @@ -300,6 +304,7 @@ impl Registry { if let Some(pair) = entry.pair.as_ref() { self.buffers.unreserve_owner(pair.buffer_id())?; } + self.note_build_room(); } return Ok(Some(UnusedRenderSpare { _entry: entry, @@ -357,11 +362,25 @@ impl Registry { .any(|(slot, entry)| entry.is_some() && self.admission.owns_slot(owner, slot)) } + /// Tell admission whether another render pair can still be built. + pub(super) fn note_build_room(&self) { + self.admission.note_build_room(self.qids.render_pair_fits()); + } + pub(super) fn ensure_render_ids(&mut self, slot: u8, owner: u64) -> Result { if owner == 0 || slot == 0 || usize::from(slot) >= RENDER_SLOTS { return Err(EINVAL); } - if self.render.entries[usize::from(slot)].is_none() { + if self.render.entries[usize::from(slot)].is_some() { + return Ok(()); + } + let built = self.build_render_ids(slot, owner); + self.note_build_room(); + built + } + + fn build_render_ids(&mut self, slot: u8, owner: u64) -> Result { + { let identity = self.identities.reserve(2)?.start; let tiling = self.qids.reserve(identity, DataMaster::Tiling, None)?; let fragment = match self.qids.reserve(identity + 1, DataMaster::Fragment, None) { @@ -405,6 +424,13 @@ impl Registry { /// ensure_render_ids cancels a first ID if the second cannot be reserved; /// validation, identity overflow, and cancellation errors still propagate. fn prepare_render_ids(&mut self, slot: u8, owner: u64, optional: bool) -> Result { + // A full queue table refuses a sibling without scanning it on every preparation. + if optional + && self.render.entries.get(usize::from(slot)).is_some_and(Option::is_none) + && !self.qids.render_pair_fits() + { + return Ok(false); + } match self.ensure_render_ids(slot, owner) { Ok(()) => Ok(true), Err(error) if optional && error == ENOSPC => Ok(false), diff --git a/drivers/gpu/drm/asahi/g17/runtime/render/exit.rs b/drivers/gpu/drm/asahi/g17/runtime/render/exit.rs index c47110cb76fad..25e1335c5ba13 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render/exit.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render/exit.rs @@ -26,6 +26,19 @@ impl RenderState { (own, foreign, slots) } + /// Slots of `context`'s pairs whose commands all ran but stay unretired until its kill. + pub(in crate::g17::runtime) fn executed_slots(&self, context: &Arc) -> u64 { + let mut slots = 0; + for (slot, entry) in self.entries.iter().enumerate() { + if entry.as_ref().and_then(|entry| entry.pair.as_ref()) + .is_some_and(|pair| pair.executed_for_kill(context)) + { + slots |= 1u64 << slot; + } + } + slots + } + pub(in crate::g17::runtime) fn qids_for_slot(&self, slot: u8) -> u128 { self.entries .get(usize::from(slot)) diff --git a/drivers/gpu/drm/asahi/g17/runtime/render/submit.rs b/drivers/gpu/drm/asahi/g17/runtime/render/submit.rs index 1ef6742b689a7..e6379178138d1 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render/submit.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render/submit.rs @@ -131,7 +131,8 @@ impl Backend { .reclaim_unused_render_spare(Some(self.owner), true)?; let ready = firmware.queues.has_render_ids(self.owner); drop(state); - if reclaimed.is_none() && !ready { + // A full queue table makes admission reuse a free killed pair instead. + if reclaimed.is_none() && !ready && !pool.killed_pair_free() { self.wait_render_capacity(&mut observed, started, sleep)?; } drop(reclaimed); diff --git a/drivers/gpu/drm/asahi/g17/runtime/render/teardown.rs b/drivers/gpu/drm/asahi/g17/runtime/render/teardown.rs index e54b1f4638f58..18063f0cd6a14 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render/teardown.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render/teardown.rs @@ -38,7 +38,7 @@ impl RenderState { return Ok(false); } if let Some(pair) = entry.pair.as_ref() { - if !pair.idle_for_context(context)? { + if !pair.idle_for_context(context)? && !pair.executed_for_kill(context) { return Ok(false); } } @@ -123,6 +123,7 @@ impl RenderState { if !out.has_room(count, 1) { return Err(EBUSY); } + let linked = context.firmware_may_link(); for entry in self.entries.iter_mut().flatten() { if !entry .context @@ -140,7 +141,14 @@ impl RenderState { let (mut parked, retired) = KBox::into_inner(pair).park(); // This park follows the consumed release of `context`. parked.release_consumed(context.scheduler_va()); - *target = Some(retired); + if linked { + // Guard (kill before free): the firmware may still walk this context's work + // list into the graph, so it is not freed or recycled before the firmware stops. + kernel::pr_warn_once!("G17: keeping render graphs of an unkilled context that still links work\n"); + keep_graph(&mut self.kept_graphs, retired); + } else { + *target = Some(retired); + } entry.parked = Some(parked); // Detached backing retains these references until the worker runs. entry.context = None; @@ -155,8 +163,12 @@ impl RenderState { if let Some(id) = self.retired[index].buffer_id() { buffers.unreserve_owner(id)?; } - let target = out.binding_slot()?; - *target = Some(self.retired.remove(index).map_err(|_| EIO)?); + let retired = self.retired.remove(index).map_err(|_| EIO)?; + if linked { + keep_graph(&mut self.kept_graphs, retired); + } else { + *out.binding_slot()? = Some(retired); + } } Ok(()) } @@ -181,3 +193,18 @@ impl Registry { Ok(()) } } + +/// Keeps `graph` until the firmware stops; if even that cannot be recorded, it is leaked +/// rather than freed. +fn keep_graph( + kept: &mut KVec, + graph: crate::g17::queue::render::RetiredBinding, +) { + if kept.reserve(1, GFP_KERNEL).is_ok() { + if let Err(error) = kept.push_within_capacity(graph) { + core::mem::forget(error.0); + } + } else { + core::mem::forget(graph); + } +} diff --git a/drivers/gpu/drm/asahi/g17/runtime/teardown.rs b/drivers/gpu/drm/asahi/g17/runtime/teardown.rs index 16bd5828265a4..0222ae4c478c8 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/teardown.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/teardown.rs @@ -100,6 +100,8 @@ impl ReclaimBatch { struct Service<'a> { render: &'a mut super::render::RenderState, + admission: &'a crate::g17::queue::admission::Pool, + objects: &'a Arc, compute: &'a [Option], buffers: &'a mut BufferIds, init: &'a InitData, @@ -137,8 +139,15 @@ impl Host for Service<'_> { self.primary.notify(MSG_CONTROL_NOTIFY) } fn detach(&mut self, context: &Arc) -> Result { + let linked = context.retain_linked_work(self.objects); + let (_, _, slots) = self.render.exit_scope(context); + crate::g17::trace::teardown(5, u64::from(context.id()), slots, u64::from(linked)); self.render .detach_context(context, self.buffers, self.reclaim)?; + // The firmware consumed this context's scheduler release after its kill (if any) + // was acknowledged and its pairs went idle: it no longer names these pairs, so + // their kill hold ends here rather than at the next processor stop. + self.admission.release_killed_slots(slots); // detach_context preflights this slot together with every graph slot. // Retain it before Pending removes its reference under the device lock. self.reclaim.context(context) @@ -173,6 +182,7 @@ impl Firmware { self.queues.render.observe_teardown_releases(&self.init)?; let recovery = self.recovery.pending() || self.init.recovery_state()? != 0; let Registry { + admission, render, compute, buffers, @@ -181,6 +191,8 @@ impl Firmware { } = &mut self.queues; let mut service = Service { render, + admission, + objects: &self.pool, compute, buffers, init: &self.init, diff --git a/drivers/gpu/drm/asahi/g17/trace.c b/drivers/gpu/drm/asahi/g17/trace.c index 23666be389b96..9e93291f5114e 100644 --- a/drivers/gpu/drm/asahi/g17/trace.c +++ b/drivers/gpu/drm/asahi/g17/trace.c @@ -54,3 +54,9 @@ void asahi_g17_trace_event(u8 phase) if (trace_asahi_g17_event_enabled()) trace_asahi_g17_event(phase, __arch_counter_get_cntvct()); } + +void asahi_g17_trace_teardown(u8 kind, u64 a, u64 b, u64 c) +{ + if (trace_asahi_g17_teardown_enabled()) + trace_asahi_g17_teardown(kind, a, b, c); +} diff --git a/drivers/gpu/drm/asahi/g17/trace.h b/drivers/gpu/drm/asahi/g17/trace.h index 7e5a50e774b59..d1529a2775fcc 100644 --- a/drivers/gpu/drm/asahi/g17/trace.h +++ b/drivers/gpu/drm/asahi/g17/trace.h @@ -125,6 +125,25 @@ TRACE_EVENT(asahi_g17_message, __entry->counter) ); +/* Failed-context teardown and render slot reuse (kind: see g17/trace.rs teardown()). */ +TRACE_EVENT(asahi_g17_teardown, + TP_PROTO(u8 kind, u64 a, u64 b, u64 c), + TP_ARGS(kind, a, b, c), + TP_STRUCT__entry( + __field(u8, kind) + __field(u64, a) + __field(u64, b) + __field(u64, c) + ), + TP_fast_assign( + __entry->kind = kind; + __entry->a = a; + __entry->b = b; + __entry->c = c; + ), + TP_printk("kind=%u a=%#llx b=%#llx c=%#llx", __entry->kind, __entry->a, __entry->b, __entry->c) +); + TRACE_EVENT(asahi_g17_event, TP_PROTO(u8 phase, u64 counter), TP_ARGS(phase, counter), diff --git a/drivers/gpu/drm/asahi/g17/trace.rs b/drivers/gpu/drm/asahi/g17/trace.rs index b76b46425d2ce..f498de82e87c6 100644 --- a/drivers/gpu/drm/asahi/g17/trace.rs +++ b/drivers/gpu/drm/asahi/g17/trace.rs @@ -16,6 +16,7 @@ unsafe extern "C" { fn asahi_g17_trace_doorbell_enabled() -> bool; fn asahi_g17_trace_doorbell(engine: u8, power: u32); fn asahi_g17_trace_event(phase: u8); + fn asahi_g17_trace_teardown(kind: u8, a: u64, b: u64, c: u64); } pub(super) const RENDER: u8 = 0; @@ -74,6 +75,15 @@ pub(super) fn message(kind: u8, value: u64) { unsafe { asahi_g17_trace_message(kind, value) }; } +/// Failed-context teardown: 1 compute fault (qid, stamp), 2 unstamped render retire (slot, +/// ordinal), 3 kill published (cookie, context, held slots | 1 if it skipped the wait for other +/// contexts' compute under memory pressure; slot 0 is never held), 4 kill acknowledged (cookie, qid), +/// 5 detach (context, slots, linked), 6 render slot to a new owner (slot, old, new | killed << 63). +pub(crate) fn teardown(kind: u8, a: u64, b: u64, c: u64) { + // SAFETY: Plain values only. + unsafe { asahi_g17_trace_teardown(kind, a, b, c) }; +} + /// Event worker pass boundaries: 0 begin, 1 fences signalled, 2 end. pub(super) fn event(phase: u8) { // SAFETY: Plain values only. diff --git a/drivers/gpu/drm/asahi/g17/trace_api.h b/drivers/gpu/drm/asahi/g17/trace_api.h index d09f529a163f0..0049aa0079634 100644 --- a/drivers/gpu/drm/asahi/g17/trace_api.h +++ b/drivers/gpu/drm/asahi/g17/trace_api.h @@ -20,5 +20,6 @@ bool asahi_g17_trace_doorbell_enabled(void); void asahi_g17_trace_doorbell(u8 engine, u32 power); void asahi_g17_trace_message(u8 kind, u64 value); void asahi_g17_trace_event(u8 phase); +void asahi_g17_trace_teardown(u8 kind, u64 a, u64 b, u64 c); #endif /* _ASAHI_G17_TRACE_API_H */ From 6ca4244633eb9e7105685f99469332f8203a4adf Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 15:03:51 +1100 Subject: [PATCH 230/388] drm/asahi: initialize the G17 idle hold from the existing DT policy The G17 backend exported an idle-delay attribute but always started at the native two-millisecond policy, ignoring apple,idle-off-delay-ms. Use the existing DT property at probe and validate it against the same 2..1000 ms bounds as the existing attribute. Document that property in the binding. Missing policy preserves the current default; other GPUs need no platform flag or new module parameter. Exercise absent, valid and invalid policy values and depth-one gaps within a bounded sixteen-millisecond hold, followed by real idle release. This supplies a board policy through the existing power-control path; it changes neither firmware register layouts nor voltage/clock tables. Assisted-by: LLM Signed-off-by: Ryan Murray --- .../devicetree/bindings/gpu/apple,agx.yaml | 10 +++++ drivers/gpu/drm/asahi/g17/mod.rs | 6 +++ drivers/gpu/drm/asahi/g17/power.rs | 44 ++++++++++++++++++- 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/Documentation/devicetree/bindings/gpu/apple,agx.yaml b/Documentation/devicetree/bindings/gpu/apple,agx.yaml index 0cd4dc1d6fd78..97c7b4d8cb68f 100644 --- a/Documentation/devicetree/bindings/gpu/apple,agx.yaml +++ b/Documentation/devicetree/bindings/gpu/apple,agx.yaml @@ -72,6 +72,16 @@ properties: - enum: [hw-cal-b, fw-text, l2-pagetables] - enum: [globals, fw-data, fw-text] + apple,idle-off-delay-ms: + $ref: /schemas/types.yaml#/definitions/uint32 + minimum: 2 + maximum: 1000 + default: 2 + description: + Continuous idle time before GPU power-off, in milliseconds. The G17 + backend extends the firmware's native delay with its host idle-hold + control; an absent property preserves the native policy. + apple,pmp: $ref: /schemas/types.yaml#/definitions/phandle description: PMP firmware provider owning the GPU power vote and telemetry diff --git a/drivers/gpu/drm/asahi/g17/mod.rs b/drivers/gpu/drm/asahi/g17/mod.rs index e527e6fa83528..78a924782a1a2 100644 --- a/drivers/gpu/drm/asahi/g17/mod.rs +++ b/drivers/gpu/drm/asahi/g17/mod.rs @@ -1141,6 +1141,12 @@ impl Gpu { oas: cfg.uat_oas, }; let max_frequency_khz = Self::max_frequency_khz(dev)?; + let node = dev.as_ref().as_ref().of_node().ok_or(EIO)?; + let idle_delay = power::idle_delay( + node.get_opt_property(c_str!("apple,idle-off-delay-ms"))?, + )?; + power::IDLE_HOLD_MS.store(idle_delay, Ordering::Relaxed); + dev_info!(dev.as_ref(), "GPU idle power-off delay: {} ms\n", idle_delay); let channels = availability::Availability::new()?; let progress = availability::Availability::new()?; let queues = availability::Availability::new()?; diff --git a/drivers/gpu/drm/asahi/g17/power.rs b/drivers/gpu/drm/asahi/g17/power.rs index b0e21e70bbe18..bbd10b204edd7 100644 --- a/drivers/gpu/drm/asahi/g17/power.rs +++ b/drivers/gpu/drm/asahi/g17/power.rs @@ -12,10 +12,22 @@ pub(super) const IDLE_HOLD_NS: u64 = 16_000_000; /// the last job. Not measured on other firmware; at or below it the hold stays off. pub(crate) const FIRMWARE_IDLE_OFF_MS: u32 = 2; -/// The `idle_off_delay_ms` sysfs knob. Above [`FIRMWARE_IDLE_OFF_MS`] the driver keeps the GPU -/// cores powered until the GPU has been idle this long ([`Hold`]). +/// Idle policy initialized from the existing DT `apple,idle-off-delay-ms` property. +/// The existing `idle_off_delay_ms` sysfs attribute may override it. Above +/// [`FIRMWARE_IDLE_OFF_MS`] the driver keeps the GPU cores powered until it has been idle +/// this long ([`Hold`]). pub(crate) static IDLE_HOLD_MS: AtomicU32 = AtomicU32::new(FIRMWARE_IDLE_OFF_MS); +/// Use the same bounds as the existing idle-delay attribute. Missing DT policy +/// preserves the native delay; malformed or out-of-range values fail probe. +pub(super) fn idle_delay(requested: Option) -> kernel::error::Result { + let delay = requested.unwrap_or(FIRMWARE_IDLE_OFF_MS); + if !(FIRMWARE_IDLE_OFF_MS..=1000).contains(&delay) { + return Err(kernel::error::code::EINVAL); + } + Ok(delay) +} + #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub(super) enum IdleAction { None, @@ -237,6 +249,34 @@ pub(super) fn release(control: &mut C) -> kernel::error::Result { mod tests { use super::*; + #[test] + fn dt_idle_delay_is_bounded_and_absence_preserves_native_policy() { + assert_eq!(idle_delay(None), Ok(FIRMWARE_IDLE_OFF_MS)); + for delay in [2, 16, 1000] { + assert_eq!(idle_delay(Some(delay)), Ok(delay)); + } + for delay in [0, 1, 1001, u32::MAX] { + assert!(idle_delay(Some(delay)).is_err()); + } + } + + #[test] + fn frame_hold_survives_depth_one_gaps_but_releases_at_idle() { + let mut hold = Hold::new(); + assert_eq!(hold.step(0, true, 1, 16), HoldAction::Assert); + hold.asserted(); + for submission in 1..=8 { + // A completed depth-one dispatch followed by another short gap. + let now = submission * 3_000_000; + assert_eq!(hold.step(now, false, submission, 16), HoldAction::Recheck(16_000_000)); + assert!(hold.active()); + } + assert_eq!(hold.step(39_999_999, false, 8, 16), HoldAction::Recheck(1)); + assert_eq!(hold.step(40_000_000, false, 8, 16), HoldAction::Release); + hold.released(); + assert!(!hold.active()); + } + #[test] fn holds_until_idle_for_the_delay_then_releases() { let mut hold = Hold::new(); From 5440369988c80c1a415b9242f1e2c6f40f9a235c Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 15:03:52 +1100 Subject: [PATCH 231/388] arm64: dts: apple: retain J616s GPU power across short submission gaps Request sixteen milliseconds of continuous idle before releasing the existing host GPU power hold. The native two-millisecond off policy can cycle the T6040 cores between interactive submissions and each wake has a substantial fixed cost. Use the existing idle-delay DT policy, with a bounded hold matching the existing performance-effort idle interval. Long idle still sends the existing power-off control. Qualify depth-one round trips, short/long idle gaps and idle power alongside the T6040 CLPC/PMP memory-floor ports; no measured speedup is asserted here. Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/boot/dts/apple/t6040-j616s.dts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/arch/arm64/boot/dts/apple/t6040-j616s.dts b/arch/arm64/boot/dts/apple/t6040-j616s.dts index 7abbd491489d0..f5ba130e5c183 100644 --- a/arch/arm64/boot/dts/apple/t6040-j616s.dts +++ b/arch/arm64/boot/dts/apple/t6040-j616s.dts @@ -853,6 +853,8 @@ operating-points-v2 = <&gpu_opp>; apple,cs-opp = <&gpu_cs_opp>; apple,afr-opp = <&gpu_afr_opp>; + /* Avoid a core power cycle between interactive submissions, then release. */ + apple,idle-off-delay-ms = <16>; status = "okay"; }; From e6bf61f5fb42978bd2f3696b0184b0ccb9b1e078 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 15:04:46 +1100 Subject: [PATCH 232/388] docs: distinguish GFX power reporting from GPU memory performance votes Record the actual T6040 power-report, PMP voter and CLPC demand paths. Explain the bounded J616s DT idle policy and the separate latency/power acceptance gates. Update the recovery audit for the release-witness and unknown-slot ownership guards. Software reachability is not evidence of achieved clocks, an honoured memory floor or a benchmark gain. Assisted-by: LLM Signed-off-by: Ryan Murray --- GAPS.md | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/GAPS.md b/GAPS.md index dd5781b12df4d..b958e0225f815 100644 --- a/GAPS.md +++ b/GAPS.md @@ -7,8 +7,9 @@ two r6 display patches. The reconstructed r6 branch is `wip/j616s-display-r6` at `af0a664046fb`. This series repairs regression coverage and a shared KUNIT=m WERROR build defect, -and ports GPU-driven memory/fabric demand and PMP voter cooperation using -T6040 data. GPU voltages, firmware features and the submission algorithm remain +ports GPU-driven memory/fabric demand and PMP voter cooperation using +T6040 data, honors the existing DT idle-delay policy, and closes a compute +release-witness gap across recovery. GPU voltages, firmware features and the submission algorithm remain unchanged from the reconstructed base. The major T6040 calibration and image-admission changes requested by this audit are already in the baseline. Software parity does not establish benchmark or CTS @@ -24,7 +25,7 @@ parity. There are no J616s results for this series yet. | DVFS ramp / effort | Full effort on publication, cleared after 16 ms of continuous idle | Same `Firmware::note_submission`, direct-port accounting and race-checked idle clear | Same runtime behavior | `g17/mod.rs`, `g17/port.rs`, `g17/power.rs`; opaque firmware power words are not a documented ramp control | | GPU-driven fabric/memory floor | #440 lifts DCS 4/fabric 3 for GPU demand | T6040 CPU-only demand, gpu_demand=false and no power-state phandle | Port 0d63308820ab enables the shared mechanism with existing T6040 DCS 5/fabric 2 data | clpc.c and T6040 DT; g1/g2 P/C/R/H/I measurements required | | PMP PMC vote cooperation | PMP/CLPC/AP agents enabled | T6040 static voters omit PMP 0 from pre-working-PMP bring-up | Port 772a9d3104ff adds the existing PMP supplier dependency and voter 0 on T6040 DCS/SOC rails | pmc.c and T6040 DT; live votes, idle release and thermal qualification required | -| Idle / off policy | Shared host hold and firmware device-control path | Same host implementation; source documents a 25G83 effective 2 ms off delay and approximately 1 ms wake cost | Same runtime behavior; default hold unchanged | `g17/power.rs`; measure actual idle/wake latency before altering policy | +| Idle / off policy | Shared host hold and firmware device-control path | Same host implementation; source documents a 25G83 effective 2 ms off delay and approximately 1 ms wake cost | J616s requests a bounded 16 ms host hold through existing `apple,idle-off-delay-ms`; absent policy retains 2 ms | `g17/power.rs`, `g17/mod.rs`, J616s DT and binding; compare short/long gaps, depth-one latency and idle power | | Fast-die / PMP | Platform-specific firmware power graph | 25G83 AON table and separate power-state target/release (101/96 C) are already populated; DT enables the GFX suppliers | Same runtime behavior | `g17/fw/initdata/v25g83.rs::POWER_STATE_WORDS`, `g17/initdata/v25g83.rs`, J616s DT; verify clocks with PMP running | | Periodic preemption | Context-switch timer mask is zero | Same zero mask; two-cluster operand-state allocation differs by hardware layout | Same runtime behavior | `g17/fw/initdata.rs::CONTEXT_SWITCH_TIMER_MASK`, `g17/fw/compute.rs::OperandState`; no evidence supports changing the mask | | High-priority queues | HIGH / MEDIUM / LOW firmware scheduling profiles | Same scheduling-policy selector and priority work rings | Same runtime behavior | `g17/mod.rs::scheduling_policy`, `g17/fw/channels.rs`, `g17/qos.rs` | @@ -39,14 +40,40 @@ parity. There are no J616s results for this series yet. | G13/G14 idle wake regressions | Firmware-specific cost plus shared host overhead | G16S shares common ioctl/GEM/sync infrastructure, but has different firmware, power handling and queue backend | Same runtime behavior | `driver.rs`, `g17/power.rs`; G13/G14 benchmark ratios cannot be transferred to G16S | | QID capacity | Firmware floor 0; reserve protects dedicated render pairs | 25G83 reserves QIDs 0..31; public queue limit is reduced accordingly | KUnit checks floor 32 in dynamic growth and full logical queue budget tests; host resource runner follows current reserve helper | `g17/kick.rs`, `g17/runtime.rs`, `tools/asahi/g17-resource-tests.py` | | KUnit / WERROR config matrix | Shared test modules require KUNIT=y | Four G17 test helpers used the broader CONFIG_KUNIT guard and failed WERROR with the shipping KUNIT=m config | Helpers now use CONFIG_KUNIT = "y", matching the Rust macro; config and warning policy unchanged | `rust/macros/kunit.rs`, `g17/kick.rs`, `g17/preparation.rs`, `g17/queue/admission.rs`, `g17/runtime.rs`; verify both KUNIT=m release and KUNIT=y QEMU builds | -| Compute ownership / retained mappings | Recovery and retirement witnesses protect mapping lifetime | Same production queue ownership, source loans, growth rollback and deferred release | Restored host fixtures and negative controls; source loans explicitly prevent premature reuse/sharing/rearming | `tools/asahi/g17-capacity-fault-tests.py`, constructor/growth/pool/release-order fixtures | -| Restart and fault classification | Shared recovery handshake plus validated T8140 per-slot keys | Shared handshake uses 25G83 status offsets; `recovery_slot_keys=None` preserves firmware blame when keys are unknown | Same runtime behavior | `hw/t6040.rs`, `g17/initdata.rs`, `g17/recovery/attribution.rs`; successful 25G83 fault/restart qualification is still required | +| Compute ownership / retained mappings | Recovery and retirement witnesses protect mapping lifetime | Same production queue ownership, source loans, growth rollback and deferred release | Aged-record fallback now requires the original recovery generation; source loans and host negative controls remain | `tools/asahi/g17-capacity-fault-tests.py`, constructor/growth/pool/release-order fixtures | +| Restart and fault classification | Shared recovery handshake plus validated T8140 per-slot keys | Shared handshake uses 25G83 status offsets; `recovery_slot_keys=None` preserves firmware blame when keys are unknown | Bounded generation/slot/progress diagnostics retain unknown ownership; any missing selected key preserves firmware blame | `hw/t6040.rs`, `g17/initdata.rs`, `g17/recovery/attribution.rs`; successful 25G83 fault/restart qualification is still required | | DART / UAT | Shared mapping ownership and checked page-table borrowing | Shared MMU, with T6040 reserved tables, PoC cleaning, per-page TLBI and native GPU permission quirks | Same runtime behavior | `hw/t6040.rs`, `mmu.rs`, `pgtable.rs`; do not remove coherency or handoff requirements for a benchmark | | Memory accounting | Creator-attributed committed/resident GEM accounting and capacity admission | Same `memacct::Client`, `File::memory_stats` and shared capacity machinery | Same runtime behavior | `file.rs`, `gem.rs`, `memacct.rs`, `g17/capacity.rs`; verify multi-client high-water and post-close behavior on hardware | | MCache / few-primitives / ray traversal | Enabled by the 26A428 variant | Disabled by 25G83 variant; source records hangs for few-primitives and stalled ray traversal, while MCache is unqualified | Same runtime behavior | `g17/fw/variant.rs::V25G83`; enabling these requires independent ABI evidence and hardware tests | | M5 Pro comparison | No M5 Pro hardware configuration exists in this public baseline | No T605x/G17S KMD path to compare | No claim of M5 parity | Pinned public tree contains T6040 and T8140; a licensed public M5 Pro reference is required | | CTS and Linux/macOS performance ratio | Hardware results belong to their exact kernel/Mesa pair | No result for this candidate | Pending hardware agent | Run gpubench with matching suite, configuration and conditions; review CTS failures and kernel faults before claiming parity | +## GPU memory demand and interactive latency + +The GFX PMP report entry and PMC performance votes are separate mechanisms. +The existing T6040 `pmp_gfx` soc-device 13 entry follows the powered GPU domain; +`apple_pmp_report_set_state()` publishes a power-state request bit and waits +for its acknowledgement. It does not encode a DCS/fabric performance request. +The GPU firmware owns the PMP's performance demand. Before this series, PMC's +static voter list omitted PMP agent 0, and CLPC lacked a GPU power-state input. +The two existing ports enable PMP agent 0 on T6040's DCS/SOC rails and make +CLPC request the already qualified DCS 5 / fabric 2 floor while graphics is +powered. They preserve the existing 32 ms memory-floor release. Prove live +request, enabled-agent and actual-state agreement; supplier binding alone +is not proof of an honoured vote. + +For dispatch latency, G17 now reads the existing `apple,idle-off-delay-ms` DT +policy, validates the existing 2..1000 ms bounds, and starts the existing host +hold with that policy. J616s selects 16 ms, matching the existing effort-idle +interval; other boards without the property retain the native 2 ms behavior. +This avoids power-off across short interactive gaps and releases after real +idle, without changing firmware layout or adding a parameter/platform flag. +KUnit covers absent/valid/invalid policy and repeated depth-one gaps followed +by idle release. A shorter software delay is not itself a measured latency +win: compare actual power transitions, firmware execution, IRQ/retirement and +fence wakeup boundaries. Keep empty CPU fence transfers separate from GPU +round trips and require low-duty/long-idle power acceptance. + ## Remaining 25G83 facts and recovery qualification No firmware capability is inferred from another GPU's successful CTS result. From 09f4a9bb81190c3029d446356771008d8ad28c83 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 15:09:18 +1100 Subject: [PATCH 233/388] dt-bindings: gpu: use the standard millisecond property type The DT meta-schema supplies the type for a standard -ms unit suffix. Drop the redundant uint32 reference on apple,idle-off-delay-ms so the binding passes validation while retaining its bounds and description. Assisted-by: LLM Signed-off-by: Ryan Murray --- Documentation/devicetree/bindings/gpu/apple,agx.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/Documentation/devicetree/bindings/gpu/apple,agx.yaml b/Documentation/devicetree/bindings/gpu/apple,agx.yaml index 97c7b4d8cb68f..145e277d6be4d 100644 --- a/Documentation/devicetree/bindings/gpu/apple,agx.yaml +++ b/Documentation/devicetree/bindings/gpu/apple,agx.yaml @@ -73,7 +73,6 @@ properties: - enum: [globals, fw-data, fw-text] apple,idle-off-delay-ms: - $ref: /schemas/types.yaml#/definitions/uint32 minimum: 2 maximum: 1000 default: 2 From 72771fbd90f04f17fef4a8bdd41cc29611c2e42f Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Tue, 27 Oct 2026 01:08:00 -0400 Subject: [PATCH 234/388] drm/asahi: kill G17 contexts left with only executed render work A closed context whose failed compute command was retired on the firmware's fault record has a clean compute side, yet the render work it queued behind the failure ran without storing its stamp and stays linked in the context's work list. Such a pair is released only after a kill of its context, and that kill was only ever published through one of the context's compute queues, of which none is left to carry it. The context, its pairs and its memory therefore stayed until the GPU firmware stopped: 4 contexts after ten minutes of ray query stress beside heavy geometry. Publish the kill for such a context on its render pairs' behalf. A context qualifies once it has been closed for 250 ms, every pair of it is idle or has executed all of its commands, at least one has, and no compute queue still runs it, switches away from it or has published its kill. The kill names one of the context's own render queues that its scheduler page lists and is refused if the page names a queue of another context; the pairs it holds must have a single admission owner. The rules of a failed context's kill apply unchanged: the pairs are held for the kill, their memory is released only once the scheduler page reports it, the kill waits for a moment without other contexts' compute unless GPU memory is pressed, and the held pairs become killed pairs, reused last, once the context's scheduler release is consumed. The teardown entry keeps the kill's cookie, so only that exact cookie acknowledges it, and its doorbell is retried without duplicating the record. debugfs reports render-only kills, their acknowledgements, the contexts refused by first reason and the kills waiting. On Neo, ten minutes of two ray query stress loops beside heavy geometry had 53 faults, 53 kills through compute queues and 4 render-only kills, all acknowledged, with 0 GPU recoveries and nothing left pending afterwards (4 teardowns before). Ten minutes of the five-lane stress load had 48 faults, 8 kills through compute queues and 2 render-only kills, all acknowledged, with 0 GPU recoveries and nothing left pending. No render-only kill was refused in either run. The asahi_g17_teardown tracepoint records render-only kills as kind 7 and their acknowledgements with queue 0x100. --- .../gpu/drm/asahi/g17/queue/compute/exit.rs | 4 + drivers/gpu/drm/asahi/g17/runtime.rs | 2 + .../gpu/drm/asahi/g17/runtime/compute_exit.rs | 201 +++++++++++++++++- .../gpu/drm/asahi/g17/runtime/render/exit.rs | 8 + drivers/gpu/drm/asahi/g17/teardown.rs | 148 +++++++++++++ drivers/gpu/drm/asahi/g17/trace.rs | 5 +- 6 files changed, 360 insertions(+), 8 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/queue/compute/exit.rs b/drivers/gpu/drm/asahi/g17/queue/compute/exit.rs index ae812dd41fc12..719a74787ca70 100644 --- a/drivers/gpu/drm/asahi/g17/queue/compute/exit.rs +++ b/drivers/gpu/drm/asahi/g17/queue/compute/exit.rs @@ -111,6 +111,10 @@ impl Queue { CANCELLED_DELAY_NS } } + /// A kill of `context` was published through this queue. + pub(crate) fn exit_published(&self, context: &Arc) -> bool { + self.exit.is_some() && self.context().is_some_and(|bound| Arc::ptr_eq(bound, context)) + } pub(crate) fn exit_owner(&self, context: &Arc) -> Option { if !self .active diff --git a/drivers/gpu/drm/asahi/g17/runtime.rs b/drivers/gpu/drm/asahi/g17/runtime.rs index 9d7564600455e..693073e0c90cc 100644 --- a/drivers/gpu/drm/asahi/g17/runtime.rs +++ b/drivers/gpu/drm/asahi/g17/runtime.rs @@ -2380,6 +2380,8 @@ impl Registry { let waits = || self.compute.iter().flatten().filter_map(|entry| entry.queue.as_deref()) .filter_map(|queue| queue.kill_waiting(now)); compute_exit::write_kill_waits(f, waits().count(), waits().max().unwrap_or(0))?; + compute_exit::write_render_kills(f, self.teardown.kill_waits(now).count(), + self.teardown.kill_waits(now).max().unwrap_or(0))?; writeln!(f, "compute_slots used={} total={} reusable={} building={}", self.compute.iter().flatten().count(), self.compute.len(), self.compute.iter().flatten().filter_map(|entry| entry.queue.as_deref()).filter(|queue| queue.reusable()).count(), diff --git a/drivers/gpu/drm/asahi/g17/runtime/compute_exit.rs b/drivers/gpu/drm/asahi/g17/runtime/compute_exit.rs index 791c1fccae6ae..c8e6de387d91c 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/compute_exit.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/compute_exit.rs @@ -31,6 +31,47 @@ static PRESSED_KILLS: AtomicU32 = AtomicU32::new(0); static PRESSED_KILL_NS: AtomicU64 = AtomicU64::new(0); const PRESSED_KILL_INTERVAL_NS: u64 = 250_000_000; +/// Render-only kills (see `Firmware::publish_render_kill`): published, acknowledged, and +/// contexts refused (each counted once, by its first reason) because the scheduler page names +/// a queue outside the context, because the held pairs have no single owner, or because +/// admission could not hold them. +static RENDER_KILLS: [AtomicU32; 5] = [const { AtomicU32::new(0) }; 5]; +const RENDER_KILL_PUBLISHED: usize = 0; +const RENDER_KILL_ACKNOWLEDGED: usize = 1; +const RENDER_KILL_SCOPE: usize = 2; +const RENDER_KILL_OWNER: usize = 3; +const RENDER_KILL_HOLD: usize = 4; +/// A render-only candidate is left this long after close, as a quarantined compute queue is, +/// so that ordinary retirement goes first. +const RENDER_KILL_DELAY_NS: u64 = 250_000_000; + +/// The render queue a render-only kill names: one of the context's own render queues that +/// its scheduler page lists, never the reserved queues 0 and 1. +fn render_kill_target(listed: u128, own: u128) -> Option { + let candidates = listed & own & !0b11; + (candidates != 0).then(|| candidates.trailing_zeros() as u8) +} + +/// The one admission owner of every pair a kill holds, if there is one. +fn hold_owner(owners: impl Iterator>) -> Option { + let mut owner = None; + for slot_owner in owners { + match (owner, slot_owner) { + (_, None) | (_, Some(0)) => return None, + (None, Some(next)) => owner = Some(next), + (Some(current), Some(next)) if current != next => return None, + _ => {} + } + } + owner +} + +fn refuse_render_kill(teardown: &mut crate::g17::teardown::Pending, index: usize, reason: usize) { + if teardown.refuse_render_kill(index) { + RENDER_KILLS[reason].fetch_add(1, Ordering::Relaxed); + } +} + /// GPU memory is within an eighth of the device limit. A failed context holds its memory /// until its kill is acknowledged, so kills waiting for other contexts' compute to pause can /// push the device to its limit, and then every client's allocations fail: on Neo, @@ -63,6 +104,15 @@ pub(in crate::g17) fn write_kill_waits(f: &mut kernel::fmt::Formatter<'_>, waiti writeln!(f, "failed_kills waiting={} longest_wait_ms={}", waiting, longest / 1_000_000) } +pub(in crate::g17) fn write_render_kills(f: &mut kernel::fmt::Formatter<'_>, waiting: usize, longest: u64) + -> kernel::fmt::Result +{ + let count = |index: usize| RENDER_KILLS[index].load(Ordering::Relaxed); + writeln!(f, "render_kills published={} acknowledged={} refused_scope={} refused_owner={} refused_hold={} waiting={} longest_wait_ms={}", + count(RENDER_KILL_PUBLISHED), count(RENDER_KILL_ACKNOWLEDGED), count(RENDER_KILL_SCOPE), + count(RENDER_KILL_OWNER), count(RENDER_KILL_HOLD), waiting, longest / 1_000_000) +} + impl super::Registry { /// ACK storage belongs to the already-retained transaction, so a full event /// batch cannot overflow a separate cache. No allocation or wait is needed. @@ -74,6 +124,10 @@ impl super::Registry { } } } + if self.teardown.acknowledge_render_kill(cookie) { + RENDER_KILLS[RENDER_KILL_ACKNOWLEDGED].fetch_add(1, Ordering::Relaxed); + crate::g17::trace::teardown(4, cookie, 0x100, 0); + } } } @@ -101,6 +155,8 @@ impl Firmware { let _ = queue.announce_exit(now, || self.primary.notify(MSG_CONTROL_NOTIFY)); } } + let primary = &mut self.primary; + self.queues.teardown.announce_render_kills(|| primary.notify(MSG_CONTROL_NOTIFY)); // Under memory pressure one kill skips the wait, at most one per interval. let last = PRESSED_KILL_NS.load(Ordering::Relaxed); let mut pressed = @@ -109,14 +165,24 @@ impl Firmware { let Some((context, age)) = self.queues.teardown.closed(index, now) else { continue; }; - // A failed context's executed pairs keep their pool leases until the kill is - // acknowledged (`Pair::firmware_drained`), so its kill cannot wait for them. - if !self.queues.render.teardown_idle(&context)? - || (!self.compute_failed(&context) && !self.queues.render.teardown_pools_released(&context)) - { + if !self.queues.render.teardown_idle(&context)? { + self.queues.teardown.clear_kill_wait(index); continue; } - if let Err(error) = self.publish_compute_exit(&context, age, now, &mut pressed) { + // A failed context's executed pairs keep their pool leases until the kill is + // acknowledged (`Pair::firmware_drained`), so its kill cannot wait for them. + let result = if self.compute_failed(&context) || self.queues.render.teardown_pools_released(&context) { + // Only a render-only kill's wait lives on the entry; a compute kill keeps its + // own on the queue. + self.queues.teardown.clear_kill_wait(index); + self.publish_compute_exit(&context, age, now, &mut pressed) + } else if self.render_kill_candidate(index, &context, age) { + self.publish_render_kill(index, &context, now, &mut pressed) + } else { + self.queues.teardown.clear_kill_wait(index); + Ok(()) + }; + if let Err(error) = result { dev_err!( self.primary.state.shared.dev.as_ref(), "Scheduler {} exit request failed: {:?}\n", @@ -243,6 +309,102 @@ impl Firmware { Ok(()) } + /// A closed context whose compute side is clean, with render pairs whose commands all ran + /// but were never stamped (`Pair::executed_for_kill`). Its failed compute command was + /// retired on the firmware's fault record, so no compute queue is left to carry a kill, + /// yet those pairs are released only after one: without this the context, its pairs and + /// its memory stay until the processors stop. + fn render_kill_candidate(&self, index: usize, context: &Arc, age: u64) -> bool { + age >= RENDER_KILL_DELAY_NS + && !self.queues.teardown.render_kill_published(index) + && self.queues.render.executed_slots(context) != 0 + && self.queues.compute.iter().flatten().filter_map(|entry| entry.queue.as_deref()).all(|queue| { + queue.idle_for_context(context) && !queue.switching_from(context) && !queue.exit_published(context) + }) + } + + /// Kill a render-only candidate (`render_kill_candidate`). The rules of a failed + /// context's kill apply: the pairs are held for the kill and their memory is released only + /// once the scheduler page reports it, the kill waits for a moment without other contexts' + /// compute unless GPU memory is pressed, and it names only queues of this context. + fn publish_render_kill(&mut self, index: usize, context: &Arc, now: u64, pressed: &mut bool) -> Result { + if self.render_control_backpressured() { + return Ok(()); + } + let beside = self.foreign_compute_running(context, now); + if beside && !*pressed { + let (waited, crossed) = self.queues.teardown.note_kill_wait(index, now, SLOW_KILL_WAIT_NS); + let last = SLOW_KILL_WARNED_NS.load(Ordering::Relaxed); + if crossed + && (last == 0 || now.saturating_sub(last) >= SLOW_KILL_WAIT_NS) + && SLOW_KILL_WARNED_NS + .compare_exchange(last, now.max(1), Ordering::Relaxed, Ordering::Relaxed) + .is_ok() + { + dev_warn!( + self.primary.state.shared.dev.as_ref(), + "Scheduler {} kill has waited {} ms for other contexts' compute to pause\n", + context.id(), + waited / 1_000_000 + ); + } + return Ok(()); + } + let (mut own, mut foreign, slots) = self.queues.render.exit_scope(context); + for entry in self.queues.compute.iter().flatten() { + if let Some(queue) = entry.queue.as_deref() { + let bit = 1u128 << queue.qid(); + if queue.context().is_some_and(|bound| Arc::ptr_eq(bound, context)) { + own |= bit; + } else { + foreign |= bit; + } + } + } + let state = exit::State::read(context)?; + let Some(qids) = render_kill_target(state.qids, own).and_then(|target| state.scope(target, own, foreign)) else { + refuse_render_kill(&mut self.queues.teardown, index, RENDER_KILL_SCOPE); + return Ok(()); + }; + let held = |slot: u8| slots & (1u64 << slot) != 0 && self.queues.render.qids_for_slot(slot) & qids != 0; + let range = 0..crate::hw::t8140::queues::RENDER_SLOTS as u8; + let hold = range.clone().filter(|slot| held(*slot)).fold(0u64, |hold, slot| hold | 1u64 << slot); + let owner = hold_owner(range.filter(|slot| held(*slot)).map(|slot| self.queues.render.slot_owner(slot))); + let Some(owner) = owner.filter(|_| hold != 0) else { + refuse_render_kill(&mut self.queues.teardown, index, RENDER_KILL_OWNER); + return Ok(()); + }; + if !self + .queues + .admission + .hold_render_slots_for_exit_kill(owner, hold, self.queues.render.executed_slots(context) & hold) + { + refuse_render_kill(&mut self.queues.teardown, index, RENDER_KILL_HOLD); + return Ok(()); + } + let cookie = exit::next_cookie()?; + let record = ControlRecord::ContextKill(ContextKill::new(context.scheduler_va(), cookie)); + if let Err(error) = Rings::publish_control(&self.init, &record) { + self.queues.admission.release_exit_kill_hold(hold); + return if error == EAGAIN { Ok(()) } else { Err(error) }; + } + // Publication is irrevocable. The held pairs stay held until this context's + // scheduler release is consumed (`release_killed_slots` at detach). + self.queues.teardown.record_render_kill(index, cookie, now)?; + record_kill_wait(self.queues.teardown.take_kill_wait(index, now)); + if beside { + *pressed = false; + PRESSED_KILLS.fetch_add(1, Ordering::Relaxed); + PRESSED_KILL_NS.store(now.max(1), Ordering::Relaxed); + } + RENDER_KILLS[RENDER_KILL_PUBLISHED].fetch_add(1, Ordering::Relaxed); + crate::g17::trace::teardown(7, cookie, u64::from(context.id()), hold | u64::from(beside)); + fence(Ordering::SeqCst); + let primary = &mut self.primary; + self.queues.teardown.announce_render_kills(|| primary.notify(MSG_CONTROL_NOTIFY)); + Ok(()) + } + /// Another context's compute command is in flight. On Neo, kills of failed contexts beside /// other clients' compute occasionally made the firmware recover the GPU (twice in ten /// minutes of a five-lane load with 67 faults), even with nothing freed before the kill's @@ -304,6 +466,9 @@ impl Firmware { { return true; } + if self.queues.teardown.render_kill_polling(now, exit::POLL_NS) { + return true; + } (0..self.queues.teardown.len()).any(|index| { self.queues .teardown @@ -321,3 +486,27 @@ impl Firmware { }) } } + +#[kernel::macros::kunit_tests(asahi_g17_render_kill_target)] +mod render_kill_target_tests { + use super::{hold_owner, render_kill_target}; + + #[test] + fn a_render_kill_holds_pairs_of_one_owner_only() { + assert_eq!(hold_owner([Some(5), Some(5)].into_iter()), Some(5)); + assert_eq!(hold_owner([Some(5), Some(6)].into_iter()), None); + assert_eq!(hold_owner([Some(5), None].into_iter()), None); + assert_eq!(hold_owner([Some(0)].into_iter()), None); + assert_eq!(hold_owner(core::iter::empty()), None); + } + + #[test] + fn a_render_kill_names_an_own_listed_queue_never_0_or_1() { + // Listed by the scheduler page but not the context's own: no target. + assert_eq!(render_kill_target(1 << 40, 1 << 41), None); + // The reserved queues are never named. + assert_eq!(render_kill_target(0b11, 0b11), None); + assert_eq!(render_kill_target(0b111 | 1 << 40, 0b111 | 1 << 40), Some(2)); + assert_eq!(render_kill_target(1 << 40 | 1 << 90, 1 << 90), Some(90)); + } +} diff --git a/drivers/gpu/drm/asahi/g17/runtime/render/exit.rs b/drivers/gpu/drm/asahi/g17/runtime/render/exit.rs index 25e1335c5ba13..6c58f728b3eb5 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render/exit.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render/exit.rs @@ -39,6 +39,14 @@ impl RenderState { slots } + /// The admission owner whose identities the pair in `slot` was built with. + pub(in crate::g17::runtime) fn slot_owner(&self, slot: u8) -> Option { + self.entries + .get(usize::from(slot)) + .and_then(Option::as_ref) + .map(|entry| entry.reservation.owner) + } + pub(in crate::g17::runtime) fn qids_for_slot(&self, slot: u8) -> u128 { self.entries .get(usize::from(slot)) diff --git a/drivers/gpu/drm/asahi/g17/teardown.rs b/drivers/gpu/drm/asahi/g17/teardown.rs index 53518a76e91a4..fa2a72a77f060 100644 --- a/drivers/gpu/drm/asahi/g17/teardown.rs +++ b/drivers/gpu/drm/asahi/g17/teardown.rs @@ -24,6 +24,45 @@ struct Entry { phase: Phase, armed: u64, announce_pending: bool, + render_kill: Option, + /// Since when, and whether reported, a render-only kill has waited for other contexts' + /// compute to pause. + kill_wait: Option<(u64, bool)>, + /// A render-only kill of this context was refused (counted once per context). + kill_refused: bool, +} + +/// The kill of a closed context whose compute side is clean, published so that its render +/// pairs' executed but unstamped commands can be released (see `Pair::firmware_drained`). +/// The scheduler page's killed state, not this record, proves the kill to the pairs. +#[derive(Clone, Copy)] +struct RenderKill { + cookie: u64, + published: u64, + announced: bool, + acknowledged: bool, +} + +impl RenderKill { + fn new(cookie: u64, now: u64) -> Self { + Self { + cookie, + published: now, + announced: false, + acknowledged: false, + } + } + /// Only this kill's exact cookie acknowledges it, once. + fn acknowledge(&mut self, cookie: u64) -> bool { + if cookie == 0 || cookie != self.cookie || self.acknowledged { + return false; + } + self.acknowledged = true; + true + } + fn awaiting(&self, now: u64, window: u64) -> bool { + !self.acknowledged && now.saturating_sub(self.published) < window + } } /// Publication and announcement errors retain the entry; no timeout frees @@ -104,6 +143,9 @@ impl Pending { phase, armed: now, announce_pending: false, + render_kill: None, + kill_wait: None, + kill_refused: false, }; self.entries.push(entry, GFP_KERNEL)?; Ok(()) @@ -233,6 +275,94 @@ impl Pending { Ok(None) } + /// A render-only kill of the closed context at `index` was published. + pub(crate) fn render_kill_published(&self, index: usize) -> bool { + self.entries.get(index).is_some_and(|entry| entry.render_kill.is_some()) + } + + /// Record a published render-only kill. Publication is irrevocable: the record stays + /// with the entry until the context's scheduler release is consumed. + pub(crate) fn record_render_kill(&mut self, index: usize, cookie: u64, now: u64) -> Result { + let entry = self.entries.get_mut(index).ok_or(EINVAL)?; + if entry.render_kill.is_some() { + return Err(EINVAL); + } + entry.render_kill = Some(RenderKill::new(cookie, now)); + Ok(()) + } + + /// Ring the doorbell for published render-only kills not yet announced. A failed + /// doorbell is retried on a later pass and never duplicates the record. + pub(crate) fn announce_render_kills(&mut self, mut notify: impl FnMut() -> Result) { + for entry in self.entries.iter_mut() { + if let Some(kill) = entry.render_kill.as_mut() { + if !kill.announced && !kill.acknowledged && notify().is_ok() { + kill.announced = true; + } + } + } + } + + /// Only the exact cookie of an unacknowledged render-only kill is accepted. + pub(crate) fn acknowledge_render_kill(&mut self, cookie: u64) -> bool { + self.entries + .iter_mut() + .filter_map(|entry| entry.render_kill.as_mut()) + .any(|kill| kill.acknowledge(cookie)) + } + + /// A render-only kill awaits its acknowledgement within the polling window. + pub(crate) fn render_kill_polling(&self, now: u64, window: u64) -> bool { + self.entries + .iter() + .any(|entry| entry.render_kill.is_some_and(|kill| kill.awaiting(now, window))) + } + + /// The render-only kill of the entry at `index` waits; how long, and whether this is the + /// first time it crossed `slow`. + pub(crate) fn note_kill_wait(&mut self, index: usize, now: u64, slow: u64) -> (u64, bool) { + let Some(entry) = self.entries.get_mut(index) else { + return (0, false); + }; + let (since, warned) = entry.kill_wait.get_or_insert((now, false)); + let waited = now.saturating_sub(*since); + let crossed = waited >= slow && !*warned; + *warned |= crossed; + (waited, crossed) + } + + /// The render-only kill was published: how long it waited, if it did. + pub(crate) fn take_kill_wait(&mut self, index: usize, now: u64) -> u64 { + self.entries + .get_mut(index) + .and_then(|entry| entry.kill_wait.take()) + .map_or(0, |(since, _)| now.saturating_sub(since)) + } + + /// A render-only kill of the context at `index` was refused: it no longer waits, and this + /// returns whether it is the first refusal for this context. + pub(crate) fn refuse_render_kill(&mut self, index: usize) -> bool { + let Some(entry) = self.entries.get_mut(index) else { + return false; + }; + entry.kill_wait = None; + !core::mem::replace(&mut entry.kill_refused, true) + } + + /// The context at `index` is not waiting for a render-only kill (any more). + pub(crate) fn clear_kill_wait(&mut self, index: usize) { + if let Some(entry) = self.entries.get_mut(index) { + entry.kill_wait = None; + } + } + + /// How long each waiting render-only kill has waited. + pub(crate) fn kill_waits(&self, now: u64) -> impl Iterator + '_ { + self.entries + .iter() + .filter_map(move |entry| entry.kill_wait.map(|(since, _)| now.saturating_sub(since))) + } + /// Poll for a bounded window at close and again at actual release publication. /// Failed announcements remain retryable on later service even after polling /// expires. Time passage never substitutes for a firmware-consumption witness. @@ -242,3 +372,21 @@ impl Pending { }) } } + +#[kernel::macros::kunit_tests(asahi_g17_render_kill)] +mod render_kill_tests { + use super::RenderKill; + + #[test] + fn only_the_exact_cookie_acknowledges_a_render_kill_once() { + let mut kill = RenderKill::new(7, 1_000); + assert!(kill.awaiting(1_000, 10)); + assert!(!kill.acknowledge(0)); + assert!(!kill.acknowledge(8)); + assert!(kill.awaiting(1_009, 10)); + assert!(!kill.awaiting(1_010, 10)); + assert!(kill.acknowledge(7)); + assert!(!kill.acknowledge(7)); + assert!(!kill.awaiting(1_001, 10)); + } +} diff --git a/drivers/gpu/drm/asahi/g17/trace.rs b/drivers/gpu/drm/asahi/g17/trace.rs index f498de82e87c6..d5f61f50a93f6 100644 --- a/drivers/gpu/drm/asahi/g17/trace.rs +++ b/drivers/gpu/drm/asahi/g17/trace.rs @@ -77,8 +77,9 @@ pub(super) fn message(kind: u8, value: u64) { /// Failed-context teardown: 1 compute fault (qid, stamp), 2 unstamped render retire (slot, /// ordinal), 3 kill published (cookie, context, held slots | 1 if it skipped the wait for other -/// contexts' compute under memory pressure; slot 0 is never held), 4 kill acknowledged (cookie, qid), -/// 5 detach (context, slots, linked), 6 render slot to a new owner (slot, old, new | killed << 63). +/// contexts' compute under memory pressure; slot 0 is never held), 4 kill acknowledged (cookie, +/// qid, or 0x100 for a render-only kill), 5 detach (context, slots, linked), 6 render slot to a +/// new owner (slot, old, new | killed << 63), 7 render-only kill published (as 3). pub(crate) fn teardown(kind: u8, a: u64, b: u64, c: u64) { // SAFETY: Plain values only. unsafe { asahi_g17_trace_teardown(kind, a, b, c) }; From 73fb974b8478c8b3b245256afa751e80b28f8db1 Mon Sep 17 00:00:00 2001 From: Ryan <81541646+RyanTheTide@users.noreply.github.com> Date: Tue, 27 Oct 2026 17:13:08 +1100 Subject: [PATCH 235/388] arm64: dts: apple: t6040-j616s: Describe every SMC sensor and add die thermal zones * arm64: dts: apple: t6040-j616s: Describe every SMC sensor and add die zones The J616s DT listed one SMC temperature (TCMz) plus the shared hwmon includes: five temperatures, two fans and a handful of rails, while its SMC has 3462 keys. A read-only walk of the key table on this machine found 466 more readable float sensors that update: 278 temperatures (P and E cluster, GPU, SoC, VR, NAND, battery, ports), 49 voltages, 67 currents and 72 powers, including the three battery cells and the battery current and power. Describe all of them, in the style of the J700, so the machine reports at least what the fanless J700 does. Keys that only ever read a fixed placeholder (Tz1*, Ta0*) or a fixed limit (TVMx, TVmS, TVms, PHPB, PHPM) stay out; rails that read 0 W at idle stay in. The GPU sensors (Tg*) report placeholder values from -4 to 4 C while the GPU is powered off and read live while it runs. Add a zone per die-maximum sensor: Tex0-3 for the E cluster, Tpx0-5 and Tpx8-D for the two P clusters, with the TCMz backstop trips (113 C passive, 118 C critical), and the seven hottest GPU sensors, which shed package power through the CPU clusters from 105 C, above the 103.8 C peak seen during conformance runs. The PMP and the SMC fans regulate in normal use; these zones catch a failure of that loop. The fans already list their minimum, maximum, target and mode keys, so the existing fan_control module parameter makes fanN_target writable; no new parameter is needed. Assisted-by: LLM Signed-off-by: Ryan Murray * arm64: dts: apple: t6040-j616s: Fix the SMC sensor comment "Tp*/Te*" in the sensor comment closed the comment early and broke the build of the J616s DT. Spell the groups out. Fixes: 8b5a2d73a21a ("arm64: dts: apple: t6040-j616s: Describe every SMC sensor and add die zones") Assisted-by: LLM Signed-off-by: Ryan Murray --------- Signed-off-by: Ryan Murray --- arch/arm64/boot/dts/apple/t6040-j616s.dts | 3492 ++++++++++++++++++++- 1 file changed, 3359 insertions(+), 133 deletions(-) diff --git a/arch/arm64/boot/dts/apple/t6040-j616s.dts b/arch/arm64/boot/dts/apple/t6040-j616s.dts index f5ba130e5c183..c47e71ed1e258 100644 --- a/arch/arm64/boot/dts/apple/t6040-j616s.dts +++ b/arch/arm64/boot/dts/apple/t6040-j616s.dts @@ -301,6 +301,21 @@ * 113 C, critical (orderly shutdown) at 118 C, polled once a second as * the SMC refreshes TCMz. The cpufreq cooling device sits on a * cluster's policy CPU, so each map lists every CPU of its cluster. + * + * Per-cluster and GPU zones follow, one per SMC die-maximum sensor + * (smc_hwmon channels 1-23, in the order of the temperature nodes + * below): Tex0-3 throttle the E cluster, Tpx0-5 the first P cluster + * and Tpx8-D the second, with the same 113/118 C backstop as TCMz. + * Unlike the fanless J700, this machine has two SMC-driven fans and + * the PMP regulates the dies itself, so these zones only catch a + * failure of that loop; they never act in normal use. + * + * The GPU has no cooling device of its own: its firmware sets the GPU + * performance state. The seven hottest GPU die sensors (Tg05, Tg0S, + * Tg0Y, Tg0k, Tg0z, Tg1V, Tg1l) shed package power through the CPU + * clusters from 105 C, above the 103.8 C peak measured during + * conformance runs, and shut down at 118 C. While the GPU is powered + * off its sensors report placeholder values from -4 to 4 C. */ thermal-zones { cpu-die-thermal { @@ -353,183 +368,3394 @@ }; }; }; + + cpu-e0-thermal { + polling-delay-passive = <1000>; + polling-delay = <1000>; + thermal-sensors = <&smc_hwmon 1>; /* Tex0 */ + + trips { + cpu_e0_passive: trip-passive { + temperature = <113000>; + hysteresis = <5000>; + type = "passive"; + }; + + trip-critical { + temperature = <118000>; + hysteresis = <0>; + type = "critical"; + }; + }; + + cooling-maps { + map-e { + trip = <&cpu_e0_passive>; + cooling-device = + <&cpu_e0 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_e1 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_e2 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_e3 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + }; + }; + + cpu-e1-thermal { + polling-delay-passive = <1000>; + polling-delay = <1000>; + thermal-sensors = <&smc_hwmon 2>; /* Tex1 */ + + trips { + cpu_e1_passive: trip-passive { + temperature = <113000>; + hysteresis = <5000>; + type = "passive"; + }; + + trip-critical { + temperature = <118000>; + hysteresis = <0>; + type = "critical"; + }; + }; + + cooling-maps { + map-e { + trip = <&cpu_e1_passive>; + cooling-device = + <&cpu_e0 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_e1 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_e2 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_e3 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + }; + }; + + cpu-e2-thermal { + polling-delay-passive = <1000>; + polling-delay = <1000>; + thermal-sensors = <&smc_hwmon 3>; /* Tex2 */ + + trips { + cpu_e2_passive: trip-passive { + temperature = <113000>; + hysteresis = <5000>; + type = "passive"; + }; + + trip-critical { + temperature = <118000>; + hysteresis = <0>; + type = "critical"; + }; + }; + + cooling-maps { + map-e { + trip = <&cpu_e2_passive>; + cooling-device = + <&cpu_e0 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_e1 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_e2 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_e3 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + }; + }; + + cpu-e3-thermal { + polling-delay-passive = <1000>; + polling-delay = <1000>; + thermal-sensors = <&smc_hwmon 4>; /* Tex3 */ + + trips { + cpu_e3_passive: trip-passive { + temperature = <113000>; + hysteresis = <5000>; + type = "passive"; + }; + + trip-critical { + temperature = <118000>; + hysteresis = <0>; + type = "critical"; + }; + }; + + cooling-maps { + map-e { + trip = <&cpu_e3_passive>; + cooling-device = + <&cpu_e0 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_e1 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_e2 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_e3 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + }; + }; + + cpu-p00-thermal { + polling-delay-passive = <1000>; + polling-delay = <1000>; + thermal-sensors = <&smc_hwmon 5>; /* Tpx0 */ + + trips { + cpu_p00_passive: trip-passive { + temperature = <113000>; + hysteresis = <5000>; + type = "passive"; + }; + + trip-critical { + temperature = <118000>; + hysteresis = <0>; + type = "critical"; + }; + }; + + cooling-maps { + map-p0 { + trip = <&cpu_p00_passive>; + cooling-device = + <&cpu_p00 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p01 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p02 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p03 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p04 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + }; + }; + + cpu-p01-thermal { + polling-delay-passive = <1000>; + polling-delay = <1000>; + thermal-sensors = <&smc_hwmon 6>; /* Tpx1 */ + + trips { + cpu_p01_passive: trip-passive { + temperature = <113000>; + hysteresis = <5000>; + type = "passive"; + }; + + trip-critical { + temperature = <118000>; + hysteresis = <0>; + type = "critical"; + }; + }; + + cooling-maps { + map-p0 { + trip = <&cpu_p01_passive>; + cooling-device = + <&cpu_p00 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p01 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p02 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p03 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p04 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + }; + }; + + cpu-p02-thermal { + polling-delay-passive = <1000>; + polling-delay = <1000>; + thermal-sensors = <&smc_hwmon 7>; /* Tpx2 */ + + trips { + cpu_p02_passive: trip-passive { + temperature = <113000>; + hysteresis = <5000>; + type = "passive"; + }; + + trip-critical { + temperature = <118000>; + hysteresis = <0>; + type = "critical"; + }; + }; + + cooling-maps { + map-p0 { + trip = <&cpu_p02_passive>; + cooling-device = + <&cpu_p00 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p01 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p02 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p03 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p04 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + }; + }; + + cpu-p03-thermal { + polling-delay-passive = <1000>; + polling-delay = <1000>; + thermal-sensors = <&smc_hwmon 8>; /* Tpx3 */ + + trips { + cpu_p03_passive: trip-passive { + temperature = <113000>; + hysteresis = <5000>; + type = "passive"; + }; + + trip-critical { + temperature = <118000>; + hysteresis = <0>; + type = "critical"; + }; + }; + + cooling-maps { + map-p0 { + trip = <&cpu_p03_passive>; + cooling-device = + <&cpu_p00 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p01 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p02 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p03 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p04 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + }; + }; + + cpu-p04-thermal { + polling-delay-passive = <1000>; + polling-delay = <1000>; + thermal-sensors = <&smc_hwmon 9>; /* Tpx4 */ + + trips { + cpu_p04_passive: trip-passive { + temperature = <113000>; + hysteresis = <5000>; + type = "passive"; + }; + + trip-critical { + temperature = <118000>; + hysteresis = <0>; + type = "critical"; + }; + }; + + cooling-maps { + map-p0 { + trip = <&cpu_p04_passive>; + cooling-device = + <&cpu_p00 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p01 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p02 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p03 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p04 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + }; + }; + + cpu-p05-thermal { + polling-delay-passive = <1000>; + polling-delay = <1000>; + thermal-sensors = <&smc_hwmon 10>; /* Tpx5 */ + + trips { + cpu_p05_passive: trip-passive { + temperature = <113000>; + hysteresis = <5000>; + type = "passive"; + }; + + trip-critical { + temperature = <118000>; + hysteresis = <0>; + type = "critical"; + }; + }; + + cooling-maps { + map-p0 { + trip = <&cpu_p05_passive>; + cooling-device = + <&cpu_p00 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p01 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p02 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p03 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p04 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + }; + }; + + cpu-p10-thermal { + polling-delay-passive = <1000>; + polling-delay = <1000>; + thermal-sensors = <&smc_hwmon 11>; /* Tpx8 */ + + trips { + cpu_p10_passive: trip-passive { + temperature = <113000>; + hysteresis = <5000>; + type = "passive"; + }; + + trip-critical { + temperature = <118000>; + hysteresis = <0>; + type = "critical"; + }; + }; + + cooling-maps { + map-p1 { + trip = <&cpu_p10_passive>; + cooling-device = + <&cpu_p10 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p11 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p12 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p13 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p14 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + }; + }; + + cpu-p11-thermal { + polling-delay-passive = <1000>; + polling-delay = <1000>; + thermal-sensors = <&smc_hwmon 12>; /* Tpx9 */ + + trips { + cpu_p11_passive: trip-passive { + temperature = <113000>; + hysteresis = <5000>; + type = "passive"; + }; + + trip-critical { + temperature = <118000>; + hysteresis = <0>; + type = "critical"; + }; + }; + + cooling-maps { + map-p1 { + trip = <&cpu_p11_passive>; + cooling-device = + <&cpu_p10 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p11 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p12 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p13 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p14 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + }; + }; + + cpu-p12-thermal { + polling-delay-passive = <1000>; + polling-delay = <1000>; + thermal-sensors = <&smc_hwmon 13>; /* TpxA */ + + trips { + cpu_p12_passive: trip-passive { + temperature = <113000>; + hysteresis = <5000>; + type = "passive"; + }; + + trip-critical { + temperature = <118000>; + hysteresis = <0>; + type = "critical"; + }; + }; + + cooling-maps { + map-p1 { + trip = <&cpu_p12_passive>; + cooling-device = + <&cpu_p10 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p11 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p12 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p13 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p14 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + }; + }; + + cpu-p13-thermal { + polling-delay-passive = <1000>; + polling-delay = <1000>; + thermal-sensors = <&smc_hwmon 14>; /* TpxB */ + + trips { + cpu_p13_passive: trip-passive { + temperature = <113000>; + hysteresis = <5000>; + type = "passive"; + }; + + trip-critical { + temperature = <118000>; + hysteresis = <0>; + type = "critical"; + }; + }; + + cooling-maps { + map-p1 { + trip = <&cpu_p13_passive>; + cooling-device = + <&cpu_p10 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p11 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p12 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p13 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p14 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + }; + }; + + cpu-p14-thermal { + polling-delay-passive = <1000>; + polling-delay = <1000>; + thermal-sensors = <&smc_hwmon 15>; /* TpxC */ + + trips { + cpu_p14_passive: trip-passive { + temperature = <113000>; + hysteresis = <5000>; + type = "passive"; + }; + + trip-critical { + temperature = <118000>; + hysteresis = <0>; + type = "critical"; + }; + }; + + cooling-maps { + map-p1 { + trip = <&cpu_p14_passive>; + cooling-device = + <&cpu_p10 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p11 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p12 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p13 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p14 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + }; + }; + + cpu-p15-thermal { + polling-delay-passive = <1000>; + polling-delay = <1000>; + thermal-sensors = <&smc_hwmon 16>; /* TpxD */ + + trips { + cpu_p15_passive: trip-passive { + temperature = <113000>; + hysteresis = <5000>; + type = "passive"; + }; + + trip-critical { + temperature = <118000>; + hysteresis = <0>; + type = "critical"; + }; + }; + + cooling-maps { + map-p1 { + trip = <&cpu_p15_passive>; + cooling-device = + <&cpu_p10 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p11 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p12 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p13 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p14 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + }; + }; + + gpu0-thermal { + polling-delay-passive = <1000>; + polling-delay = <1000>; + thermal-sensors = <&smc_hwmon 17>; /* Tg05 */ + + trips { + gpu0_passive: trip-passive { + temperature = <105000>; + hysteresis = <5000>; + type = "passive"; + }; + + trip-critical { + temperature = <118000>; + hysteresis = <0>; + type = "critical"; + }; + }; + + cooling-maps { + map-e { + trip = <&gpu0_passive>; + cooling-device = + <&cpu_e0 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_e1 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_e2 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_e3 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + + map-p0 { + trip = <&gpu0_passive>; + cooling-device = + <&cpu_p00 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p01 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p02 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p03 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p04 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + + map-p1 { + trip = <&gpu0_passive>; + cooling-device = + <&cpu_p10 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p11 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p12 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p13 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p14 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + }; + }; + + gpu1-thermal { + polling-delay-passive = <1000>; + polling-delay = <1000>; + thermal-sensors = <&smc_hwmon 18>; /* Tg0S */ + + trips { + gpu1_passive: trip-passive { + temperature = <105000>; + hysteresis = <5000>; + type = "passive"; + }; + + trip-critical { + temperature = <118000>; + hysteresis = <0>; + type = "critical"; + }; + }; + + cooling-maps { + map-e { + trip = <&gpu1_passive>; + cooling-device = + <&cpu_e0 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_e1 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_e2 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_e3 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + + map-p0 { + trip = <&gpu1_passive>; + cooling-device = + <&cpu_p00 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p01 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p02 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p03 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p04 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + + map-p1 { + trip = <&gpu1_passive>; + cooling-device = + <&cpu_p10 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p11 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p12 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p13 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p14 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + }; + }; + + gpu2-thermal { + polling-delay-passive = <1000>; + polling-delay = <1000>; + thermal-sensors = <&smc_hwmon 19>; /* Tg0Y */ + + trips { + gpu2_passive: trip-passive { + temperature = <105000>; + hysteresis = <5000>; + type = "passive"; + }; + + trip-critical { + temperature = <118000>; + hysteresis = <0>; + type = "critical"; + }; + }; + + cooling-maps { + map-e { + trip = <&gpu2_passive>; + cooling-device = + <&cpu_e0 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_e1 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_e2 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_e3 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + + map-p0 { + trip = <&gpu2_passive>; + cooling-device = + <&cpu_p00 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p01 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p02 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p03 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p04 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + + map-p1 { + trip = <&gpu2_passive>; + cooling-device = + <&cpu_p10 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p11 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p12 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p13 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p14 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + }; + }; + + gpu3-thermal { + polling-delay-passive = <1000>; + polling-delay = <1000>; + thermal-sensors = <&smc_hwmon 20>; /* Tg0k */ + + trips { + gpu3_passive: trip-passive { + temperature = <105000>; + hysteresis = <5000>; + type = "passive"; + }; + + trip-critical { + temperature = <118000>; + hysteresis = <0>; + type = "critical"; + }; + }; + + cooling-maps { + map-e { + trip = <&gpu3_passive>; + cooling-device = + <&cpu_e0 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_e1 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_e2 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_e3 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + + map-p0 { + trip = <&gpu3_passive>; + cooling-device = + <&cpu_p00 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p01 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p02 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p03 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p04 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + + map-p1 { + trip = <&gpu3_passive>; + cooling-device = + <&cpu_p10 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p11 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p12 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p13 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p14 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + }; + }; + + gpu4-thermal { + polling-delay-passive = <1000>; + polling-delay = <1000>; + thermal-sensors = <&smc_hwmon 21>; /* Tg0z */ + + trips { + gpu4_passive: trip-passive { + temperature = <105000>; + hysteresis = <5000>; + type = "passive"; + }; + + trip-critical { + temperature = <118000>; + hysteresis = <0>; + type = "critical"; + }; + }; + + cooling-maps { + map-e { + trip = <&gpu4_passive>; + cooling-device = + <&cpu_e0 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_e1 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_e2 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_e3 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + + map-p0 { + trip = <&gpu4_passive>; + cooling-device = + <&cpu_p00 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p01 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p02 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p03 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p04 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + + map-p1 { + trip = <&gpu4_passive>; + cooling-device = + <&cpu_p10 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p11 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p12 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p13 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p14 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + }; + }; + + gpu5-thermal { + polling-delay-passive = <1000>; + polling-delay = <1000>; + thermal-sensors = <&smc_hwmon 22>; /* Tg1V */ + + trips { + gpu5_passive: trip-passive { + temperature = <105000>; + hysteresis = <5000>; + type = "passive"; + }; + + trip-critical { + temperature = <118000>; + hysteresis = <0>; + type = "critical"; + }; + }; + + cooling-maps { + map-e { + trip = <&gpu5_passive>; + cooling-device = + <&cpu_e0 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_e1 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_e2 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_e3 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + + map-p0 { + trip = <&gpu5_passive>; + cooling-device = + <&cpu_p00 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p01 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p02 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p03 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p04 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + + map-p1 { + trip = <&gpu5_passive>; + cooling-device = + <&cpu_p10 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p11 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p12 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p13 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p14 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + }; + }; + + gpu6-thermal { + polling-delay-passive = <1000>; + polling-delay = <1000>; + thermal-sensors = <&smc_hwmon 23>; /* Tg1l */ + + trips { + gpu6_passive: trip-passive { + temperature = <105000>; + hysteresis = <5000>; + type = "passive"; + }; + + trip-critical { + temperature = <118000>; + hysteresis = <0>; + type = "critical"; + }; + }; + + cooling-maps { + map-e { + trip = <&gpu6_passive>; + cooling-device = + <&cpu_e0 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_e1 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_e2 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_e3 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + + map-p0 { + trip = <&gpu6_passive>; + cooling-device = + <&cpu_p00 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p01 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p02 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p03 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p04 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + + map-p1 { + trip = <&gpu6_passive>; + cooling-device = + <&cpu_p10 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p11 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p12 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p13 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, + <&cpu_p14 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; + }; + }; + }; + }; + + /* + * The speaker amps' shared shutdown line (SDZ, AP pin 54, low = + * shutdown). One fixed regulator, the SDZ supply of all six, not + * per-amp shutdown-gpios: with GPIO_SHARED six references become a + * vote, and one amp driving the line low would not shut the other + * five down. + */ + speaker_sdz: regulator-sn012776-sdz { + compatible = "regulator-fixed"; + regulator-name = "sn012776-sdz"; + gpios = <&pinctrl 54 GPIO_ACTIVE_HIGH>; + enable-active-high; + }; + + /* + * The AOP firmware runs the stream (MCA cluster 0, one serializer for + * all six amps); the J616 variant of macaudio links the AOP audio + * component's speaker back-end (DAI 3) to the amps. The codecs are + * listed in the ADT provider order, so each gets its native playback + * slot (LW1 0, RW1 1, LW2 2, RW2 3, LT 4, RT 5). The idle masks are the + * ADT amp-tx-zd-config rows 0 and 1: LW1 drives zeros in the right + * amps' sense slots, RW1 in the left amps'. + */ + sound: sound { + compatible = "apple,j616-macaudio", "apple,macaudio"; + model = "MacBook Pro J616"; + #address-cells = <1>; + #size-cells = <0>; + + dai-link@0 { + reg = <0>; + link-name = "Speakers"; + dai-tdm-idle-mode-0 = "zero"; + dai-tdm-idle-mode-1 = "zero"; + dai-tdm-slot-tx-idle-mask-0 = <0xf0f0f0>; + dai-tdm-slot-tx-idle-mask-1 = <0x0f0f0f>; + + cpu { + sound-dai = <&aop_audio 3>; + }; + + codec { + sound-dai = <&speaker_left_woof1>, <&speaker_right_woof1>, + <&speaker_left_woof2>, <&speaker_right_woof2>, + <&speaker_left_tweet>, <&speaker_right_tweet>; + }; + }; + }; + + /* + * The headphone jack: the AOP audio component's jack back-end (DAI 4) + * to the CS42L84. A card of its own, so a codec that does not answer + * costs the jack only, never the speakers. + */ + sound_jack: sound-jack { + compatible = "apple,j616-macaudio-jack", "apple,macaudio"; + model = "MacBook Pro J616 Jack"; + #address-cells = <1>; + #size-cells = <0>; + + dai-link@0 { + reg = <0>; + link-name = "Headphone Jack"; + + cpu { + sound-dai = <&aop_audio 4>; + }; + + codec { + sound-dai = <&jack_codec>; + }; + }; + }; +}; + +&cpuidle { + status = "okay"; +}; + +&smc { + status = "okay"; +}; + +&smc_mbox { + status = "okay"; +}; + +&nub_spmi0 { + status = "okay"; +}; + +&pinctrl { + status = "okay"; + + wifi_clkreq_pins: wifi-clkreq-pins { + pinmux = ; + }; +}; + +/* + * The AP takes none of the always-on bank's interrupts: the AOP firmware + * owns some of its pins and interrupt registers, and a chained handler here + * would see their pending bits without a handler of ours. + */ +&pinctrl_nub { + status = "okay"; + /delete-property/ interrupt-controller; + /delete-property/ #interrupt-cells; + /delete-property/ interrupts; +}; + +&fpwm0 { + status = "okay"; +}; + +/* The Type-C retimers (ADT atcrt at 0x18/0x19/0x1a) sit on this bus; no driver yet. */ +&i2c6 { + status = "okay"; +}; + +&mtp { + status = "okay"; +}; + +&mtp_mbox { + status = "okay"; +}; + +&mtp_dart { + status = "okay"; +}; + +&mtp_dockchannel { + status = "okay"; +}; + +&mtp_hid { + /* SMC key gp1c powers the trackpad analog front end. */ + apple,afe-smc = <&smc>; + + multi-touch { + firmware-name = "apple/tpmtfw-j616s.bin"; + }; + + keyboard: keyboard { + hid-country-code = <0>; + apple,keyboard-layout-id = <2>; + }; + + stm { + }; + + actuator { + }; + + tp-accel { + }; + + mtp { + }; +}; + +&smc_hwmon { + /* + * TCMz and the 23 zone sensors after it must stay the first + * temperature children: the thermal zones name a sensor by its index, + * so the shared hwmon includes below only append after them. + * + * Every key below is in this machine's SMC key table as a readable + * float and was read on the J616s (SMC firmware of macOS 26.6.2). + * Keys that only ever read a fixed placeholder (Tz1*, Ta0*) or a + * fixed limit (TVMx, TVmS, TVms, PHPB, PHPM) are left out; rails that + * read 0 W while idle are kept. GPU (Tg*), P-cluster (Tp*) and + * E-cluster (Te*) labels follow load tests: Tg* only read live while + * the GPU is powered and rise with GPU load, Tp* and Te* rise with CPU + * load. Other names follow the SMC key groups and are not otherwise + * verified. SBA1-3 are the three cells of the 3S battery. + */ + temperature-TCMz { + apple,key-id = "TCMz"; + label = "CPU Die Max"; + }; + + temperature-Tex0 { + apple,key-id = "Tex0"; + label = "E-cluster Tex0"; + }; + + temperature-Tex1 { + apple,key-id = "Tex1"; + label = "E-cluster Tex1"; + }; + + temperature-Tex2 { + apple,key-id = "Tex2"; + label = "E-cluster Tex2"; + }; + + temperature-Tex3 { + apple,key-id = "Tex3"; + label = "E-cluster Tex3"; + }; + + temperature-Tpx0 { + apple,key-id = "Tpx0"; + label = "P-cluster Tpx0"; + }; + + temperature-Tpx1 { + apple,key-id = "Tpx1"; + label = "P-cluster Tpx1"; + }; + + temperature-Tpx2 { + apple,key-id = "Tpx2"; + label = "P-cluster Tpx2"; + }; + + temperature-Tpx3 { + apple,key-id = "Tpx3"; + label = "P-cluster Tpx3"; + }; + + temperature-Tpx4 { + apple,key-id = "Tpx4"; + label = "P-cluster Tpx4"; + }; + + temperature-Tpx5 { + apple,key-id = "Tpx5"; + label = "P-cluster Tpx5"; + }; + + temperature-Tpx8 { + apple,key-id = "Tpx8"; + label = "P-cluster Tpx8"; + }; + + temperature-Tpx9 { + apple,key-id = "Tpx9"; + label = "P-cluster Tpx9"; + }; + + temperature-TpxA { + apple,key-id = "TpxA"; + label = "P-cluster TpxA"; + }; + + temperature-TpxB { + apple,key-id = "TpxB"; + label = "P-cluster TpxB"; + }; + + temperature-TpxC { + apple,key-id = "TpxC"; + label = "P-cluster TpxC"; + }; + + temperature-TpxD { + apple,key-id = "TpxD"; + label = "P-cluster TpxD"; + }; + + temperature-Tg05 { + apple,key-id = "Tg05"; + label = "GPU Tg05"; + }; + + temperature-Tg0S { + apple,key-id = "Tg0S"; + label = "GPU Tg0S"; + }; + + temperature-Tg0Y { + apple,key-id = "Tg0Y"; + label = "GPU Tg0Y"; + }; + + temperature-Tg0k { + apple,key-id = "Tg0k"; + label = "GPU Tg0k"; + }; + + temperature-Tg0z { + apple,key-id = "Tg0z"; + label = "GPU Tg0z"; + }; + + temperature-Tg1V { + apple,key-id = "Tg1V"; + label = "GPU Tg1V"; + }; + + temperature-Tg1l { + apple,key-id = "Tg1l"; + label = "GPU Tg1l"; + }; + + temperature-TAOL { + apple,key-id = "TAOL"; + label = "SMC TAOL"; + }; + + temperature-TB1T { + apple,key-id = "TB1T"; + label = "Battery 1"; + }; + + temperature-TB2T { + apple,key-id = "TB2T"; + label = "Battery 2"; + }; + + temperature-TCMb { + apple,key-id = "TCMb"; + label = "SMC TCMb"; + }; + + temperature-TD00 { + apple,key-id = "TD00"; + label = "SMC TD00"; + }; + + temperature-TD01 { + apple,key-id = "TD01"; + label = "SMC TD01"; + }; + + temperature-TD02 { + apple,key-id = "TD02"; + label = "SMC TD02"; + }; + + temperature-TD03 { + apple,key-id = "TD03"; + label = "SMC TD03"; + }; + + temperature-TD04 { + apple,key-id = "TD04"; + label = "SMC TD04"; + }; + + temperature-TD10 { + apple,key-id = "TD10"; + label = "SMC TD10"; + }; + + temperature-TD11 { + apple,key-id = "TD11"; + label = "SMC TD11"; + }; + + temperature-TD12 { + apple,key-id = "TD12"; + label = "SMC TD12"; + }; + + temperature-TD13 { + apple,key-id = "TD13"; + label = "SMC TD13"; + }; + + temperature-TD14 { + apple,key-id = "TD14"; + label = "SMC TD14"; + }; + + temperature-TD20 { + apple,key-id = "TD20"; + label = "SMC TD20"; + }; + + temperature-TD21 { + apple,key-id = "TD21"; + label = "SMC TD21"; + }; + + temperature-TD22 { + apple,key-id = "TD22"; + label = "SMC TD22"; + }; + + temperature-TD23 { + apple,key-id = "TD23"; + label = "SMC TD23"; + }; + + temperature-TD24 { + apple,key-id = "TD24"; + label = "SMC TD24"; + }; + + temperature-TDBP { + apple,key-id = "TDBP"; + label = "SMC TDBP"; + }; + + temperature-TDCR { + apple,key-id = "TDCR"; + label = "SMC TDCR"; + }; + + temperature-TDEC { + apple,key-id = "TDEC"; + label = "SMC TDEC"; + }; + + temperature-TDEL { + apple,key-id = "TDEL"; + label = "SMC TDEL"; + }; + + temperature-TDER { + apple,key-id = "TDER"; + label = "SMC TDER"; + }; + + temperature-TDTP { + apple,key-id = "TDTP"; + label = "SMC TDTP"; + }; + + temperature-TDVx { + apple,key-id = "TDVx"; + label = "SMC TDVx"; + }; + + temperature-TDeL { + apple,key-id = "TDeL"; + label = "SMC TDeL"; + }; + + temperature-TDeR { + apple,key-id = "TDeR"; + label = "SMC TDeR"; + }; + + temperature-TF2S { + apple,key-id = "TF2S"; + label = "SMC TF2S"; + }; + + temperature-TH0a { + apple,key-id = "TH0a"; + label = "SMC TH0a"; + }; + + temperature-TH0b { + apple,key-id = "TH0b"; + label = "SMC TH0b"; + }; + + temperature-TMVR { + apple,key-id = "TMVR"; + label = "SMC TMVR"; + }; + + temperature-TPD0 { + apple,key-id = "TPD0"; + label = "SMC TPD0"; + }; + + temperature-TPD1 { + apple,key-id = "TPD1"; + label = "SMC TPD1"; + }; + + temperature-TPD2 { + apple,key-id = "TPD2"; + label = "SMC TPD2"; + }; + + temperature-TPD3 { + apple,key-id = "TPD3"; + label = "SMC TPD3"; + }; + + temperature-TPD4 { + apple,key-id = "TPD4"; + label = "SMC TPD4"; + }; + + temperature-TPD5 { + apple,key-id = "TPD5"; + label = "SMC TPD5"; + }; + + temperature-TPD6 { + apple,key-id = "TPD6"; + label = "SMC TPD6"; + }; + + temperature-TPD7 { + apple,key-id = "TPD7"; + label = "SMC TPD7"; + }; + + temperature-TPD8 { + apple,key-id = "TPD8"; + label = "SMC TPD8"; + }; + + temperature-TPD9 { + apple,key-id = "TPD9"; + label = "SMC TPD9"; + }; + + temperature-TPDX { + apple,key-id = "TPDX"; + label = "SMC TPDX"; + }; + + temperature-TPDa { + apple,key-id = "TPDa"; + label = "SMC TPDa"; + }; + + temperature-TPDb { + apple,key-id = "TPDb"; + label = "SMC TPDb"; + }; + + temperature-TPDc { + apple,key-id = "TPDc"; + label = "SMC TPDc"; + }; + + temperature-TPDd { + apple,key-id = "TPDd"; + label = "SMC TPDd"; + }; + + temperature-TPDe { + apple,key-id = "TPDe"; + label = "SMC TPDe"; + }; + + temperature-TPDf { + apple,key-id = "TPDf"; + label = "SMC TPDf"; + }; + + temperature-TPH3 { + apple,key-id = "TPH3"; + label = "SMC TPH3"; + }; + + temperature-TPMP { + apple,key-id = "TPMP"; + label = "SMC TPMP"; + }; + + temperature-TPSP { + apple,key-id = "TPSP"; + label = "SMC TPSP"; + }; + + temperature-TRD0 { + apple,key-id = "TRD0"; + label = "SMC TRD0"; + }; + + temperature-TRD1 { + apple,key-id = "TRD1"; + label = "SMC TRD1"; + }; + + temperature-TRD2 { + apple,key-id = "TRD2"; + label = "SMC TRD2"; + }; + + temperature-TRD3 { + apple,key-id = "TRD3"; + label = "SMC TRD3"; + }; + + temperature-TRD4 { + apple,key-id = "TRD4"; + label = "SMC TRD4"; + }; + + temperature-TRD5 { + apple,key-id = "TRD5"; + label = "SMC TRD5"; + }; + + temperature-TRD6 { + apple,key-id = "TRD6"; + label = "SMC TRD6"; + }; + + temperature-TRD7 { + apple,key-id = "TRD7"; + label = "SMC TRD7"; + }; + + temperature-TRD8 { + apple,key-id = "TRD8"; + label = "SMC TRD8"; + }; + + temperature-TRD9 { + apple,key-id = "TRD9"; + label = "SMC TRD9"; + }; + + temperature-TRDX { + apple,key-id = "TRDX"; + label = "SMC TRDX"; + }; + + temperature-TRDa { + apple,key-id = "TRDa"; + label = "SMC TRDa"; + }; + + temperature-TRDb { + apple,key-id = "TRDb"; + label = "SMC TRDb"; + }; + + temperature-TRDc { + apple,key-id = "TRDc"; + label = "SMC TRDc"; + }; + + temperature-TRDd { + apple,key-id = "TRDd"; + label = "SMC TRDd"; + }; + + temperature-TRDe { + apple,key-id = "TRDe"; + label = "SMC TRDe"; + }; + + temperature-TRDf { + apple,key-id = "TRDf"; + label = "SMC TRDf"; + }; + + temperature-TS0P { + apple,key-id = "TS0P"; + label = "SMC TS0P"; + }; + + temperature-TSG1 { + apple,key-id = "TSG1"; + label = "SMC TSG1"; + }; + + temperature-TUD0 { + apple,key-id = "TUD0"; + label = "SMC TUD0"; + }; + + temperature-TUD1 { + apple,key-id = "TUD1"; + label = "SMC TUD1"; + }; + + temperature-TUD2 { + apple,key-id = "TUD2"; + label = "SMC TUD2"; + }; + + temperature-TUD3 { + apple,key-id = "TUD3"; + label = "SMC TUD3"; + }; + + temperature-TUD4 { + apple,key-id = "TUD4"; + label = "SMC TUD4"; + }; + + temperature-TUD5 { + apple,key-id = "TUD5"; + label = "SMC TUD5"; + }; + + temperature-TUD6 { + apple,key-id = "TUD6"; + label = "SMC TUD6"; + }; + + temperature-TUD7 { + apple,key-id = "TUD7"; + label = "SMC TUD7"; + }; + + temperature-TUD8 { + apple,key-id = "TUD8"; + label = "SMC TUD8"; + }; + + temperature-TUD9 { + apple,key-id = "TUD9"; + label = "SMC TUD9"; + }; + + temperature-TUDX { + apple,key-id = "TUDX"; + label = "SMC TUDX"; + }; + + temperature-TUDa { + apple,key-id = "TUDa"; + label = "SMC TUDa"; + }; + + temperature-TUDb { + apple,key-id = "TUDb"; + label = "SMC TUDb"; + }; + + temperature-TUDc { + apple,key-id = "TUDc"; + label = "SMC TUDc"; + }; + + temperature-TUDd { + apple,key-id = "TUDd"; + label = "SMC TUDd"; + }; + + temperature-TUDe { + apple,key-id = "TUDe"; + label = "SMC TUDe"; + }; + + temperature-TUDf { + apple,key-id = "TUDf"; + label = "SMC TUDf"; + }; + + temperature-TVA0 { + apple,key-id = "TVA0"; + label = "SMC TVA0"; + }; + + temperature-TVD0 { + apple,key-id = "TVD0"; + label = "SMC TVD0"; + }; + + temperature-TVMR { + apple,key-id = "TVMR"; + label = "SMC TVMR"; + }; + + temperature-TVMX { + apple,key-id = "TVMX"; + label = "SMC TVMX"; + }; + + temperature-TVMr { + apple,key-id = "TVMr"; + label = "SMC TVMr"; + }; + + temperature-TVS0 { + apple,key-id = "TVS0"; + label = "SMC TVS0"; + }; + + temperature-TVS1 { + apple,key-id = "TVS1"; + label = "SMC TVS1"; + }; + + temperature-TVS2 { + apple,key-id = "TVS2"; + label = "SMC TVS2"; + }; + + temperature-TVV0 { + apple,key-id = "TVV0"; + label = "SMC TVV0"; + }; + + temperature-TVXX { + apple,key-id = "TVXX"; + label = "SMC TVXX"; + }; + + temperature-TVXh { + apple,key-id = "TVXh"; + label = "SMC TVXh"; + }; + + temperature-TVXm { + apple,key-id = "TVXm"; + label = "SMC TVXm"; + }; + + temperature-TVh0 { + apple,key-id = "TVh0"; + label = "SMC TVh0"; + }; + + temperature-TVh1 { + apple,key-id = "TVh1"; + label = "SMC TVh1"; + }; + + temperature-TVm0 { + apple,key-id = "TVm0"; + label = "SMC TVm0"; + }; + + temperature-TVxx { + apple,key-id = "TVxx"; + label = "SMC TVxx"; + }; + + temperature-TaLP { + apple,key-id = "TaLP"; + label = "SMC TaLP"; + }; + + temperature-TaLT { + apple,key-id = "TaLT"; + label = "SMC TaLT"; + }; + + temperature-TaLW { + apple,key-id = "TaLW"; + label = "SMC TaLW"; + }; + + temperature-TaRF { + apple,key-id = "TaRF"; + label = "SMC TaRF"; + }; + + temperature-TaRT { + apple,key-id = "TaRT"; + label = "SMC TaRT"; + }; + + temperature-TaRW { + apple,key-id = "TaRW"; + label = "SMC TaRW"; + }; + + temperature-TaTP { + apple,key-id = "TaTP"; + label = "SMC TaTP"; + }; + + temperature-Te04 { + apple,key-id = "Te04"; + label = "E-cluster Te04"; + }; + + temperature-Te05 { + apple,key-id = "Te05"; + label = "E-cluster Te05"; + }; + + temperature-Te06 { + apple,key-id = "Te06"; + label = "E-cluster Te06"; + }; + + temperature-Te0R { + apple,key-id = "Te0R"; + label = "E-cluster Te0R"; + }; + + temperature-Te0S { + apple,key-id = "Te0S"; + label = "E-cluster Te0S"; + }; + + temperature-Te0T { + apple,key-id = "Te0T"; + label = "E-cluster Te0T"; + }; + + temperature-TfC0 { + apple,key-id = "TfC0"; + label = "SMC TfC0"; + }; + + temperature-TfC1 { + apple,key-id = "TfC1"; + label = "SMC TfC1"; + }; + + temperature-TfC2 { + apple,key-id = "TfC2"; + label = "SMC TfC2"; + }; + + temperature-TfC3 { + apple,key-id = "TfC3"; + label = "SMC TfC3"; + }; + + temperature-TfC4 { + apple,key-id = "TfC4"; + label = "SMC TfC4"; + }; + + temperature-Tg04 { + apple,key-id = "Tg04"; + label = "GPU Tg04"; + }; + + temperature-Tg0K { + apple,key-id = "Tg0K"; + label = "GPU Tg0K"; + }; + + temperature-Tg0L { + apple,key-id = "Tg0L"; + label = "GPU Tg0L"; + }; + + temperature-Tg0R { + apple,key-id = "Tg0R"; + label = "GPU Tg0R"; + }; + + temperature-Tg0X { + apple,key-id = "Tg0X"; + label = "GPU Tg0X"; + }; + + temperature-Tg0d { + apple,key-id = "Tg0d"; + label = "GPU Tg0d"; + }; + + temperature-Tg0e { + apple,key-id = "Tg0e"; + label = "GPU Tg0e"; + }; + + temperature-Tg0j { + apple,key-id = "Tg0j"; + label = "GPU Tg0j"; + }; + + temperature-Tg0y { + apple,key-id = "Tg0y"; + label = "GPU Tg0y"; + }; + + temperature-Tg1E { + apple,key-id = "Tg1E"; + label = "GPU Tg1E"; + }; + + temperature-Tg1F { + apple,key-id = "Tg1F"; + label = "GPU Tg1F"; + }; + + temperature-Tg1U { + apple,key-id = "Tg1U"; + label = "GPU Tg1U"; + }; + + temperature-Tg1c { + apple,key-id = "Tg1c"; + label = "GPU Tg1c"; + }; + + temperature-Tg1d { + apple,key-id = "Tg1d"; + label = "GPU Tg1d"; + }; + + temperature-Tg1k { + apple,key-id = "Tg1k"; + label = "GPU Tg1k"; + }; + + temperature-Tp00 { + apple,key-id = "Tp00"; + label = "P-cluster Tp00"; + }; + + temperature-Tp01 { + apple,key-id = "Tp01"; + label = "P-cluster Tp01"; + }; + + temperature-Tp02 { + apple,key-id = "Tp02"; + label = "P-cluster Tp02"; + }; + + temperature-Tp04 { + apple,key-id = "Tp04"; + label = "P-cluster Tp04"; + }; + + temperature-Tp05 { + apple,key-id = "Tp05"; + label = "P-cluster Tp05"; + }; + + temperature-Tp06 { + apple,key-id = "Tp06"; + label = "P-cluster Tp06"; + }; + + temperature-Tp08 { + apple,key-id = "Tp08"; + label = "P-cluster Tp08"; + }; + + temperature-Tp09 { + apple,key-id = "Tp09"; + label = "P-cluster Tp09"; + }; + + temperature-Tp0A { + apple,key-id = "Tp0A"; + label = "P-cluster Tp0A"; + }; + + temperature-Tp0C { + apple,key-id = "Tp0C"; + label = "P-cluster Tp0C"; + }; + + temperature-Tp0D { + apple,key-id = "Tp0D"; + label = "P-cluster Tp0D"; + }; + + temperature-Tp0E { + apple,key-id = "Tp0E"; + label = "P-cluster Tp0E"; + }; + + temperature-Tp0G { + apple,key-id = "Tp0G"; + label = "P-cluster Tp0G"; + }; + + temperature-Tp0H { + apple,key-id = "Tp0H"; + label = "P-cluster Tp0H"; + }; + + temperature-Tp0I { + apple,key-id = "Tp0I"; + label = "P-cluster Tp0I"; + }; + + temperature-Tp0K { + apple,key-id = "Tp0K"; + label = "P-cluster Tp0K"; + }; + + temperature-Tp0L { + apple,key-id = "Tp0L"; + label = "P-cluster Tp0L"; + }; + + temperature-Tp0M { + apple,key-id = "Tp0M"; + label = "P-cluster Tp0M"; + }; + + temperature-Tp0O { + apple,key-id = "Tp0O"; + label = "P-cluster Tp0O"; + }; + + temperature-Tp0P { + apple,key-id = "Tp0P"; + label = "P-cluster Tp0P"; + }; + + temperature-Tp0Q { + apple,key-id = "Tp0Q"; + label = "P-cluster Tp0Q"; + }; + + temperature-Tp0S { + apple,key-id = "Tp0S"; + label = "P-cluster Tp0S"; + }; + + temperature-Tp0T { + apple,key-id = "Tp0T"; + label = "P-cluster Tp0T"; + }; + + temperature-Tp0U { + apple,key-id = "Tp0U"; + label = "P-cluster Tp0U"; + }; + + temperature-Tp0W { + apple,key-id = "Tp0W"; + label = "P-cluster Tp0W"; + }; + + temperature-Tp0X { + apple,key-id = "Tp0X"; + label = "P-cluster Tp0X"; + }; + + temperature-Tp0Y { + apple,key-id = "Tp0Y"; + label = "P-cluster Tp0Y"; + }; + + temperature-Tp0a { + apple,key-id = "Tp0a"; + label = "P-cluster Tp0a"; + }; + + temperature-Tp0b { + apple,key-id = "Tp0b"; + label = "P-cluster Tp0b"; + }; + + temperature-Tp0c { + apple,key-id = "Tp0c"; + label = "P-cluster Tp0c"; + }; + + temperature-Tp0d { + apple,key-id = "Tp0d"; + label = "P-cluster Tp0d"; + }; + + temperature-Tp0e { + apple,key-id = "Tp0e"; + label = "P-cluster Tp0e"; + }; + + temperature-Tp0f { + apple,key-id = "Tp0f"; + label = "P-cluster Tp0f"; + }; + + temperature-Tp0g { + apple,key-id = "Tp0g"; + label = "P-cluster Tp0g"; + }; + + temperature-Tp0i { + apple,key-id = "Tp0i"; + label = "P-cluster Tp0i"; + }; + + temperature-Tp0k { + apple,key-id = "Tp0k"; + label = "P-cluster Tp0k"; + }; + + temperature-Tp0l { + apple,key-id = "Tp0l"; + label = "P-cluster Tp0l"; + }; + + temperature-Tp0m { + apple,key-id = "Tp0m"; + label = "P-cluster Tp0m"; + }; + + temperature-Tp0n { + apple,key-id = "Tp0n"; + label = "P-cluster Tp0n"; + }; + + temperature-Tp0o { + apple,key-id = "Tp0o"; + label = "P-cluster Tp0o"; + }; + + temperature-Tp0q { + apple,key-id = "Tp0q"; + label = "P-cluster Tp0q"; + }; + + temperature-Tp0t { + apple,key-id = "Tp0t"; + label = "P-cluster Tp0t"; + }; + + temperature-Tp0u { + apple,key-id = "Tp0u"; + label = "P-cluster Tp0u"; + }; + + temperature-Tp0v { + apple,key-id = "Tp0v"; + label = "P-cluster Tp0v"; + }; + + temperature-Tp0w { + apple,key-id = "Tp0w"; + label = "P-cluster Tp0w"; + }; + + temperature-Tp1i { + apple,key-id = "Tp1i"; + label = "P-cluster Tp1i"; + }; + + temperature-Tp1j { + apple,key-id = "Tp1j"; + label = "P-cluster Tp1j"; + }; + + temperature-Tp1k { + apple,key-id = "Tp1k"; + label = "P-cluster Tp1k"; + }; + + temperature-Tp1m { + apple,key-id = "Tp1m"; + label = "P-cluster Tp1m"; + }; + + temperature-Tp1n { + apple,key-id = "Tp1n"; + label = "P-cluster Tp1n"; + }; + + temperature-Tp1o { + apple,key-id = "Tp1o"; + label = "P-cluster Tp1o"; + }; + + temperature-Tp1q { + apple,key-id = "Tp1q"; + label = "P-cluster Tp1q"; + }; + + temperature-Tp1t { + apple,key-id = "Tp1t"; + label = "P-cluster Tp1t"; + }; + + temperature-Tp1u { + apple,key-id = "Tp1u"; + label = "P-cluster Tp1u"; + }; + + temperature-Tp1v { + apple,key-id = "Tp1v"; + label = "P-cluster Tp1v"; + }; + + temperature-Tp1w { + apple,key-id = "Tp1w"; + label = "P-cluster Tp1w"; + }; + + temperature-Tp1x { + apple,key-id = "Tp1x"; + label = "P-cluster Tp1x"; + }; + + temperature-Tp1y { + apple,key-id = "Tp1y"; + label = "P-cluster Tp1y"; + }; + + temperature-Tp1z { + apple,key-id = "Tp1z"; + label = "P-cluster Tp1z"; + }; + + temperature-Tp20 { + apple,key-id = "Tp20"; + label = "P-cluster Tp20"; + }; + + temperature-Tp21 { + apple,key-id = "Tp21"; + label = "P-cluster Tp21"; + }; + + temperature-Tp22 { + apple,key-id = "Tp22"; + label = "P-cluster Tp22"; + }; + + temperature-Tp23 { + apple,key-id = "Tp23"; + label = "P-cluster Tp23"; + }; + + temperature-Tp24 { + apple,key-id = "Tp24"; + label = "P-cluster Tp24"; + }; + + temperature-Tp25 { + apple,key-id = "Tp25"; + label = "P-cluster Tp25"; + }; + + temperature-Tp26 { + apple,key-id = "Tp26"; + label = "P-cluster Tp26"; + }; + + temperature-Tp27 { + apple,key-id = "Tp27"; + label = "P-cluster Tp27"; + }; + + temperature-Tp28 { + apple,key-id = "Tp28"; + label = "P-cluster Tp28"; + }; + + temperature-Tp29 { + apple,key-id = "Tp29"; + label = "P-cluster Tp29"; + }; + + temperature-Tp2A { + apple,key-id = "Tp2A"; + label = "P-cluster Tp2A"; + }; + + temperature-Tp2B { + apple,key-id = "Tp2B"; + label = "P-cluster Tp2B"; + }; + + temperature-Tp2C { + apple,key-id = "Tp2C"; + label = "P-cluster Tp2C"; + }; + + temperature-Tp2D { + apple,key-id = "Tp2D"; + label = "P-cluster Tp2D"; + }; + + temperature-Tp2E { + apple,key-id = "Tp2E"; + label = "P-cluster Tp2E"; + }; + + temperature-Tp2G { + apple,key-id = "Tp2G"; + label = "P-cluster Tp2G"; + }; + + temperature-Tp2I { + apple,key-id = "Tp2I"; + label = "P-cluster Tp2I"; + }; + + temperature-Tp2J { + apple,key-id = "Tp2J"; + label = "P-cluster Tp2J"; + }; + + temperature-Tp2K { + apple,key-id = "Tp2K"; + label = "P-cluster Tp2K"; + }; + + temperature-Tp2L { + apple,key-id = "Tp2L"; + label = "P-cluster Tp2L"; + }; + + temperature-Tp2M { + apple,key-id = "Tp2M"; + label = "P-cluster Tp2M"; + }; + + temperature-Tp2N { + apple,key-id = "Tp2N"; + label = "P-cluster Tp2N"; + }; + + temperature-Tp2O { + apple,key-id = "Tp2O"; + label = "P-cluster Tp2O"; + }; + + temperature-Tp2Q { + apple,key-id = "Tp2Q"; + label = "P-cluster Tp2Q"; + }; + + temperature-Tp2R { + apple,key-id = "Tp2R"; + label = "P-cluster Tp2R"; + }; + + temperature-Tp2S { + apple,key-id = "Tp2S"; + label = "P-cluster Tp2S"; + }; + + temperature-Tp2T { + apple,key-id = "Tp2T"; + label = "P-cluster Tp2T"; + }; + + temperature-Tp2U { + apple,key-id = "Tp2U"; + label = "P-cluster Tp2U"; + }; + + temperature-Tp2V { + apple,key-id = "Tp2V"; + label = "P-cluster Tp2V"; + }; + + temperature-Tp2W { + apple,key-id = "Tp2W"; + label = "P-cluster Tp2W"; + }; + + temperature-Tp2X { + apple,key-id = "Tp2X"; + label = "P-cluster Tp2X"; + }; + + temperature-Ts00 { + apple,key-id = "Ts00"; + label = "SMC Ts00"; + }; + + temperature-Ts01 { + apple,key-id = "Ts01"; + label = "SMC Ts01"; + }; + + temperature-Ts02 { + apple,key-id = "Ts02"; + label = "SMC Ts02"; + }; + + temperature-Ts04 { + apple,key-id = "Ts04"; + label = "SMC Ts04"; + }; + + temperature-Ts05 { + apple,key-id = "Ts05"; + label = "SMC Ts05"; + }; + + temperature-Ts06 { + apple,key-id = "Ts06"; + label = "SMC Ts06"; + }; + + temperature-Ts08 { + apple,key-id = "Ts08"; + label = "SMC Ts08"; + }; + + temperature-Ts09 { + apple,key-id = "Ts09"; + label = "SMC Ts09"; + }; + + temperature-Ts0A { + apple,key-id = "Ts0A"; + label = "SMC Ts0A"; + }; + + temperature-Ts0C { + apple,key-id = "Ts0C"; + label = "SMC Ts0C"; + }; + + temperature-Ts0D { + apple,key-id = "Ts0D"; + label = "SMC Ts0D"; + }; + + temperature-Ts0E { + apple,key-id = "Ts0E"; + label = "SMC Ts0E"; + }; + + temperature-Ts0G { + apple,key-id = "Ts0G"; + label = "SMC Ts0G"; + }; + + temperature-Ts0H { + apple,key-id = "Ts0H"; + label = "SMC Ts0H"; + }; + + temperature-Ts0I { + apple,key-id = "Ts0I"; + label = "SMC Ts0I"; + }; + + temperature-Ts0K { + apple,key-id = "Ts0K"; + label = "SMC Ts0K"; + }; + + temperature-Ts0L { + apple,key-id = "Ts0L"; + label = "SMC Ts0L"; + }; + + temperature-Ts0M { + apple,key-id = "Ts0M"; + label = "SMC Ts0M"; + }; + + temperature-Ts0O { + apple,key-id = "Ts0O"; + label = "SMC Ts0O"; + }; + + temperature-Ts0P { + apple,key-id = "Ts0P"; + label = "SMC Ts0P"; + }; + + temperature-Ts0Q { + apple,key-id = "Ts0Q"; + label = "SMC Ts0Q"; + }; + + temperature-Ts0R { + apple,key-id = "Ts0R"; + label = "SMC Ts0R"; + }; + + temperature-Ts0T { + apple,key-id = "Ts0T"; + label = "SMC Ts0T"; + }; + + temperature-Ts0U { + apple,key-id = "Ts0U"; + label = "SMC Ts0U"; + }; + + temperature-Ts0W { + apple,key-id = "Ts0W"; + label = "SMC Ts0W"; + }; + + temperature-Ts1P { + apple,key-id = "Ts1P"; + label = "SMC Ts1P"; + }; + + temperature-Tsx0 { + apple,key-id = "Tsx0"; + label = "SMC Tsx0"; + }; + + temperature-Tsx1 { + apple,key-id = "Tsx1"; + label = "SMC Tsx1"; + }; + + voltage-VDM2 { + apple,key-id = "VDM2"; + label = "SMC VDM2"; + }; + + voltage-VDMA { + apple,key-id = "VDMA"; + label = "SMC VDMA"; + }; + + voltage-VDMM { + apple,key-id = "VDMM"; + label = "SMC VDMM"; + }; + + voltage-VP0R { + apple,key-id = "VP0R"; + label = "SMC VP0R"; + }; + + voltage-VP0b { + apple,key-id = "VP0b"; + label = "SMC VP0b"; + }; + + voltage-VP1b { + apple,key-id = "VP1b"; + label = "SMC VP1b"; + }; + + voltage-VP1l { + apple,key-id = "VP1l"; + label = "SMC VP1l"; + }; + + voltage-VP2b { + apple,key-id = "VP2b"; + label = "SMC VP2b"; + }; + + voltage-VP3b { + apple,key-id = "VP3b"; + label = "SMC VP3b"; + }; + + voltage-VP4b { + apple,key-id = "VP4b"; + label = "SMC VP4b"; + }; + + voltage-VP5l { + apple,key-id = "VP5l"; + label = "SMC VP5l"; + }; + + voltage-VP7b { + apple,key-id = "VP7b"; + label = "SMC VP7b"; + }; + + voltage-VP7l { + apple,key-id = "VP7l"; + label = "SMC VP7l"; + }; + + voltage-VP9b { + apple,key-id = "VP9b"; + label = "SMC VP9b"; + }; + + voltage-VP9l { + apple,key-id = "VP9l"; + label = "SMC VP9l"; + }; + + voltage-VPal { + apple,key-id = "VPal"; + label = "SMC VPal"; + }; + + voltage-VPbb { + apple,key-id = "VPbb"; + label = "SMC VPbb"; + }; + + voltage-VPdb { + apple,key-id = "VPdb"; + label = "SMC VPdb"; + }; + + voltage-VPeb { + apple,key-id = "VPeb"; + label = "SMC VPeb"; + }; + + voltage-VR0b { + apple,key-id = "VR0b"; + label = "SMC VR0b"; + }; + + voltage-VR1b { + apple,key-id = "VR1b"; + label = "SMC VR1b"; + }; + + voltage-VR2b { + apple,key-id = "VR2b"; + label = "SMC VR2b"; + }; + + voltage-VR3b { + apple,key-id = "VR3b"; + label = "SMC VR3b"; + }; + + voltage-VR3l { + apple,key-id = "VR3l"; + label = "SMC VR3l"; + }; + + voltage-VR4b { + apple,key-id = "VR4b"; + label = "SMC VR4b"; + }; + + voltage-VR7b { + apple,key-id = "VR7b"; + label = "SMC VR7b"; + }; + + voltage-VR8l { + apple,key-id = "VR8l"; + label = "SMC VR8l"; + }; + + voltage-VR9b { + apple,key-id = "VR9b"; + label = "SMC VR9b"; + }; + + voltage-VR9l { + apple,key-id = "VR9l"; + label = "SMC VR9l"; + }; + + voltage-VRal { + apple,key-id = "VRal"; + label = "SMC VRal"; + }; + + voltage-VRbb { + apple,key-id = "VRbb"; + label = "SMC VRbb"; + }; + + voltage-VRdb { + apple,key-id = "VRdb"; + label = "SMC VRdb"; + }; + + voltage-VReb { + apple,key-id = "VReb"; + label = "SMC VReb"; + }; + + voltage-VS0b { + apple,key-id = "VS0b"; + label = "SMC VS0b"; + }; + + voltage-VS1b { + apple,key-id = "VS1b"; + label = "SMC VS1b"; + }; + + voltage-VS2b { + apple,key-id = "VS2b"; + label = "SMC VS2b"; + }; + + voltage-VS3b { + apple,key-id = "VS3b"; + label = "SMC VS3b"; + }; + + voltage-VS4b { + apple,key-id = "VS4b"; + label = "SMC VS4b"; + }; + + voltage-VS7b { + apple,key-id = "VS7b"; + label = "SMC VS7b"; + }; + + voltage-VS9b { + apple,key-id = "VS9b"; + label = "SMC VS9b"; + }; + + voltage-VS9l { + apple,key-id = "VS9l"; + label = "SMC VS9l"; + }; + + voltage-VSal { + apple,key-id = "VSal"; + label = "SMC VSal"; + }; + + voltage-VSbb { + apple,key-id = "VSbb"; + label = "SMC VSbb"; + }; + + voltage-VSeb { + apple,key-id = "VSeb"; + label = "SMC VSeb"; + }; + + voltage-Vb0f { + apple,key-id = "Vb0f"; + label = "SMC Vb0f"; + }; + + voltage-Vb1f { + apple,key-id = "Vb1f"; + label = "SMC Vb1f"; + }; + + voltage-SBA1 { + apple,key-id = "SBA1"; + label = "Battery Cell 1"; + }; + + voltage-SBA2 { + apple,key-id = "SBA2"; + label = "Battery Cell 2"; + }; + + voltage-SBA3 { + apple,key-id = "SBA3"; + label = "Battery Cell 3"; + }; + + current-I3F2 { + apple,key-id = "I3F2"; + label = "SMC I3F2"; + }; + + current-I5SR { + apple,key-id = "I5SR"; + label = "SMC I5SR"; + }; + + current-IALR { + apple,key-id = "IALR"; + label = "SMC IALR"; + }; + + current-IARR { + apple,key-id = "IARR"; + label = "SMC IARR"; + }; + + current-ICMC { + apple,key-id = "ICMC"; + label = "SMC ICMC"; + }; + + current-ID3C { + apple,key-id = "ID3C"; + label = "SMC ID3C"; + }; + + current-IDBC { + apple,key-id = "IDBC"; + label = "SMC IDBC"; + }; + + current-IDBR { + apple,key-id = "IDBR"; + label = "SMC IDBR"; + }; + + current-IFLR { + apple,key-id = "IFLR"; + label = "SMC IFLR"; + }; + + current-IH0R { + apple,key-id = "IH0R"; + label = "SMC IH0R"; + }; + + current-IHCR { + apple,key-id = "IHCR"; + label = "SMC IHCR"; + }; + + current-IIDR { + apple,key-id = "IIDR"; + label = "SMC IIDR"; + }; + + current-IINC { + apple,key-id = "IINC"; + label = "SMC IINC"; + }; + + current-IMVC { + apple,key-id = "IMVC"; + label = "SMC IMVC"; + }; + + current-IMVR { + apple,key-id = "IMVR"; + label = "SMC IMVR"; + }; + + current-IP0b { + apple,key-id = "IP0b"; + label = "SMC IP0b"; + }; + + current-IP1b { + apple,key-id = "IP1b"; + label = "SMC IP1b"; + }; + + current-IP1l { + apple,key-id = "IP1l"; + label = "SMC IP1l"; + }; + + current-IP2b { + apple,key-id = "IP2b"; + label = "SMC IP2b"; + }; + + current-IP3b { + apple,key-id = "IP3b"; + label = "SMC IP3b"; + }; + + current-IP4b { + apple,key-id = "IP4b"; + label = "SMC IP4b"; + }; + + current-IP5l { + apple,key-id = "IP5l"; + label = "SMC IP5l"; + }; + + current-IP7b { + apple,key-id = "IP7b"; + label = "SMC IP7b"; + }; + + current-IP7l { + apple,key-id = "IP7l"; + label = "SMC IP7l"; + }; + + current-IP9b { + apple,key-id = "IP9b"; + label = "SMC IP9b"; + }; + + current-IP9l { + apple,key-id = "IP9l"; + label = "SMC IP9l"; + }; + + current-IPBR { + apple,key-id = "IPBR"; + label = "SMC IPBR"; + }; + + current-IPMC { + apple,key-id = "IPMC"; + label = "SMC IPMC"; + }; + + current-IPSC { + apple,key-id = "IPSC"; + label = "SMC IPSC"; + }; + + current-IPal { + apple,key-id = "IPal"; + label = "SMC IPal"; + }; + + current-IPbb { + apple,key-id = "IPbb"; + label = "SMC IPbb"; + }; + + current-IPdb { + apple,key-id = "IPdb"; + label = "SMC IPdb"; + }; + + current-IPeb { + apple,key-id = "IPeb"; + label = "SMC IPeb"; + }; + + current-IR0b { + apple,key-id = "IR0b"; + label = "SMC IR0b"; + }; + + current-IR1b { + apple,key-id = "IR1b"; + label = "SMC IR1b"; + }; + + current-IR2b { + apple,key-id = "IR2b"; + label = "SMC IR2b"; + }; + + current-IR3b { + apple,key-id = "IR3b"; + label = "SMC IR3b"; + }; + + current-IR3l { + apple,key-id = "IR3l"; + label = "SMC IR3l"; + }; + + current-IR4b { + apple,key-id = "IR4b"; + label = "SMC IR4b"; + }; + + current-IR7b { + apple,key-id = "IR7b"; + label = "SMC IR7b"; + }; + + current-IR8l { + apple,key-id = "IR8l"; + label = "SMC IR8l"; + }; + + current-IR9b { + apple,key-id = "IR9b"; + label = "SMC IR9b"; + }; + + current-IR9l { + apple,key-id = "IR9l"; + label = "SMC IR9l"; + }; + + current-IRal { + apple,key-id = "IRal"; + label = "SMC IRal"; + }; + + current-IRbb { + apple,key-id = "IRbb"; + label = "SMC IRbb"; + }; + + current-IRdb { + apple,key-id = "IRdb"; + label = "SMC IRdb"; + }; + + current-IReb { + apple,key-id = "IReb"; + label = "SMC IReb"; + }; + + current-IS0b { + apple,key-id = "IS0b"; + label = "SMC IS0b"; + }; + + current-IS1b { + apple,key-id = "IS1b"; + label = "SMC IS1b"; + }; + + current-IS2b { + apple,key-id = "IS2b"; + label = "SMC IS2b"; + }; + + current-IS3b { + apple,key-id = "IS3b"; + label = "SMC IS3b"; + }; + + current-IS4b { + apple,key-id = "IS4b"; + label = "SMC IS4b"; + }; + + current-IS7b { + apple,key-id = "IS7b"; + label = "SMC IS7b"; + }; + + current-IS9b { + apple,key-id = "IS9b"; + label = "SMC IS9b"; + }; + + current-IS9l { + apple,key-id = "IS9l"; + label = "SMC IS9l"; + }; + + current-ISDR { + apple,key-id = "ISDR"; + label = "SMC ISDR"; }; - /* - * The speaker amps' shared shutdown line (SDZ, AP pin 54, low = - * shutdown). One fixed regulator, the SDZ supply of all six, not - * per-amp shutdown-gpios: with GPIO_SHARED six references become a - * vote, and one amp driving the line low would not shut the other - * five down. - */ - speaker_sdz: regulator-sn012776-sdz { - compatible = "regulator-fixed"; - regulator-name = "sn012776-sdz"; - gpios = <&pinctrl 54 GPIO_ACTIVE_HIGH>; - enable-active-high; + current-ISal { + apple,key-id = "ISal"; + label = "SMC ISal"; }; - /* - * The AOP firmware runs the stream (MCA cluster 0, one serializer for - * all six amps); the J616 variant of macaudio links the AOP audio - * component's speaker back-end (DAI 3) to the amps. The codecs are - * listed in the ADT provider order, so each gets its native playback - * slot (LW1 0, RW1 1, LW2 2, RW2 3, LT 4, RT 5). The idle masks are the - * ADT amp-tx-zd-config rows 0 and 1: LW1 drives zeros in the right - * amps' sense slots, RW1 in the left amps'. - */ - sound: sound { - compatible = "apple,j616-macaudio", "apple,macaudio"; - model = "MacBook Pro J616"; - #address-cells = <1>; - #size-cells = <0>; + current-ISbb { + apple,key-id = "ISbb"; + label = "SMC ISbb"; + }; - dai-link@0 { - reg = <0>; - link-name = "Speakers"; - dai-tdm-idle-mode-0 = "zero"; - dai-tdm-idle-mode-1 = "zero"; - dai-tdm-slot-tx-idle-mask-0 = <0xf0f0f0>; - dai-tdm-slot-tx-idle-mask-1 = <0x0f0f0f>; + current-ISeb { + apple,key-id = "ISeb"; + label = "SMC ISeb"; + }; - cpu { - sound-dai = <&aop_audio 3>; - }; + current-IU1C { + apple,key-id = "IU1C"; + label = "SMC IU1C"; + }; - codec { - sound-dai = <&speaker_left_woof1>, <&speaker_right_woof1>, - <&speaker_left_woof2>, <&speaker_right_woof2>, - <&speaker_left_tweet>, <&speaker_right_tweet>; - }; - }; + current-IU1R { + apple,key-id = "IU1R"; + label = "SMC IU1R"; }; - /* - * The headphone jack: the AOP audio component's jack back-end (DAI 4) - * to the CS42L84. A card of its own, so a codec that does not answer - * costs the jack only, never the speakers. - */ - sound_jack: sound-jack { - compatible = "apple,j616-macaudio-jack", "apple,macaudio"; - model = "MacBook Pro J616 Jack"; - #address-cells = <1>; - #size-cells = <0>; + current-IU2C { + apple,key-id = "IU2C"; + label = "SMC IU2C"; + }; - dai-link@0 { - reg = <0>; - link-name = "Headphone Jack"; + current-IU2R { + apple,key-id = "IU2R"; + label = "SMC IU2R"; + }; - cpu { - sound-dai = <&aop_audio 4>; - }; + current-IW3C { + apple,key-id = "IW3C"; + label = "SMC IW3C"; + }; - codec { - sound-dai = <&jack_codec>; - }; - }; + current-Ib0f { + apple,key-id = "Ib0f"; + label = "SMC Ib0f"; }; -}; -&cpuidle { - status = "okay"; -}; + current-Ib8f { + apple,key-id = "Ib8f"; + label = "SMC Ib8f"; + }; -&smc { - status = "okay"; -}; + current-SBAC { + apple,key-id = "SBAC"; + label = "Battery Current"; + }; -&smc_mbox { - status = "okay"; -}; + power-P3F2 { + apple,key-id = "P3F2"; + label = "SMC P3F2"; + }; -&nub_spmi0 { - status = "okay"; -}; + power-P5SR { + apple,key-id = "P5SR"; + label = "SMC P5SR"; + }; -&pinctrl { - status = "okay"; + power-PALR { + apple,key-id = "PALR"; + label = "SMC PALR"; + }; - wifi_clkreq_pins: wifi-clkreq-pins { - pinmux = ; + power-PARR { + apple,key-id = "PARR"; + label = "SMC PARR"; }; -}; -/* - * The AP takes none of the always-on bank's interrupts: the AOP firmware - * owns some of its pins and interrupt registers, and a chained handler here - * would see their pending bits without a handler of ours. - */ -&pinctrl_nub { - status = "okay"; - /delete-property/ interrupt-controller; - /delete-property/ #interrupt-cells; - /delete-property/ interrupts; -}; + power-PCMC { + apple,key-id = "PCMC"; + label = "SMC PCMC"; + }; -&fpwm0 { - status = "okay"; -}; + power-PD3C { + apple,key-id = "PD3C"; + label = "SMC PD3C"; + }; -/* The Type-C retimers (ADT atcrt at 0x18/0x19/0x1a) sit on this bus; no driver yet. */ -&i2c6 { - status = "okay"; -}; + power-PDBC { + apple,key-id = "PDBC"; + label = "SMC PDBC"; + }; -&mtp { - status = "okay"; -}; + power-PDBR { + apple,key-id = "PDBR"; + label = "SMC PDBR"; + }; -&mtp_mbox { - status = "okay"; -}; + power-PFLR { + apple,key-id = "PFLR"; + label = "SMC PFLR"; + }; -&mtp_dart { - status = "okay"; -}; + power-PH0R { + apple,key-id = "PH0R"; + label = "SMC PH0R"; + }; -&mtp_dockchannel { - status = "okay"; -}; + power-PHCR { + apple,key-id = "PHCR"; + label = "SMC PHCR"; + }; -&mtp_hid { - /* SMC key gp1c powers the trackpad analog front end. */ - apple,afe-smc = <&smc>; + power-PIDR { + apple,key-id = "PIDR"; + label = "SMC PIDR"; + }; - multi-touch { - firmware-name = "apple/tpmtfw-j616s.bin"; + power-PINC { + apple,key-id = "PINC"; + label = "SMC PINC"; }; - keyboard: keyboard { - hid-country-code = <0>; - apple,keyboard-layout-id = <2>; + power-PMVC { + apple,key-id = "PMVC"; + label = "SMC PMVC"; }; - stm { + power-PP0b { + apple,key-id = "PP0b"; + label = "SMC PP0b"; }; - actuator { + power-PP1b { + apple,key-id = "PP1b"; + label = "SMC PP1b"; }; - tp-accel { + power-PP1l { + apple,key-id = "PP1l"; + label = "SMC PP1l"; }; - mtp { + power-PP2b { + apple,key-id = "PP2b"; + label = "SMC PP2b"; }; -}; -&smc_hwmon { - /* - * TCMz must stay the first temperature child: the thermal zone names - * the sensor by its index, so the shared hwmon includes below only - * append after it. - */ - temperature-TCMz { - apple,key-id = "TCMz"; - label = "CPU Die Max"; + power-PP3b { + apple,key-id = "PP3b"; + label = "SMC PP3b"; + }; + + power-PP4b { + apple,key-id = "PP4b"; + label = "SMC PP4b"; + }; + + power-PP5l { + apple,key-id = "PP5l"; + label = "SMC PP5l"; + }; + + power-PP7b { + apple,key-id = "PP7b"; + label = "SMC PP7b"; + }; + + power-PP7l { + apple,key-id = "PP7l"; + label = "SMC PP7l"; + }; + + power-PP9b { + apple,key-id = "PP9b"; + label = "SMC PP9b"; + }; + + power-PP9l { + apple,key-id = "PP9l"; + label = "SMC PP9l"; + }; + + power-PPBR { + apple,key-id = "PPBR"; + label = "SMC PPBR"; + }; + + power-PPMC { + apple,key-id = "PPMC"; + label = "SMC PPMC"; + }; + + power-PPSC { + apple,key-id = "PPSC"; + label = "SMC PPSC"; + }; + + power-PPal { + apple,key-id = "PPal"; + label = "SMC PPal"; + }; + + power-PPbb { + apple,key-id = "PPbb"; + label = "SMC PPbb"; + }; + + power-PPdb { + apple,key-id = "PPdb"; + label = "SMC PPdb"; + }; + + power-PPeb { + apple,key-id = "PPeb"; + label = "SMC PPeb"; + }; + + power-PR0b { + apple,key-id = "PR0b"; + label = "SMC PR0b"; + }; + + power-PR1b { + apple,key-id = "PR1b"; + label = "SMC PR1b"; + }; + + power-PR2b { + apple,key-id = "PR2b"; + label = "SMC PR2b"; + }; + + power-PR3b { + apple,key-id = "PR3b"; + label = "SMC PR3b"; + }; + + power-PR3l { + apple,key-id = "PR3l"; + label = "SMC PR3l"; + }; + + power-PR4b { + apple,key-id = "PR4b"; + label = "SMC PR4b"; + }; + + power-PR7b { + apple,key-id = "PR7b"; + label = "SMC PR7b"; + }; + + power-PR8l { + apple,key-id = "PR8l"; + label = "SMC PR8l"; + }; + + power-PR9b { + apple,key-id = "PR9b"; + label = "SMC PR9b"; + }; + + power-PR9l { + apple,key-id = "PR9l"; + label = "SMC PR9l"; + }; + + power-PRal { + apple,key-id = "PRal"; + label = "SMC PRal"; + }; + + power-PRbb { + apple,key-id = "PRbb"; + label = "SMC PRbb"; + }; + + power-PRdb { + apple,key-id = "PRdb"; + label = "SMC PRdb"; + }; + + power-PReb { + apple,key-id = "PReb"; + label = "SMC PReb"; + }; + + power-PS0b { + apple,key-id = "PS0b"; + label = "SMC PS0b"; + }; + + power-PS1b { + apple,key-id = "PS1b"; + label = "SMC PS1b"; + }; + + power-PS2b { + apple,key-id = "PS2b"; + label = "SMC PS2b"; + }; + + power-PS3b { + apple,key-id = "PS3b"; + label = "SMC PS3b"; + }; + + power-PS4b { + apple,key-id = "PS4b"; + label = "SMC PS4b"; + }; + + power-PS7b { + apple,key-id = "PS7b"; + label = "SMC PS7b"; + }; + + power-PS9b { + apple,key-id = "PS9b"; + label = "SMC PS9b"; + }; + + power-PS9l { + apple,key-id = "PS9l"; + label = "SMC PS9l"; + }; + + power-PSDR { + apple,key-id = "PSDR"; + label = "SMC PSDR"; + }; + + power-PSal { + apple,key-id = "PSal"; + label = "SMC PSal"; + }; + + power-PSbb { + apple,key-id = "PSbb"; + label = "SMC PSbb"; + }; + + power-PSeb { + apple,key-id = "PSeb"; + label = "SMC PSeb"; + }; + + power-PTHS { + apple,key-id = "PTHS"; + label = "SMC PTHS"; + }; + + power-PU1C { + apple,key-id = "PU1C"; + label = "SMC PU1C"; + }; + + power-PU1R { + apple,key-id = "PU1R"; + label = "SMC PU1R"; + }; + + power-PU2C { + apple,key-id = "PU2C"; + label = "SMC PU2C"; + }; + + power-PU2R { + apple,key-id = "PU2R"; + label = "SMC PU2R"; + }; + + power-PW3C { + apple,key-id = "PW3C"; + label = "SMC PW3C"; + }; + + power-PZC0 { + apple,key-id = "PZC0"; + label = "SMC PZC0"; + }; + + power-PZC1 { + apple,key-id = "PZC1"; + label = "SMC PZC1"; + }; + + power-PZCB { + apple,key-id = "PZCB"; + label = "SMC PZCB"; + }; + + power-PZCU { + apple,key-id = "PZCU"; + label = "SMC PZCU"; + }; + + power-PZD1 { + apple,key-id = "PZD1"; + label = "SMC PZD1"; + }; + + power-PZT0 { + apple,key-id = "PZT0"; + label = "SMC PZT0"; + }; + + power-Pb0f { + apple,key-id = "Pb0f"; + label = "SMC Pb0f"; + }; + + power-SBAP { + apple,key-id = "SBAP"; + label = "Battery Power"; }; }; -/* Read-only fan speeds and more SMC sensors; every key exists on this machine's SMC. */ +/* + * Fan speeds, limits, targets and modes, and the common SMC sensors; every + * key exists on this machine's SMC. Both fans list their target (F0Tg, + * F1Tg) and mode (F0Md, F1Md) keys, so with the driver's fan_control + * parameter fanN_target becomes writable; by default the SMC drives the + * fans and the attributes are read-only. + */ #include "hwmon-common.dtsi" #include "hwmon-fan-dual.dtsi" #include "hwmon-laptop.dtsi" From 0d1540e6304f2850ba78ff50bcdf8078f7dde983 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Tue, 27 Oct 2026 17:16:17 +1100 Subject: [PATCH 236/388] drm/asahi: retire faulted G17 compute commands on the firmware's fault record When a compute command fails on the GPU the firmware writes a zero-duration completion record for it, which quarantines its queue, and reports the failure with a firmware event of type 7 carrying the queue and the stamp the command would have stored. It never stores that stamp. The driver ignored the event, so the quarantine waited for a retirement witness that could not arrive, and the queue kept its pool, its traversal scratch and its share of the lower GPU VA window for as long as the module stayed loaded. Under ray query stress that fails this way every few seconds (for example ray_query.stress.geometry_shader beside other clients) the retained queues filled the lower window within minutes. Every later allocation there failed: publications returned EBUSY, and gnome-shell crashed on ENOMEM creating a 16K texture after about 7 minutes. Decode the event as ComputeFault and accept its stamp as the faulted command's completion stamp for the quarantine's retirement witness, while it is newer than the stored one. The witness still requires the queue's ring to be fully consumed, so commands published after the failed one keep waiting for their own stamp. Port prerequisite for #505. Keep type 7 unknown on 25G83: the public retirement witness is qualified on 26A428 only, and a matching tag and aligned integers cannot establish T6040 DMA visibility. KUnit checks that identical words cannot become a T6040 release witness. Link: https://github.com/aurora-silicon/linux/commit/9a76756bf2d7122695042095530f03c441a9bcad Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/asahi/g17/channel.rs | 6 +- drivers/gpu/drm/asahi/g17/event.rs | 7 +++ drivers/gpu/drm/asahi/g17/fw/channels.rs | 62 +++++++++++++++++++ drivers/gpu/drm/asahi/g17/queue/compute.rs | 31 +++++++++- .../drm/asahi/g17/queue/compute/retirement.rs | 15 +++++ drivers/gpu/drm/asahi/g17/runtime/events.rs | 11 +++- 6 files changed, 128 insertions(+), 4 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/channel.rs b/drivers/gpu/drm/asahi/g17/channel.rs index d96924cbbf9f9..b06743abfc446 100644 --- a/drivers/gpu/drm/asahi/g17/channel.rs +++ b/drivers/gpu/drm/asahi/g17/channel.rs @@ -147,6 +147,7 @@ impl Rings { /// the caller can service the copied event using the complete device owner. pub(crate) struct Events { role: Role, + graph: super::fw::variant::InitGraph, remaining: u32, unknown_reported: bool, } @@ -167,9 +168,10 @@ impl Events { Ok(consumer != producer) } - pub(crate) fn new(role: Role) -> Self { + pub(crate) fn new(role: Role, graph: super::fw::variant::InitGraph) -> Self { Self { role, + graph, remaining: EVENT_SLOTS, unknown_reported: false, } @@ -196,7 +198,7 @@ impl Events { fence(Ordering::Acquire); let offset = status::EVENT_RING as usize + consumer as usize * EVENT_RECORD_SIZE; let record = EventRecord::from_words(init.status_read_words(role, offset)?); - let event = record.decode(); + let event = record.decode_for(self.graph); fence(Ordering::SeqCst); consumer_word.store((consumer + 1) & (EVENT_SLOTS - 1), Ordering::Relaxed); fence(Ordering::SeqCst); diff --git a/drivers/gpu/drm/asahi/g17/event.rs b/drivers/gpu/drm/asahi/g17/event.rs index 9508a827be4b1..e726a75602b86 100644 --- a/drivers/gpu/drm/asahi/g17/event.rs +++ b/drivers/gpu/drm/asahi/g17/event.rs @@ -36,6 +36,8 @@ pub(super) trait Queues { fn retain_render_completions(&mut self, masks: [u64; 2]); fn complete_compute(&mut self, masks: [u64; 2]) -> Result; fn context_killed(&mut self, cookie: u64); + /// The firmware failed a compute command (see [`Event::ComputeFault`]). + fn compute_fault(&mut self, qid: u8, stamp: u32); } /// Work requested after releasing the device mutex. @@ -72,6 +74,11 @@ fn batch(host: &mut H, role: Role, masks: &mut [u64; 2]) -> Result { } Event::FreeListGrow(completion) => host.complete_freelist_grow(completion)?, Event::ContextKilled(cookie) => host.context_killed(cookie), + Event::ComputeFault { qid, stamp } => { + host.compute_fault(qid, stamp); + // Its queue retires or settles its quarantine on this pass. + masks[usize::from(qid) / 64] |= 1 << (qid % 64); + } Event::Unknown(_) => {} } Ok(true) diff --git a/drivers/gpu/drm/asahi/g17/fw/channels.rs b/drivers/gpu/drm/asahi/g17/fw/channels.rs index 524132c4ae7be..26c6adcf34c83 100644 --- a/drivers/gpu/drm/asahi/g17/fw/channels.rs +++ b/drivers/gpu/drm/asahi/g17/fw/channels.rs @@ -136,6 +136,9 @@ pub(crate) enum Event { TvbGrow(TvbGrowRequest), FreeListGrow(FreeListCompletion), ContextKilled(u64), + /// The firmware failed a compute command of queue `qid` and stores no completion stamp for + /// it: `stamp` is the value that command would have stored. + ComputeFault { qid: u8, stamp: u32 }, Unknown(u32), } @@ -145,6 +148,8 @@ impl EventRecord { const TVB_GROW: u32 = 6; const FREE_LIST_GROW: u32 = 13; const CONTEXT_KILLED: u32 = 12; + const COMPUTE_FAULT: u32 = 7; + const COMPUTE_QIDS: u32 = 128; const DIAGNOSTIC_PAIRS_MAX: u16 = 5; const TVB_SLOTS: u32 = 127; const SUBPIPES: u32 = 64; @@ -160,6 +165,16 @@ impl EventRecord { unsafe { self.words[0] } } + /// Type 7 is a retirement witness only on the firmware whose layout and + /// visibility semantics are qualified. 25G83 must keep consuming it as an + /// unknown event until its own fault contract is established. + pub(crate) fn decode_for(&self, graph: super::variant::InitGraph) -> Result { + if graph == super::variant::InitGraph::V25G83 && self.tag() == Self::COMPUTE_FAULT { + return Ok(Event::Unknown(Self::COMPUTE_FAULT)); + } + self.decode() + } + /// Validates the record's fields without interpreting resource ownership. /// A malformed known event is an error, unlike an unknown tag. pub(crate) fn decode(&self) -> Result { @@ -198,6 +213,19 @@ impl EventRecord { halt_count: record.halt_count, })) } + Self::COMPUTE_FAULT => { + // SAFETY: Every constructor initializes all integer words. + let words = unsafe { self.words }; + // Only compute faults are known to use this type: any other layout stays an + // unknown event, consumed and ignored as before. + if words[3] >= Self::COMPUTE_QIDS || words[4] & 0xff != 0 { + return Ok(Event::Unknown(Self::COMPUTE_FAULT)); + } + Ok(Event::ComputeFault { + qid: words[3] as u8, + stamp: words[4], + }) + } Self::CONTEXT_KILLED => { // SAFETY: Every constructor initializes all integer words. let words = unsafe { self.words }; @@ -525,3 +553,37 @@ impl ContextKill { } } } + +#[cfg_attr(not(test), kernel::macros::kunit_tests(asahi_g17_event_records))] +mod tests { + use super::*; + + fn record(words: &[u32]) -> EventRecord { + let mut all = [0u32; EVENT_WORDS]; + all[..words.len()].copy_from_slice(words); + EventRecord::from_words(all) + } + + #[test] + fn a_compute_fault_names_its_queue_and_stamp() { + let event = record(&[7, 1, 2, 0x39, 0x300, 0xffff_fc00, 1]).decode(); + assert_eq!(event, Ok(Event::ComputeFault { qid: 0x39, stamp: 0x300 })); + } + + #[test] + fn v25g83_does_not_accept_an_unqualified_fault_witness() { + use super::super::variant::InitGraph; + let fault = record(&[7, 1, 2, 0x39, 0x300]); + assert_eq!(fault.decode_for(InitGraph::G17P), + Ok(Event::ComputeFault { qid: 0x39, stamp: 0x300 })); + assert_eq!(fault.decode_for(InitGraph::V25G83), Ok(Event::Unknown(7))); + assert_eq!(record(&[7, 1, 2, 128, 0x301]).decode_for(InitGraph::V25G83), + Ok(Event::Unknown(7))); + } + + #[test] + fn an_unrecognized_type_7_layout_stays_unknown() { + assert_eq!(record(&[7, 1, 2, 128, 0x300]).decode(), Ok(Event::Unknown(7))); + assert_eq!(record(&[7, 1, 2, 9, 0x301]).decode(), Ok(Event::Unknown(7))); + } +} diff --git a/drivers/gpu/drm/asahi/g17/queue/compute.rs b/drivers/gpu/drm/asahi/g17/queue/compute.rs index 9c4c5c92d049d..3d5e12be64842 100644 --- a/drivers/gpu/drm/asahi/g17/queue/compute.rs +++ b/drivers/gpu/drm/asahi/g17/queue/compute.rs @@ -575,6 +575,9 @@ pub(crate) struct Queue { config_cache: Option<(QueueConfig, u64)>, ordinal: u32, submitted: u32, + /// The stamp of the newest command the firmware reported failed ([`Self::note_fault`]), + /// while it is newer than the stored stamp. + fault_stamp: Option, /// Prepared packets of the bound owner about to publish (see [`Self::claim`]). claims: u32, last_published: u64, @@ -694,6 +697,7 @@ impl Queue { config_cache: None, ordinal: 0, submitted: 0, + fault_stamp: None, claims: 0, last_published: 0, last_end: 0, @@ -714,6 +718,29 @@ impl Queue { withheld_doorbell: None, }) } + /// The firmware failed the command whose completion stamp is `stamp`. It writes a + /// zero-duration completion record for it, which quarantines the queue, but stores no + /// stamp: without this the quarantine would wait for its retirement witness forever and + /// keep the queue's backing. Commands published after the failed one still need their own + /// stamp or record. + pub(crate) fn note_fault(&mut self, stamp: u32) { + if self + .fault_stamp + .is_none_or(|old| retirement::stamp_newer(stamp, old)) + { + self.fault_stamp = Some(stamp); + } + } + + /// The stamp that proves retirement: the stored one, or a newer reported fault's. + fn witness_stamp(&self) -> Result { + let stored = self.graph.stamp()?; + Ok(match self.fault_stamp { + Some(fault) if retirement::stamp_newer(fault, stored) => fault, + _ => stored, + }) + } + pub(crate) fn qid(&self) -> u8 { self.kick.id().qid() } @@ -1096,7 +1123,7 @@ impl Queue { if !retirement::quarantined_retired( &newest, self.graph.cursors()?, - self.graph.stamp()?, + self.witness_stamp()?, self.active.len as u32, self.submitted, )? { @@ -1140,6 +1167,7 @@ impl Queue { drop(self.active.pop()); } self.awaiting_witness = false; + self.fault_stamp = None; self.retirement_proved = false; self.replay_retirement = false; self.spared_deferred = false; @@ -1584,6 +1612,7 @@ impl Queue { self.spared_deferred = false; self.retire_pending = false; self.awaiting_witness = false; + self.fault_stamp = None; self.retirement_proved = false; self.replay_retirement = false; self.replay_error = None; diff --git a/drivers/gpu/drm/asahi/g17/queue/compute/retirement.rs b/drivers/gpu/drm/asahi/g17/queue/compute/retirement.rs index c132b87a9860a..6b942dcd86a3e 100644 --- a/drivers/gpu/drm/asahi/g17/queue/compute/retirement.rs +++ b/drivers/gpu/drm/asahi/g17/queue/compute/retirement.rs @@ -52,6 +52,12 @@ impl Cursors { } } +/// Whether stamp `a` was stored after stamp `b` (both kick stamps, modulo wrap). +pub(crate) const fn stamp_newer(a: u32, b: u32) -> bool { + let delta = a.wrapping_sub(b); + delta != 0 && delta < 0x8000_0000 +} + /// Outstanding kicks span fewer than half the 24-bit stamp sequence. Zero is a valid wrap. pub(crate) const fn stamp_covers(word: u32, kick: u64) -> bool { let delta = word.wrapping_sub((kick as u32).wrapping_shl(8)); @@ -455,6 +461,15 @@ mod tests { assert!(stamp_covers(0, 0xff_ffff)); } + #[test] + fn a_reported_fault_stamp_is_newer_only_until_stored() { + assert!(stamp_newer(own(KICK), own(KICK - 1))); + assert!(!stamp_newer(own(KICK), own(KICK))); + assert!(!stamp_newer(own(KICK - 1), own(KICK))); + // Across the wrap of the 24-bit kick in the stamp word. + assert!(stamp_newer(own(0), own(0xff_ffff))); + } + #[test] fn a_late_stamp_is_waited_for() { // Under games on J700 the stamp lands up to about 4 ms after the record while the diff --git a/drivers/gpu/drm/asahi/g17/runtime/events.rs b/drivers/gpu/drm/asahi/g17/runtime/events.rs index 4cf2dd0fafb97..b410a90568d04 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/events.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/events.rs @@ -44,7 +44,7 @@ impl event::Transport for Service<'_> { role: Role, mut consume: impl FnMut(&mut Self, Event) -> Result, ) -> Result { - let mut events = channel::Events::new(role); + let mut events = channel::Events::new(role, self.firmware.primary.state.shared.variant.graph); while let Some(event) = events.next(&self.firmware.primary.state.shared.dev, &self.firmware.init)? { @@ -72,6 +72,15 @@ impl event::Queues for Service<'_> { fn context_killed(&mut self, cookie: u64) { self.firmware.queues.acknowledge_compute_exit(cookie); } + fn compute_fault(&mut self, qid: u8, stamp: u32) { + for entry in self.firmware.queues.compute.iter_mut().flatten() { + if let Some(mut queue) = entry.queue.as_deref_mut() { + if queue.qid() == qid { + queue.note_fault(stamp); + } + } + } + } fn flush_tvb_reply(&mut self) -> Result { self.firmware.flush_tvb_reply() } From bb047c758c11648a328729efb1669f2393d956bd Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Tue, 27 Oct 2026 17:16:17 +1100 Subject: [PATCH 237/388] drm/asahi: kill faulted G17 contexts before freeing their render work When a compute command fails, the firmware holds the commands its context published after it and never runs them; only a kill of the context returns them. The context's render work behind the same failure behaves differently. It runs about a second later, consumes its ring entries and returns its free-list blocks, but the firmware never stores its completion stamp and leaves the job linked in the context's work list until it processes a kill. None of that work could retire, so a closed context whose compute failed kept its pools, its render pairs, its memory and its scheduler teardown until the GPU firmware stopped. Let the kill proceed for such a context. A quarantined render pair whose commands all ran (both pass timestamps written, rings consumed, free list balanced) counts as idle for the kill and is held for it, and the kill no longer waits for render pools that are only released after it. A faulted compute queue with no commands behind the failure is a kill candidate as well. This applies only once the owner has closed; a live owner's quarantined pair stays quarantined. Free only after the firmware acknowledges the kill. The pair's unstamped work is retired once the context's scheduler page reports the kill, and its graph memory stays allocated and out of the reuse cache until then. The detach path enforces the same rule: a context whose work list still links a node and has no acknowledged kill never has its graphs freed or recycled. The firmware leaves a killed context's last job linked even after the kill, so the context's 32 KiB work object is kept out of reuse until the firmware stops; debugfs counts these as firmware_linked_objects and a warning is printed past 256. Reuse a killed context's render pairs last. The kill hold on them ends when the context's scheduler release is consumed. A new owner then takes a clean released pair first, then an unowned slot while the physical queue table still has room for both of its identities, and only then the killed pair released longest ago. Siblings and spares never take killed pairs, and a sibling no longer claims an unowned slot once the table is full. Admission is told whether the table has room whenever the runtime changes it. Publish a failed context's kill only while no other context has a compute command in flight, or once the GPU has retired nothing for the liveness interval. When GPU memory comes within an eighth of the device limit, at most one waiting kill per 250 ms skips that wait. debugfs reports the waits and the pressure kills, and a wait over 5 s is warned about once per kill. The order matters. Earlier versions retired and freed the pair first and killed afterwards, and the firmware then used memory that had been handed out again: GPU recoveries 4-7 ms after the kill and occasional firmware crashes when the kill followed within milliseconds, and a storm of compute watchdog timeouts when it followed minutes later. A run that handed killed pairs to new owners first stranded the teardowns of 24 contexts, and a render stall behind a failed context took the desktop down. Reusing them last without regard to the queue table ran out of physical queues after about 50 kills. A kill published beside other clients' running compute made the firmware recover the GPU twice in ten minutes of a five-lane stress load, which the wait avoids. Waiting without a bound let the dead clients' memory reach the device limit under sustained compute, and gnome-shell then crashed on ENOMEM, which the memory escape avoids. On Neo, ten minutes of a five-lane stress load (render, render2, long ALU, churn and global-priority CTS beside two ray query geometry-shader stress loops) had 52 faults and 15 kills (2 under memory pressure), all acknowledged and their pairs retired. There were 0 GPU recoveries and nothing was left pending afterwards, against 12 teardowns left pending without this change. Ten minutes of two ray query stress loops beside heavy geometry had 51 faults and 51 kills (3 under memory pressure), with 0 recoveries, the physical queue table full at 127 of 128 with no refusals, and 4 render-only teardowns left pending. In both, GPU memory peaked at 4.3 GiB of the 5.0 GiB device limit, no allocation was refused and gnome-shell stayed up. Known limits: - A killed context's 32 KiB work object stays allocated until the GPU firmware restarts. - While other clients keep compute running, a faulted client's memory stays held until its kill publishes: at the next moment without foreign compute, or through the memory escape near the device limit. - A closed context whose compute side is clean but whose render pair holds executed, unstamped work is not killed, since no compute queue is left to carry the kill. A kill that doesn't need a compute exit candidate would close this. - A kill that is safe beside running compute would remove the wait. It is not understood yet. An asahi_g17_teardown tracepoint records fault, unstamped retire, kill, acknowledgement, detach and slot-owner events. Link: https://github.com/aurora-silicon/linux/commit/078d744a260cb923d0ddfcc2b58ce8903be5a96d Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/asahi/g17/context.rs | 38 ++++ drivers/gpu/drm/asahi/g17/event.rs | 1 + drivers/gpu/drm/asahi/g17/kick.rs | 5 + drivers/gpu/drm/asahi/g17/object.rs | 87 ++++++++- drivers/gpu/drm/asahi/g17/queue/admission.rs | 173 ++++++++++++++++-- drivers/gpu/drm/asahi/g17/queue/compute.rs | 12 ++ .../gpu/drm/asahi/g17/queue/compute/exit.rs | 25 ++- drivers/gpu/drm/asahi/g17/queue/render.rs | 48 ++++- .../drm/asahi/g17/queue/render/retirement.rs | 28 +++ drivers/gpu/drm/asahi/g17/runtime.rs | 7 + .../gpu/drm/asahi/g17/runtime/compute_exit.rs | 138 +++++++++++++- drivers/gpu/drm/asahi/g17/runtime/render.rs | 28 ++- .../gpu/drm/asahi/g17/runtime/render/exit.rs | 13 ++ .../drm/asahi/g17/runtime/render/submit.rs | 3 +- .../drm/asahi/g17/runtime/render/teardown.rs | 35 +++- drivers/gpu/drm/asahi/g17/runtime/teardown.rs | 12 ++ drivers/gpu/drm/asahi/g17/trace.c | 6 + drivers/gpu/drm/asahi/g17/trace.h | 19 ++ drivers/gpu/drm/asahi/g17/trace.rs | 10 + drivers/gpu/drm/asahi/g17/trace_api.h | 1 + 20 files changed, 654 insertions(+), 35 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/context.rs b/drivers/gpu/drm/asahi/g17/context.rs index 0d3948a2b69ff..ba74140d4f243 100644 --- a/drivers/gpu/drm/asahi/g17/context.rs +++ b/drivers/gpu/drm/asahi/g17/context.rs @@ -280,6 +280,28 @@ impl Context { _work: self.work.clone(), } } + /// Called once the firmware consumed this context's scheduler release. If its work list + /// still links a node, the firmware keeps referencing that node: on Neo a render command + /// that ran behind a failed compute command is never unlinked, and a later context given + /// the same work storage (the object cache hands it out again) made the firmware recover + /// the GPU. Keep the storage mapped and out of reuse until firmware stop; returns whether + /// it was linked. + pub(crate) fn retain_linked_work(&self, pool: &Arc) -> bool { + // An unreadable head counts as linked: retaining is the safe side. + let linked = self.work_head().map_or(true, |head| head != [0, self.work_head_va()]); + if linked { + self.work.lock().object.retain_until_stop(pool); + } + linked + } + + /// The firmware may still walk this context's work list into memory it names: a node is + /// linked and the scheduler page shows no kill. Unreadable state counts as linked. + pub(crate) fn firmware_may_link(&self) -> bool { + let killed = super::queue::compute::exit::State::read(self).map_or(0, |state| state.killed); + must_retain(self.work_head().ok(), self.work_head_va(), killed) + } + pub(crate) fn work_head_va(&self) -> u64 { self.work_base + (UAT_PGSZ + 0x400) as u64 } @@ -463,10 +485,26 @@ impl QosIds { } } +/// Memory a context's work list may lead the firmware to stays allocated: a linked node (or +/// an unreadable head) and no acknowledged kill. +fn must_retain(head: Option<[u64; 2]>, head_va: u64, killed: u32) -> bool { + killed == 0 && head.is_none_or(|head| head != [0, head_va]) +} + #[kunit_tests(asahi_g17_qos_capacity)] mod capacity_tests { use super::*; + #[test] + fn linked_unkilled_work_is_retained() { + let va = 0xfffffc20_0000_4400; + assert!(!must_retain(Some([0, va]), va, 0)); + assert!(must_retain(Some([0xfffffc20_0000_0000, 0xfffffc20_0000_0098]), va, 0)); + assert!(must_retain(None, va, 0)); + assert!(!must_retain(Some([0xfffffc20_0000_0000, 0xfffffc20_0000_0098]), va, 1)); + assert!(!must_retain(None, va, 1)); + } + #[test] fn failed_client_construction_returns_every_qos_id() { let ids = QosIds::new().unwrap(); diff --git a/drivers/gpu/drm/asahi/g17/event.rs b/drivers/gpu/drm/asahi/g17/event.rs index e726a75602b86..a175e58392379 100644 --- a/drivers/gpu/drm/asahi/g17/event.rs +++ b/drivers/gpu/drm/asahi/g17/event.rs @@ -75,6 +75,7 @@ fn batch(host: &mut H, role: Role, masks: &mut [u64; 2]) -> Result { Event::FreeListGrow(completion) => host.complete_freelist_grow(completion)?, Event::ContextKilled(cookie) => host.context_killed(cookie), Event::ComputeFault { qid, stamp } => { + crate::g17::trace::teardown(1, u64::from(qid), u64::from(stamp), 0); host.compute_fault(qid, stamp); // Its queue retires or settles its quarantine on this pass. masks[usize::from(qid) / 64] |= 1 << (qid % 64); diff --git a/drivers/gpu/drm/asahi/g17/kick.rs b/drivers/gpu/drm/asahi/g17/kick.rs index f694dad86bbf0..6499cae106309 100644 --- a/drivers/gpu/drm/asahi/g17/kick.rs +++ b/drivers/gpu/drm/asahi/g17/kick.rs @@ -145,6 +145,11 @@ impl QueueIds { self.render_ids() + (QID_COUNT - self.floor - self.leased) >= 2 * queues + 2 } + /// Whether a render pair not yet built can still reserve both of its identities. + pub(crate) fn render_pair_fits(&self) -> bool { + QID_COUNT - self.floor - self.leased >= 2 + } + /// Whether a fresh compute identity fits beside the render reserve of /// `queues` open logical queues, without reserving it. pub(crate) fn compute_fits(&self, queues: usize) -> bool { diff --git a/drivers/gpu/drm/asahi/g17/object.rs b/drivers/gpu/drm/asahi/g17/object.rs index f1b22ad5d7331..7cc13dbba705e 100644 --- a/drivers/gpu/drm/asahi/g17/object.rs +++ b/drivers/gpu/drm/asahi/g17/object.rs @@ -466,8 +466,17 @@ struct PoolInner { reserved: [usize; POOL_FAMILIES], bytes: usize, stopped: bool, + /// Objects the firmware may still reference after their owner dropped them, kept mapped + /// and out of reuse until firmware stop (see [`PooledObject::retain_until_stop`]). + kept: KVec, + kept_bytes: usize, + /// Kept objects still owned by their users, whose list entries are reserved. + kept_reserved: usize, } +/// Retained objects past this many are still retained, with one warning. +const KEPT_WARN: usize = 256; + /// Per-device cache-safe object recycling. Every converted mapping reserves /// one return slot before its page-table attributes change. Active leases and /// poisoned mappings count against the same capacity as reusable objects. @@ -490,6 +499,7 @@ struct CacheLease { pub(crate) struct PooledObject { object: core::mem::ManuallyDrop, lease: Option, + keep: Option>, } impl core::ops::Deref for PooledObject { @@ -503,11 +513,28 @@ impl core::ops::DerefMut for PooledObject { &mut self.object } } +impl PooledObject { + /// The firmware may still reference this object after its owner drops it: keep it mapped + /// and never hand it out again before firmware stop. It leaves the reuse cache, so it + /// does not take cache capacity. Call from sleepable context: this reserves the list + /// entry, so the drop, wherever it happens, does not allocate. + pub(crate) fn retain_until_stop(&mut self, pool: &kernel::sync::Arc) { + if self.keep.is_none() { + pool.reserve_kept(); + self.keep = Some(pool.clone()); + } + } +} + impl Drop for PooledObject { fn drop(&mut self) { // SAFETY: This destructor is the only operation that moves `object` // out; ManuallyDrop prevents a second implicit drop of the field. let object = unsafe { core::mem::ManuallyDrop::take(&mut self.object) }; + if let Some(pool) = self.keep.take() { + pool.keep_object(object, self.lease.take()); + return; + } match self.lease.take() { Some(lease) => lease.pool.return_object(object, &lease), None => drop(object), @@ -523,7 +550,10 @@ impl Pool { } kernel::sync::Arc::pin_init( pin_init!(Self { - inner <- kernel::new_mutex!(PoolInner { returned, reserved: [0; POOL_FAMILIES], bytes: 0, stopped: false }, "g17::object::Pool"), + inner <- kernel::new_mutex!(PoolInner { + returned, reserved: [0; POOL_FAMILIES], bytes: 0, stopped: false, kept: KVec::new(), + kept_bytes: 0, kept_reserved: 0, + }, "g17::object::Pool"), }), GFP_KERNEL, ) @@ -572,6 +602,7 @@ impl Pool { return Ok(PooledObject { object: core::mem::ManuallyDrop::new(entry.object), lease: Some(lease()), + keep: None, }); } let object = alloc.kernel(size, mmu::UAT_PGSZ as u64, prot, cpu)?; @@ -596,6 +627,7 @@ impl Pool { let mut result = PooledObject { object: core::mem::ManuallyDrop::new(object), lease: reserved.then(lease), + keep: None, }; if !reserved { return Ok(result); @@ -655,10 +687,58 @@ impl Pool { core::mem::forget(object); } + /// Room for one more kept object, taken by the matching `keep_object`. A failed + /// allocation is reported by that drop, which then leaks the object instead. + fn reserve_kept(&self) { + let mut inner = self.inner.lock(); + let pending = inner.kept_reserved + 1; + if inner.kept.capacity() - inner.kept.len() < pending + && inner.kept.reserve(pending, GFP_KERNEL).is_err() + { + return; + } + inner.kept_reserved = pending; + } + + fn keep_object(&self, object: KernelObject, lease: Option) { + let mut inner = self.inner.lock(); + inner.kept_reserved = inner.kept_reserved.saturating_sub(1); + if let Some(lease) = &lease { + // The object leaves the reuse cache and its capacity. + inner.reserved[lease.family as usize] -= 1; + inner.bytes -= object.size(); + } + if inner.stopped { + drop(inner); + drop(object); + return; + } + let size = object.size(); + if inner.kept.len() == KEPT_WARN { + pr_warn!("G17 keeps {} firmware-linked objects until the GPU firmware restarts\n", KEPT_WARN); + } + // retain_until_stop reserved the entry; this never allocates. + match inner.kept.push_within_capacity(object) { + Ok(()) => inner.kept_bytes += size, + Err(error) => { + // Never unmap an object the firmware may still reference. + pr_err!("G17 could not record a firmware-linked object; leaking it\n"); + core::mem::forget(error.0); + } + } + } + /// Releases retained mappings after both firmware processors have stopped. /// Active objects returned later observe the same stop witness. pub(crate) fn stop_after_firmware(&self) { - self.inner.lock().stopped = true; + let kept = { + let mut inner = self.inner.lock(); + inner.stopped = true; + inner.kept_bytes = 0; + core::mem::take(&mut inner.kept) + }; + // Mapping teardown must not run while the pool mutex is held. + drop(kept); for index in 0..POOL_FAMILIES * POOL_SLOTS { let entry = { let mut inner = self.inner.lock(); @@ -684,6 +764,7 @@ impl Pool { inner.reserved[family], POOL_SLOTS, entries.iter().flatten().count(), entries.iter().flatten().filter(|entry| entry.poisoned).count())?; } - writeln!(f, "cached_object_bytes used={} total={}", inner.bytes, POOL_BYTES) + writeln!(f, "cached_object_bytes used={} total={}", inner.bytes, POOL_BYTES)?; + writeln!(f, "firmware_linked_objects kept={} bytes={}", inner.kept.len(), inner.kept_bytes) } } diff --git a/drivers/gpu/drm/asahi/g17/queue/admission.rs b/drivers/gpu/drm/asahi/g17/queue/admission.rs index 5d770ca1f64b0..5c657444be01f 100644 --- a/drivers/gpu/drm/asahi/g17/queue/admission.rs +++ b/drivers/gpu/drm/asahi/g17/queue/admission.rs @@ -71,11 +71,19 @@ struct State { /// their own fence with EBUSY instead of a device-loss error, so the VM stays usable. spared: u64, kill_hold: u64, + /// Slots whose kill hold ended at their killed context's consumed scheduler release, + /// and when. A new owner takes such a pair last, the one released longest ago first. + killed: u64, + killed_at: [u64; cfg::RENDER_SLOTS], + /// The physical queue table still has room for a pair built in an unowned slot. The + /// runtime updates this whenever the table changes. + build_room: bool, released: u64, epoch: u64, footprints: KVec, next_ticket: u64, } + impl State { const MASK: u64 = (u64::MAX >> (64 - cfg::RENDER_SLOTS)) & !1; const fn new() -> Self { @@ -87,6 +95,9 @@ impl State { quarantined: 0, spared: 0, kill_hold: 0, + killed: 0, + killed_at: [0; cfg::RENDER_SLOTS], + build_room: true, released: 0, epoch: 0, footprints: KVec::new(), @@ -120,6 +131,15 @@ impl State { self.epochs[slot] = self.epoch; } fn take_idle(&mut self, slot: usize, owner: u64) -> u8 { + if self.owners[slot] != owner { + crate::g17::trace::teardown( + 6, + slot as u64, + self.owners[slot], + owner | u64::from(self.killed & (1u64 << slot) != 0) << 63, + ); + self.killed &= !(1u64 << slot); + } self.refs[slot] += 1; self.active |= 1u64 << slot; self.released &= !(1u64 << slot); @@ -165,13 +185,23 @@ impl State { } let free = !self.active & Self::MASK & !self.quarantined & !self.kill_hold; let eligible = |slot: usize| free & (1u64 << slot) != 0; - // Reuse a closed, exactly retired owner before consuming a new graph. + let released = |slot: usize| self.released & (1u64 << slot) != 0; + let unowned = || (0..cfg::RENDER_SLOTS).find(|slot| eligible(*slot) && self.owners[*slot] == 0); + // Reuse a closed, exactly retired owner before consuming a new graph. A killed + // context's pair comes after a new graph while one can still be built: on Neo, new + // owners of such pairs had renders that executed but never stored their stamps. + // Once the queue table is full it is reused, the one released longest ago first, + // so killed pairs never strand physical queues. let slot = (0..cfg::RENDER_SLOTS) - .filter(|slot| eligible(*slot) && self.released & (1u64 << slot) != 0) + .filter(|slot| eligible(*slot) && released(*slot) && self.killed & (1u64 << slot) == 0) .min_by_key(|slot| self.epochs[*slot]) + .or_else(|| if self.build_room { unowned() } else { None }) .or_else(|| { - (0..cfg::RENDER_SLOTS).find(|slot| eligible(*slot) && self.owners[*slot] == 0) - })?; + (0..cfg::RENDER_SLOTS) + .filter(|slot| eligible(*slot) && released(*slot)) + .min_by_key(|slot| self.killed_at[*slot]) + }) + .or_else(unowned)?; let first = self.take_idle(slot, owner); self.claim_sibling(owner); Some(first) @@ -180,7 +210,8 @@ impl State { if self.owners.iter().filter(|id| **id == owner).count() >= cfg::RENDER_PAIRS_PER_OWNER { return; } - let free = !self.active & Self::MASK & !self.quarantined & !self.kill_hold; + // A speculative sibling never takes a killed context's pair. + let free = !self.active & Self::MASK & !self.quarantined & !self.kill_hold & !self.killed; let eligible = |slot: usize| free & (1u64 << slot) != 0; let sibling = (0..cfg::RENDER_SLOTS) .filter(|slot| { @@ -190,6 +221,10 @@ impl State { }) .min_by_key(|slot| self.epochs[*slot]) .or_else(|| { + // Nor an unowned slot once no pair can be built there. + if !self.build_room { + return None; + } (0..cfg::RENDER_SLOTS).find(|slot| eligible(*slot) && self.owners[*slot] == 0) }); if let Some(slot) = sibling { @@ -223,6 +258,7 @@ impl State { let bit = 1u64 << slot; let old = self.owners[slot]; if old == 0 + || self.killed & bit != 0 || owner == Some(old) || expected.is_some_and(|expected| expected != old) || self.refs[slot] != 0 @@ -648,9 +684,27 @@ impl Pool { state.quarantined &= !released; state.spared &= !released; state.kill_hold = 0; + state.killed = 0; drop(state); self.returned(); } + + /// Whether the physical queue table can hold another pair (see `State::build_room`). + pub(crate) fn note_build_room(&self, room: bool) { + let mut state = self.state.lock(); + if state.build_room != room { + state.build_room = room; + drop(state); + self.returned(); + } + } + + /// A killed context's pair is free for a new owner. + pub(crate) fn killed_pair_free(&self) -> bool { + let state = self.state.lock(); + state.killed & state.released & !state.active & !state.quarantined & !state.kill_hold & State::MASK != 0 + } + } #[kunit_tests(asahi_g17_admission_accounting)] @@ -863,7 +917,7 @@ impl Drop for RenderLease { } impl State { - fn hold_for_exit_kill(&mut self, owner: u64, slots: u64) -> bool { + fn hold_for_exit_kill(&mut self, owner: u64, slots: u64, executed: u64) -> bool { if owner == 0 || slots & !Self::MASK != 0 { return false; } @@ -872,10 +926,10 @@ impl State { if slots & bit == 0 { continue; } + // A pair whose commands all ran is held with them (see `Pair::executed_for_kill`). if self.owners[slot] != owner - || self.refs[slot] != 0 - || self.active & bit != 0 - || self.released & bit == 0 + || (executed & bit == 0 + && (self.refs[slot] != 0 || self.active & bit != 0 || self.released & bit == 0)) { return false; } @@ -887,11 +941,11 @@ impl State { impl Pool { /// Hold the departed owner's idle pairs before publishing its exit record. /// Successful publication retains these bits until both processors stop. - pub(crate) fn hold_render_slots_for_exit_kill(&self, owner: u64, slots: u64) -> bool { - self.state.lock().hold_for_exit_kill(owner, slots) + pub(crate) fn hold_render_slots_for_exit_kill(&self, owner: u64, slots: u64, executed: u64) -> bool { + self.state.lock().hold_for_exit_kill(owner, slots, executed) } - /// Undo only a hold whose control record was not published. + /// Undo a hold whose control record was not published. pub(crate) fn release_exit_kill_hold(&self, slots: u64) { if slots == 0 { return; @@ -899,6 +953,26 @@ impl Pool { self.state.lock().kill_hold &= !slots; self.returned(); } + + /// End the kill hold of `slots` once their killed context's scheduler release was + /// consumed (the firmware no longer names these pairs). A new owner takes them last. + pub(crate) fn release_killed_slots(&self, slots: u64) { + let mut state = self.state.lock(); + let held = state.kill_hold & slots & State::MASK; + if held == 0 { + return; + } + let now = crate::g17::now_ns(); + state.kill_hold &= !held; + state.killed |= held; + for slot in 0..cfg::RENDER_SLOTS { + if held & (1u64 << slot) != 0 { + state.killed_at[slot] = now; + } + } + drop(state); + self.returned(); + } } impl Pool { @@ -972,13 +1046,86 @@ mod fault_capacity_tests { let bit = 1u64 << lease.slot(); pool.release_owner(1); drop(lease); - assert!(pool.hold_render_slots_for_exit_kill(1, bit)); + assert!(pool.hold_render_slots_for_exit_kill(1, bit, 0)); assert!(!pool.reassign_unused_spare(bit.trailing_zeros() as u8, Some(1), Some(2))); // A failed (unpublished) control may roll back its hold. Published kills // keep the existing processor-stop rule; this test must not weaken it. pool.release_retained_owners_after_processor_stop(); assert_eq!(pool.state.lock().kill_hold, 0); } + + #[test] + fn consumed_scheduler_release_ends_the_kill_hold() { + let pool = Pool::new(MAX_EXECUTION_CONTEXTS).unwrap(); + let lease = pool.acquire_render(1, None, u64::MAX, || Ok(())).unwrap(); + let slot = lease.slot(); + let bit = 1u64 << slot; + pool.release_owner(1); + drop(lease); + assert!(pool.hold_render_slots_for_exit_kill(1, bit, 0)); + assert!(!pool.reassign_unused_spare(slot, Some(1), Some(2))); + pool.release_killed_slots(bit); + assert_eq!(pool.state.lock().kill_hold, 0); + assert_eq!(pool.state.lock().killed, bit); + assert!(pool.killed_pair_free()); + // The killed context's pair is never reassigned as a spare, and a new owner takes + // a new graph before it. + assert!(!pool.reassign_unused_spare(slot, Some(1), Some(2))); + let next = pool.acquire_render(2, None, u64::MAX, || Ok(())).unwrap(); + assert_ne!(next.slot(), slot); + drop(next); + pool.release_retained_owners_after_processor_stop(); + assert_eq!(pool.state.lock().killed, 0); + } + + #[test] + fn killed_pairs_are_taken_last_oldest_first() { + let pool = Pool::new(MAX_EXECUTION_CONTEXTS).unwrap(); + { + let mut state = pool.state.lock(); + for slot in 1..cfg::RENDER_SLOTS { + state.owners[slot] = 100 + slot as u64; + state.released |= 1u64 << slot; + state.killed |= 1u64 << slot; + state.killed_at[slot] = 1000 - slot as u64; + } + // One pair released without a kill. + state.killed &= !(1u64 << 7); + } + let first = pool.acquire_render(1, None, u64::MAX, || Ok(())).unwrap(); + assert_eq!(first.slot(), 7); + let second = pool.acquire_render(2, None, u64::MAX, || Ok(())).unwrap(); + assert_eq!(usize::from(second.slot()), cfg::RENDER_SLOTS - 1); + assert_eq!(pool.state.lock().killed & (1u64 << second.slot()), 0); + } + + #[test] + fn a_full_queue_table_reuses_killed_pairs_before_unowned_slots() { + let pool = Pool::new(MAX_EXECUTION_CONTEXTS).unwrap(); + { + let mut state = pool.state.lock(); + for slot in 1..8 { + state.owners[slot] = 100 + slot as u64; + state.released |= 1u64 << slot; + state.killed |= 1u64 << slot; + state.killed_at[slot] = 1000 - slot as u64; + } + } + // With room in the queue table a new owner builds a pair in an unowned slot. + let built = pool.acquire_render(1, None, u64::MAX, || Ok(())).unwrap(); + assert!(usize::from(built.slot()) >= 8); + // Without room it takes the killed pair released longest ago. + pool.note_build_room(false); + let reused = pool.acquire_render(2, None, u64::MAX, || Ok(())).unwrap(); + assert_eq!(reused.slot(), 7); + assert_eq!(pool.state.lock().killed & (1u64 << 7), 0); + // It claims no sibling: neither another killed pair nor an unowned slot. + let state = pool.state.lock(); + for slot in 1..7 { + assert_eq!(state.owners[slot], 100 + slot as u64); + } + assert_eq!(state.owners.iter().filter(|owner| **owner == 2).count(), 1); + } } impl Pool { diff --git a/drivers/gpu/drm/asahi/g17/queue/compute.rs b/drivers/gpu/drm/asahi/g17/queue/compute.rs index 3d5e12be64842..68fd03c42b42b 100644 --- a/drivers/gpu/drm/asahi/g17/queue/compute.rs +++ b/drivers/gpu/drm/asahi/g17/queue/compute.rs @@ -566,6 +566,9 @@ pub(crate) struct Queue { replay_error: Option, replay_prepared: bool, exit: Option, + /// Since when the kill of this failed queue's context waits for other contexts' compute, + /// and whether that wait was reported. + kill_wait: Option<(u64, bool)>, installed: bool, /// Bit `p` set once a work slot of priority class `p` named this queue. outer_started: u8, @@ -691,6 +694,7 @@ impl Queue { replay_error: None, replay_prepared: false, exit: None, + kill_wait: None, installed: false, outer_started: 0, published_config: None, @@ -732,6 +736,14 @@ impl Queue { } } + /// The firmware failed a command of this quarantined queue, and the queue still waits for + /// its retirement witness: commands published after the failed one, which the firmware + /// never runs, or the failed command itself, whose ring entry it does not consume (a ray + /// traversal's, on Neo). Only a kill of the queue's context gives them back. + pub(crate) fn fault_pending(&self) -> bool { + self.fault_stamp.is_some() && self.quarantined && self.awaiting_witness && self.exit.is_none() + } + /// The stamp that proves retirement: the stored one, or a newer reported fault's. fn witness_stamp(&self) -> Result { let stored = self.graph.stamp()?; diff --git a/drivers/gpu/drm/asahi/g17/queue/compute/exit.rs b/drivers/gpu/drm/asahi/g17/queue/compute/exit.rs index d7386e335e167..ae812dd41fc12 100644 --- a/drivers/gpu/drm/asahi/g17/queue/compute/exit.rs +++ b/drivers/gpu/drm/asahi/g17/queue/compute/exit.rs @@ -83,11 +83,12 @@ impl Transaction { } impl Queue { - /// A command with ray traversal scratch is never killed. On Neo, killing a + /// A running command with ray traversal scratch is never killed. On Neo, killing a /// traversal that stopped making progress either crashed the GPU firmware /// when the queue's pool was then released, or stalled every later render /// when it was retained; left alone, the rest of the GPU kept working. Such - /// a queue stays quarantined and retained until its commands retire. + /// a queue stays quarantined and retained until its commands retire. A + /// traversal the firmware reported failed no longer runs (see `fault_pending`). fn active_traversal(&self) -> bool { self.active.iter().any(|active| { matches!( @@ -101,7 +102,7 @@ impl Queue { && self.released && self.active.len != 0 && self.exit.is_none() - && !self.active_traversal() + && (!self.active_traversal() || self.fault_pending()) && (self.quarantined || self.retired_by_teardown) && age >= if self.quarantined { @@ -124,6 +125,23 @@ impl Queue { .and_then(|active| active.ticket.map(|ticket| ticket.owner)) }) } + /// The kill of this queue's failed context waits; returns how long it has waited, and + /// whether this is the first time it crossed `slow`. + pub(crate) fn note_kill_wait(&mut self, now: u64, slow: u64) -> (u64, bool) { + let (since, warned) = self.kill_wait.get_or_insert((now, false)); + let waited = now.saturating_sub(*since); + let crossed = waited >= slow && !*warned; + *warned |= crossed; + (waited, crossed) + } + /// How long the kill of this queue's failed context has been waiting, if it is. + pub(crate) fn kill_waiting(&self, now: u64) -> Option { + self.kill_wait.map(|(since, _)| now.saturating_sub(since)) + } + /// The kill was published: how long it waited, if it did. + pub(crate) fn take_kill_wait(&mut self, now: u64) -> u64 { + self.kill_wait.take().map_or(0, |(since, _)| now.saturating_sub(since)) + } pub(crate) fn record_exit(&mut self, cookie: u64, now: u64) { self.exit = Some(Transaction { cookie, @@ -162,6 +180,7 @@ impl Queue { return false; } exit.phase = Phase::Acknowledged; + crate::g17::trace::teardown(4, cookie, u64::from(self.qid()), 0); true } pub(crate) fn settle_exit( diff --git a/drivers/gpu/drm/asahi/g17/queue/render.rs b/drivers/gpu/drm/asahi/g17/queue/render.rs index 696a1486eb0ec..bcf10f97fa2d7 100644 --- a/drivers/gpu/drm/asahi/g17/queue/render.rs +++ b/drivers/gpu/drm/asahi/g17/queue/render.rs @@ -1614,7 +1614,43 @@ impl Pair { let counters = self.free_list.counters()?; let stamps = host.stamps(self.qids())?; let (_, state) = host.epoch()?; - Ok(retirement::drained(self.ordinal, stamps, counters, state)) + Ok(retirement::drained(self.ordinal, stamps, counters, state) + || (state == 0 && self.executed_unstamped() && self.context_killed())) + } + + /// The firmware acknowledged a kill of this pair's context (scheduler page): it no longer + /// walks that context's work list, so the commands it never unlinked can be released. + fn context_killed(&self) -> bool { + crate::g17::queue::compute::exit::State::read(&self.memory.context).is_ok_and(|state| state.killed != 0) + } + + /// The pair's context may be killed although its commands were not retired: they all ran + /// (`executed_unstamped`), so the kill leaves nothing queued for the firmware to run. + /// Its memory stays until the kill is acknowledged. + pub(crate) fn executed_for_kill(&self, context: &Arc) -> bool { + Arc::ptr_eq(context, &self.memory.context) && self.executed_unstamped() + } + + /// Every command of this quarantined pair ran, its ring entries were consumed and its + /// free-list blocks returned, yet the firmware never stores the stamp of the last one. + /// On Neo this is the render work of a context whose compute command failed: it runs + /// about a second after the failure, but its stamp never arrives and it stays linked in + /// the firmware's job list, so neither the stamp proof nor the abandoned-group proof + /// ever holds and the pair would stay quarantined, keeping its context, until the + /// processors stop. + fn executed_unstamped(&self) -> bool { + self.quarantined + // Only for a closed owner: a live owner would go on publishing into a context + // whose work list still links the unstamped node. + && self.owner.is_none() + && self.prepared.is_none() + && self.active.len() != 0 + && self.free_list.counters().is_ok_and(|counters| counters[0] == counters[1]) + && self.active.iter().all(|active| { + active.outer_published + && observation::snapshot(&self.memory, active.ticket.ordinal) + .is_ok_and(retirement::Snapshot::executed_and_consumed) + }) } /// Account one witnessed tracker, preserving all completion ownership. @@ -1628,6 +1664,10 @@ impl Pair { let qids = self.qids(); let qos = self.qos_owners(); let buffer = self.buffer_id(); + // Commands the firmware never unlinked are released only after it acknowledged the + // kill of their context, on every path (see `firmware_drained`). + let unstamped = self.executed_unstamped(); + let killed = unstamped && self.context_killed(); let Some(active) = self.active.iter_mut().find(|a| !a.flist_completed) else { return Ok(false); }; @@ -1638,6 +1678,12 @@ impl Pair { }; let result = match retirement::poll(&mut active.ticket, &mut observer)? { retirement::Poll::Complete(ticks) => Ok(ticks), + // Its commands ran without a stamp (see `executed_unstamped`): they already failed + // with the quarantine, and the client sees them cancelled. + retirement::Poll::Pending | retirement::Poll::Retry if killed => { + crate::g17::trace::teardown(2, u64::from(self.slot), active.ticket.ordinal, 0); + Err(ECANCELED) + } retirement::Poll::Pending | retirement::Poll::Retry => { core::sync::atomic::fence(core::sync::atomic::Ordering::Acquire); let snapshot = observer.snapshot(active.ticket.ordinal)?; diff --git a/drivers/gpu/drm/asahi/g17/queue/render/retirement.rs b/drivers/gpu/drm/asahi/g17/queue/render/retirement.rs index 00e4997ef3661..7996970ee369f 100644 --- a/drivers/gpu/drm/asahi/g17/queue/render/retirement.rs +++ b/drivers/gpu/drm/asahi/g17/queue/render/retirement.rs @@ -127,6 +127,12 @@ impl Snapshot { pub(crate) fn untouched(self) -> bool { self.pass == [0, 0] } + + /// The command ran (tiler start and fragment end written) and the firmware consumed both + /// queues' ring entries. + pub(crate) fn executed_and_consumed(self) -> bool { + self.executed() && self.cursors.iter().all(|cursors| cursors.done == cursors.write) + } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -355,3 +361,25 @@ impl Tracker { .filter_map(Option::as_mut) } } + +#[kunit_tests(asahi_g17_render_retirement)] +mod executed_tests { + use super::*; + + fn snapshot(done: u32, write: u32, pass: [u64; 2]) -> Snapshot { + Snapshot { + cursors: [Cursors { done, read: done, write }; 2], + job_list_empty: false, + pass, + } + } + + #[test] + fn executed_and_consumed_needs_both_passes_and_consumed_rings() { + assert!(snapshot(3, 3, [1, 2]).executed_and_consumed()); + assert!(!snapshot(3, 3, [0, 0]).executed_and_consumed()); + assert!(!snapshot(3, 3, [1, 0]).executed_and_consumed()); + assert!(!snapshot(3, 3, [0, 2]).executed_and_consumed()); + assert!(!snapshot(2, 3, [1, 2]).executed_and_consumed()); + } +} diff --git a/drivers/gpu/drm/asahi/g17/runtime.rs b/drivers/gpu/drm/asahi/g17/runtime.rs index 9fe17db8a2809..1e721a51f726e 100644 --- a/drivers/gpu/drm/asahi/g17/runtime.rs +++ b/drivers/gpu/drm/asahi/g17/runtime.rs @@ -541,6 +541,7 @@ impl Registry { .qids .reserve_for(identity, DataMaster::Compute, Some(FIRST_COMPUTE_QID), queues) .map_err(|error| self.compute_admission_error(error))?; + self.note_build_room(); let reservation = ComputeReservation { slot, id, owner }; self.compute[slot] = Some(ComputeEntry { reservation, @@ -584,6 +585,7 @@ impl Registry { self.reserved(reservation)?; self.qids.cancel_unpublished(reservation.id)?; self.compute[reservation.slot] = None; + self.note_build_room(); Ok(()) } @@ -601,6 +603,7 @@ impl Registry { if unpublished { self.qids.cancel_unpublished(entry.reservation.id)?; self.compute[slot] = None; + self.note_build_room(); } Ok(()) } @@ -2373,6 +2376,10 @@ impl Registry { writeln!(f, "qos_ids used={} total=126", self.qos_ids.used())?; writeln!(f, "metrics_ids used={} total={}", self.metrics.used(), super::fw::buffer::METRICS_SLOTS)?; self.render.write_capacity(f)?; + let now = now_ns(); + let waits = || self.compute.iter().flatten().filter_map(|entry| entry.queue.as_deref()) + .filter_map(|queue| queue.kill_waiting(now)); + compute_exit::write_kill_waits(f, waits().count(), waits().max().unwrap_or(0))?; writeln!(f, "compute_slots used={} total={} reusable={} building={}", self.compute.iter().flatten().count(), self.compute.len(), self.compute.iter().flatten().filter_map(|entry| entry.queue.as_deref()).filter(|queue| queue.reusable()).count(), diff --git a/drivers/gpu/drm/asahi/g17/runtime/compute_exit.rs b/drivers/gpu/drm/asahi/g17/runtime/compute_exit.rs index bf9d023038a51..791c1fccae6ae 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/compute_exit.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/compute_exit.rs @@ -12,9 +12,57 @@ use crate::g17::{ timeout::LIVENESS_NS, Firmware, MSG_CONTROL_NOTIFY, }; -use core::sync::atomic::{fence, Ordering}; +use core::sync::atomic::{fence, AtomicU32, AtomicU64, Ordering}; use kernel::{prelude::*, sync::Arc}; +/// Waits of failed contexts' kills for a moment without other contexts' compute in flight, +/// by bucket (under 10 ms, 100 ms, 1 s, 5 s, and longer) and the longest. +static KILL_WAITS: [AtomicU32; 5] = [const { AtomicU32::new(0) }; 5]; +static KILL_WAIT_MAX_NS: AtomicU64 = AtomicU64::new(0); +const KILL_WAIT_BOUNDS_NS: [u64; 4] = [10_000_000, 100_000_000, 1_000_000_000, 5_000_000_000]; +/// A failed context's kill waiting this long is reported, once per kill and at most once per +/// this interval across kills. +const SLOW_KILL_WAIT_NS: u64 = 5_000_000_000; +static SLOW_KILL_WARNED_NS: AtomicU64 = AtomicU64::new(0); +/// Failed contexts' kills published beside other contexts' compute under GPU memory pressure, +/// and when the last one was: at most one per [`PRESSED_KILL_INTERVAL_NS`], so the memory of +/// one is freed before another is considered. +static PRESSED_KILLS: AtomicU32 = AtomicU32::new(0); +static PRESSED_KILL_NS: AtomicU64 = AtomicU64::new(0); +const PRESSED_KILL_INTERVAL_NS: u64 = 250_000_000; + +/// GPU memory is within an eighth of the device limit. A failed context holds its memory +/// until its kill is acknowledged, so kills waiting for other contexts' compute to pause can +/// push the device to its limit, and then every client's allocations fail: on Neo, +/// gnome-shell crashed on such an ENOMEM. A kill beside foreign compute risks one GPU +/// recovery instead. +fn memory_pressed() -> bool { + let ledger = &crate::memacct::LEDGER; + let limit = ledger.limit.load(Ordering::Relaxed); + limit != 0 + && ledger + .user_committed + .load(Ordering::Relaxed) + .saturating_add(ledger.kernel_resident.load(Ordering::Relaxed)) + > limit - limit / 8 +} + +fn record_kill_wait(waited: u64) { + let bucket = KILL_WAIT_BOUNDS_NS.iter().position(|bound| waited < *bound).unwrap_or(4); + KILL_WAITS[bucket].fetch_add(1, Ordering::Relaxed); + KILL_WAIT_MAX_NS.fetch_max(waited, Ordering::Relaxed); +} + +pub(in crate::g17) fn write_kill_waits(f: &mut kernel::fmt::Formatter<'_>, waiting: usize, longest: u64) + -> kernel::fmt::Result +{ + let count = |bucket: usize| KILL_WAITS[bucket].load(Ordering::Relaxed); + writeln!(f, "failed_kill_waits under_10ms={} under_100ms={} under_1s={} under_5s={} longer={} max_ms={} memory_pressed={}", + count(0), count(1), count(2), count(3), count(4), + KILL_WAIT_MAX_NS.load(Ordering::Relaxed) / 1_000_000, PRESSED_KILLS.load(Ordering::Relaxed))?; + writeln!(f, "failed_kills waiting={} longest_wait_ms={}", waiting, longest / 1_000_000) +} + impl super::Registry { /// ACK storage belongs to the already-retained transaction, so a full event /// batch cannot overflow a separate cache. No allocation or wait is needed. @@ -53,16 +101,22 @@ impl Firmware { let _ = queue.announce_exit(now, || self.primary.notify(MSG_CONTROL_NOTIFY)); } } + // Under memory pressure one kill skips the wait, at most one per interval. + let last = PRESSED_KILL_NS.load(Ordering::Relaxed); + let mut pressed = + (last == 0 || now.saturating_sub(last) >= PRESSED_KILL_INTERVAL_NS) && memory_pressed(); for index in 0..self.queues.teardown.len() { let Some((context, age)) = self.queues.teardown.closed(index, now) else { continue; }; + // A failed context's executed pairs keep their pool leases until the kill is + // acknowledged (`Pair::firmware_drained`), so its kill cannot wait for them. if !self.queues.render.teardown_idle(&context)? - || !self.queues.render.teardown_pools_released(&context) + || (!self.compute_failed(&context) && !self.queues.render.teardown_pools_released(&context)) { continue; } - if let Err(error) = self.publish_compute_exit(&context, age, now) { + if let Err(error) = self.publish_compute_exit(&context, age, now, &mut pressed) { dev_err!( self.primary.state.shared.dev.as_ref(), "Scheduler {} exit request failed: {:?}\n", @@ -74,7 +128,7 @@ impl Firmware { Ok(()) } - fn publish_compute_exit(&mut self, context: &Arc, age: u64, now: u64) -> Result { + fn publish_compute_exit(&mut self, context: &Arc, age: u64, now: u64, pressed: &mut bool) -> Result { let Some(index) = self.queues.compute.iter().position(|entry| { entry .as_ref() @@ -83,7 +137,36 @@ impl Firmware { }) else { return Ok(()); }; - if self.render_control_backpressured() || self.kill_unsafe(context, now) { + let failed = self.compute_failed(context); + if self.render_control_backpressured() { + return Ok(()); + } + let beside = failed && self.foreign_compute_running(context, now); + if failed { + if beside && !*pressed { + let queue = self.queues.compute[index] + .as_mut() + .and_then(|entry| entry.queue.as_deref_mut()); + if let Some(mut queue) = queue { + let (waited, crossed) = queue.note_kill_wait(now, SLOW_KILL_WAIT_NS); + let last = SLOW_KILL_WARNED_NS.load(Ordering::Relaxed); + if crossed + && (last == 0 || now.saturating_sub(last) >= SLOW_KILL_WAIT_NS) + && SLOW_KILL_WARNED_NS + .compare_exchange(last, now.max(1), Ordering::Relaxed, Ordering::Relaxed) + .is_ok() + { + dev_warn!( + self.primary.state.shared.dev.as_ref(), + "Scheduler {} kill has waited {} ms for other contexts' compute to pause\n", + context.id(), + waited / 1_000_000 + ); + } + } + return Ok(()); + } + } else if self.kill_unsafe(context, now) { return Ok(()); } let (owner, target) = { @@ -122,7 +205,7 @@ impl Firmware { && !self .queues .admission - .hold_render_slots_for_exit_kill(owner, hold) + .hold_render_slots_for_exit_kill(owner, hold, self.queues.render.executed_slots(context) & hold) { return Ok(()); } @@ -131,13 +214,23 @@ impl Firmware { self.queues.admission.release_exit_kill_hold(hold); return if error == EAGAIN { Ok(()) } else { Err(error) }; } - // Publication is irrevocable even if its doorbell fails. Keep both - // the killed graph and the held render identities until processor stop. + // Publication is irrevocable even if its doorbell fails. Keep the killed + // graph until processor stop, and the held render identities until this + // context's scheduler release is consumed or processors stop. let mut queue = self.queues.compute[index] .as_mut() .and_then(|entry| entry.queue.as_deref_mut()) .ok_or(EIO)?; + if failed { + record_kill_wait(queue.take_kill_wait(now)); + } + if beside { + *pressed = false; + PRESSED_KILLS.fetch_add(1, Ordering::Relaxed); + PRESSED_KILL_NS.store(now.max(1), Ordering::Relaxed); + } queue.record_exit(cookie, now); + crate::g17::trace::teardown(3, cookie, u64::from(context.id()), hold | u64::from(beside)); fence(Ordering::SeqCst); if let Err(error) = queue.announce_exit(now, || self.primary.notify(MSG_CONTROL_NOTIFY)) { dev_warn!( @@ -150,12 +243,39 @@ impl Firmware { Ok(()) } + /// Another context's compute command is in flight. On Neo, kills of failed contexts beside + /// other clients' compute occasionally made the firmware recover the GPU (twice in ten + /// minutes of a five-lane load with 67 faults), even with nothing freed before the kill's + /// acknowledgement; such a kill takes the next moment without foreign compute instead. + /// Render work is not waited for. A GPU that retired nothing for [`LIVENESS_NS`] has + /// stopped. + fn foreign_compute_running(&self, context: &Arc, now: u64) -> bool { + let stalled = crate::g17::recovery::since_retired(now).is_none_or(|since| since >= LIVENESS_NS); + !stalled + && self.queues.compute.iter().flatten().filter_map(|entry| entry.queue.as_deref()).any(|queue| { + queue.in_flight() && !queue.context().is_some_and(|bound| Arc::ptr_eq(bound, context)) + }) + } + + /// A compute command of `context` failed and the firmware holds commands behind it that it + /// never runs. `publish_compute_exit` is only reached once `teardown_idle` found all of + /// the context's render pairs idle; the pairs behind such a failure become idle when + /// their commands have executed (`Pair::firmware_drained`), so by then nothing of the + /// context runs any more. + fn compute_failed(&self, context: &Arc) -> bool { + self.queues.compute.iter().flatten().filter_map(|entry| entry.queue.as_deref()).any(|queue| { + queue.context().is_some_and(|bound| Arc::ptr_eq(bound, context)) && queue.fault_pending() + }) + } + /// A kill while other contexts' commands run makes the firmware restart the /// GPU, and repeated ones crash it; on an otherwise idle GPU it is safe. A /// closed context's commands left alone retire like any others once the GPU /// reaches them, so its queues stay quarantined and retained until then. Only /// a GPU that retired nothing for [`LIVENESS_NS`] has stopped, and then the - /// kill proceeds. + /// kill proceeds. A context whose compute failed ([`Self::compute_failed`]) + /// is not left alone, since its held commands never retire: its kill waits + /// only for other contexts' compute ([`Self::foreign_compute_running`]). fn kill_unsafe(&self, context: &Arc, now: u64) -> bool { let stalled = crate::g17::recovery::since_retired(now).is_none_or(|since| since >= LIVENESS_NS); if stalled { diff --git a/drivers/gpu/drm/asahi/g17/runtime/render.rs b/drivers/gpu/drm/asahi/g17/runtime/render.rs index b5649f059f5d2..22cd1f18876ca 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render.rs @@ -76,6 +76,9 @@ pub(super) struct RenderState { pools: pools::Pools, grow: grow::Grow, retired: KVec, + /// Graphs of released contexts the firmware may still walk (see `detach_context`): kept + /// until the firmware stops, when this state is dropped. + kept_graphs: KVec, next_pm_generation: u64, } @@ -102,6 +105,7 @@ impl RenderState { pools: pools::Pools::new()?, grow: grow::Grow::new()?, retired: KVec::with_capacity(128, GFP_KERNEL)?, + kept_graphs: KVec::new(), next_pm_generation: 1, }) } @@ -300,6 +304,7 @@ impl Registry { if let Some(pair) = entry.pair.as_ref() { self.buffers.unreserve_owner(pair.buffer_id())?; } + self.note_build_room(); } return Ok(Some(UnusedRenderSpare { _entry: entry, @@ -357,11 +362,25 @@ impl Registry { .any(|(slot, entry)| entry.is_some() && self.admission.owns_slot(owner, slot)) } + /// Tell admission whether another render pair can still be built. + pub(super) fn note_build_room(&self) { + self.admission.note_build_room(self.qids.render_pair_fits()); + } + pub(super) fn ensure_render_ids(&mut self, slot: u8, owner: u64) -> Result { if owner == 0 || slot == 0 || usize::from(slot) >= RENDER_SLOTS { return Err(EINVAL); } - if self.render.entries[usize::from(slot)].is_none() { + if self.render.entries[usize::from(slot)].is_some() { + return Ok(()); + } + let built = self.build_render_ids(slot, owner); + self.note_build_room(); + built + } + + fn build_render_ids(&mut self, slot: u8, owner: u64) -> Result { + { let identity = self.identities.reserve(2)?.start; let tiling = self.qids.reserve(identity, DataMaster::Tiling, None)?; let fragment = match self.qids.reserve(identity + 1, DataMaster::Fragment, None) { @@ -405,6 +424,13 @@ impl Registry { /// ensure_render_ids cancels a first ID if the second cannot be reserved; /// validation, identity overflow, and cancellation errors still propagate. fn prepare_render_ids(&mut self, slot: u8, owner: u64, optional: bool) -> Result { + // A full queue table refuses a sibling without scanning it on every preparation. + if optional + && self.render.entries.get(usize::from(slot)).is_some_and(Option::is_none) + && !self.qids.render_pair_fits() + { + return Ok(false); + } match self.ensure_render_ids(slot, owner) { Ok(()) => Ok(true), Err(error) if optional && error == ENOSPC => Ok(false), diff --git a/drivers/gpu/drm/asahi/g17/runtime/render/exit.rs b/drivers/gpu/drm/asahi/g17/runtime/render/exit.rs index c47110cb76fad..25e1335c5ba13 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render/exit.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render/exit.rs @@ -26,6 +26,19 @@ impl RenderState { (own, foreign, slots) } + /// Slots of `context`'s pairs whose commands all ran but stay unretired until its kill. + pub(in crate::g17::runtime) fn executed_slots(&self, context: &Arc) -> u64 { + let mut slots = 0; + for (slot, entry) in self.entries.iter().enumerate() { + if entry.as_ref().and_then(|entry| entry.pair.as_ref()) + .is_some_and(|pair| pair.executed_for_kill(context)) + { + slots |= 1u64 << slot; + } + } + slots + } + pub(in crate::g17::runtime) fn qids_for_slot(&self, slot: u8) -> u128 { self.entries .get(usize::from(slot)) diff --git a/drivers/gpu/drm/asahi/g17/runtime/render/submit.rs b/drivers/gpu/drm/asahi/g17/runtime/render/submit.rs index 1ef6742b689a7..e6379178138d1 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render/submit.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render/submit.rs @@ -131,7 +131,8 @@ impl Backend { .reclaim_unused_render_spare(Some(self.owner), true)?; let ready = firmware.queues.has_render_ids(self.owner); drop(state); - if reclaimed.is_none() && !ready { + // A full queue table makes admission reuse a free killed pair instead. + if reclaimed.is_none() && !ready && !pool.killed_pair_free() { self.wait_render_capacity(&mut observed, started, sleep)?; } drop(reclaimed); diff --git a/drivers/gpu/drm/asahi/g17/runtime/render/teardown.rs b/drivers/gpu/drm/asahi/g17/runtime/render/teardown.rs index e54b1f4638f58..18063f0cd6a14 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render/teardown.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render/teardown.rs @@ -38,7 +38,7 @@ impl RenderState { return Ok(false); } if let Some(pair) = entry.pair.as_ref() { - if !pair.idle_for_context(context)? { + if !pair.idle_for_context(context)? && !pair.executed_for_kill(context) { return Ok(false); } } @@ -123,6 +123,7 @@ impl RenderState { if !out.has_room(count, 1) { return Err(EBUSY); } + let linked = context.firmware_may_link(); for entry in self.entries.iter_mut().flatten() { if !entry .context @@ -140,7 +141,14 @@ impl RenderState { let (mut parked, retired) = KBox::into_inner(pair).park(); // This park follows the consumed release of `context`. parked.release_consumed(context.scheduler_va()); - *target = Some(retired); + if linked { + // Guard (kill before free): the firmware may still walk this context's work + // list into the graph, so it is not freed or recycled before the firmware stops. + kernel::pr_warn_once!("G17: keeping render graphs of an unkilled context that still links work\n"); + keep_graph(&mut self.kept_graphs, retired); + } else { + *target = Some(retired); + } entry.parked = Some(parked); // Detached backing retains these references until the worker runs. entry.context = None; @@ -155,8 +163,12 @@ impl RenderState { if let Some(id) = self.retired[index].buffer_id() { buffers.unreserve_owner(id)?; } - let target = out.binding_slot()?; - *target = Some(self.retired.remove(index).map_err(|_| EIO)?); + let retired = self.retired.remove(index).map_err(|_| EIO)?; + if linked { + keep_graph(&mut self.kept_graphs, retired); + } else { + *out.binding_slot()? = Some(retired); + } } Ok(()) } @@ -181,3 +193,18 @@ impl Registry { Ok(()) } } + +/// Keeps `graph` until the firmware stops; if even that cannot be recorded, it is leaked +/// rather than freed. +fn keep_graph( + kept: &mut KVec, + graph: crate::g17::queue::render::RetiredBinding, +) { + if kept.reserve(1, GFP_KERNEL).is_ok() { + if let Err(error) = kept.push_within_capacity(graph) { + core::mem::forget(error.0); + } + } else { + core::mem::forget(graph); + } +} diff --git a/drivers/gpu/drm/asahi/g17/runtime/teardown.rs b/drivers/gpu/drm/asahi/g17/runtime/teardown.rs index 16bd5828265a4..0222ae4c478c8 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/teardown.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/teardown.rs @@ -100,6 +100,8 @@ impl ReclaimBatch { struct Service<'a> { render: &'a mut super::render::RenderState, + admission: &'a crate::g17::queue::admission::Pool, + objects: &'a Arc, compute: &'a [Option], buffers: &'a mut BufferIds, init: &'a InitData, @@ -137,8 +139,15 @@ impl Host for Service<'_> { self.primary.notify(MSG_CONTROL_NOTIFY) } fn detach(&mut self, context: &Arc) -> Result { + let linked = context.retain_linked_work(self.objects); + let (_, _, slots) = self.render.exit_scope(context); + crate::g17::trace::teardown(5, u64::from(context.id()), slots, u64::from(linked)); self.render .detach_context(context, self.buffers, self.reclaim)?; + // The firmware consumed this context's scheduler release after its kill (if any) + // was acknowledged and its pairs went idle: it no longer names these pairs, so + // their kill hold ends here rather than at the next processor stop. + self.admission.release_killed_slots(slots); // detach_context preflights this slot together with every graph slot. // Retain it before Pending removes its reference under the device lock. self.reclaim.context(context) @@ -173,6 +182,7 @@ impl Firmware { self.queues.render.observe_teardown_releases(&self.init)?; let recovery = self.recovery.pending() || self.init.recovery_state()? != 0; let Registry { + admission, render, compute, buffers, @@ -181,6 +191,8 @@ impl Firmware { } = &mut self.queues; let mut service = Service { render, + admission, + objects: &self.pool, compute, buffers, init: &self.init, diff --git a/drivers/gpu/drm/asahi/g17/trace.c b/drivers/gpu/drm/asahi/g17/trace.c index 23666be389b96..9e93291f5114e 100644 --- a/drivers/gpu/drm/asahi/g17/trace.c +++ b/drivers/gpu/drm/asahi/g17/trace.c @@ -54,3 +54,9 @@ void asahi_g17_trace_event(u8 phase) if (trace_asahi_g17_event_enabled()) trace_asahi_g17_event(phase, __arch_counter_get_cntvct()); } + +void asahi_g17_trace_teardown(u8 kind, u64 a, u64 b, u64 c) +{ + if (trace_asahi_g17_teardown_enabled()) + trace_asahi_g17_teardown(kind, a, b, c); +} diff --git a/drivers/gpu/drm/asahi/g17/trace.h b/drivers/gpu/drm/asahi/g17/trace.h index 7e5a50e774b59..d1529a2775fcc 100644 --- a/drivers/gpu/drm/asahi/g17/trace.h +++ b/drivers/gpu/drm/asahi/g17/trace.h @@ -125,6 +125,25 @@ TRACE_EVENT(asahi_g17_message, __entry->counter) ); +/* Failed-context teardown and render slot reuse (kind: see g17/trace.rs teardown()). */ +TRACE_EVENT(asahi_g17_teardown, + TP_PROTO(u8 kind, u64 a, u64 b, u64 c), + TP_ARGS(kind, a, b, c), + TP_STRUCT__entry( + __field(u8, kind) + __field(u64, a) + __field(u64, b) + __field(u64, c) + ), + TP_fast_assign( + __entry->kind = kind; + __entry->a = a; + __entry->b = b; + __entry->c = c; + ), + TP_printk("kind=%u a=%#llx b=%#llx c=%#llx", __entry->kind, __entry->a, __entry->b, __entry->c) +); + TRACE_EVENT(asahi_g17_event, TP_PROTO(u8 phase, u64 counter), TP_ARGS(phase, counter), diff --git a/drivers/gpu/drm/asahi/g17/trace.rs b/drivers/gpu/drm/asahi/g17/trace.rs index b76b46425d2ce..f498de82e87c6 100644 --- a/drivers/gpu/drm/asahi/g17/trace.rs +++ b/drivers/gpu/drm/asahi/g17/trace.rs @@ -16,6 +16,7 @@ unsafe extern "C" { fn asahi_g17_trace_doorbell_enabled() -> bool; fn asahi_g17_trace_doorbell(engine: u8, power: u32); fn asahi_g17_trace_event(phase: u8); + fn asahi_g17_trace_teardown(kind: u8, a: u64, b: u64, c: u64); } pub(super) const RENDER: u8 = 0; @@ -74,6 +75,15 @@ pub(super) fn message(kind: u8, value: u64) { unsafe { asahi_g17_trace_message(kind, value) }; } +/// Failed-context teardown: 1 compute fault (qid, stamp), 2 unstamped render retire (slot, +/// ordinal), 3 kill published (cookie, context, held slots | 1 if it skipped the wait for other +/// contexts' compute under memory pressure; slot 0 is never held), 4 kill acknowledged (cookie, qid), +/// 5 detach (context, slots, linked), 6 render slot to a new owner (slot, old, new | killed << 63). +pub(crate) fn teardown(kind: u8, a: u64, b: u64, c: u64) { + // SAFETY: Plain values only. + unsafe { asahi_g17_trace_teardown(kind, a, b, c) }; +} + /// Event worker pass boundaries: 0 begin, 1 fences signalled, 2 end. pub(super) fn event(phase: u8) { // SAFETY: Plain values only. diff --git a/drivers/gpu/drm/asahi/g17/trace_api.h b/drivers/gpu/drm/asahi/g17/trace_api.h index d09f529a163f0..0049aa0079634 100644 --- a/drivers/gpu/drm/asahi/g17/trace_api.h +++ b/drivers/gpu/drm/asahi/g17/trace_api.h @@ -20,5 +20,6 @@ bool asahi_g17_trace_doorbell_enabled(void); void asahi_g17_trace_doorbell(u8 engine, u32 power); void asahi_g17_trace_message(u8 kind, u64 value); void asahi_g17_trace_event(u8 phase); +void asahi_g17_trace_teardown(u8 kind, u64 a, u64 b, u64 c); #endif /* _ASAHI_G17_TRACE_API_H */ From a1eb22ddf5054cb9c738dd15aa63928a9a4b5b53 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Tue, 27 Oct 2026 17:16:26 +1100 Subject: [PATCH 238/388] drm/asahi: retire a G17 render pass on its fragment notification An event pass considered a render pair only once both of its queues had posted a stamp notification, the tiling queue's and the fragment queue's. The tiling notification adds nothing a retirement needs: the fragment kick starts only after its fragment barrier saw the tiling stamp, and the observation still requires both stamps in memory to cover the pass. Recovery attribution reads the stamps, and parameter-buffer growth arrives as its own record. Select a pair, and decide that it needs a rescan, on the fragment queue's notification alone, through one helper with KUnit coverage. A pass whose only news is a tiling stamp no longer borrows the pair for an observation that cannot retire. Link: https://github.com/aurora-silicon/linux/commit/de0b22a8370e0dec3ffb080c173aea4aa4fc331b Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/asahi/g17/queue/render.rs | 48 +++++++++++++++++-- drivers/gpu/drm/asahi/g17/runtime/render.rs | 8 ++-- .../drm/asahi/g17/runtime/render/observe.rs | 4 +- 3 files changed, 48 insertions(+), 12 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/queue/render.rs b/drivers/gpu/drm/asahi/g17/queue/render.rs index bcf10f97fa2d7..9c0b1e4e86f63 100644 --- a/drivers/gpu/drm/asahi/g17/queue/render.rs +++ b/drivers/gpu/drm/asahi/g17/queue/render.rs @@ -1034,11 +1034,7 @@ impl Pair { ] } fn selected(&self, masks: Option<[u64; 2]>) -> bool { - masks.is_none_or(|masks| { - self.qids() - .into_iter() - .all(|qid| masks[usize::from(qid) / 64] & (1u64 << (qid % 64)) != 0) - }) + masks.is_none_or(|masks| completion_noted(self.qids(), masks)) } /// The oldest published group still running, for cross-VM timeout attribution /// (crate::g17::timeout). A quarantined pair's work holds the GPU until the firmware drains @@ -2246,3 +2242,45 @@ impl Pair { Ok(()) } } + +/// Whether `masks`, the stamp notifications collected for a pair whose tiling and fragment +/// queues are `qids`, can mean a finished pass: the fragment queue's bit. A fragment kick starts +/// only after its fragment barrier saw the tiling stamp, and retirement still checks both stamps +/// in memory, so the tiling notification is not needed to retire. +pub(crate) fn completion_noted(qids: [u8; 2], masks: [u64; 2]) -> bool { + let fragment = usize::from(qids[1]); + masks[fragment / 64] & (1u64 << (fragment % 64)) != 0 +} + +#[kunit_tests(asahi_g17_render_completion_noted)] +mod completion_noted_tests { + use super::completion_noted; + + fn bit(qid: u8) -> [u64; 2] { + let mut masks = [0; 2]; + masks[usize::from(qid) / 64] |= 1u64 << (qid % 64); + masks + } + + #[test] + fn fragment_notification_alone_selects() { + assert!(completion_noted([3, 4], bit(4))); + assert!(completion_noted([70, 100], bit(100))); + } + + #[test] + fn tiling_notification_alone_does_not_select() { + assert!(!completion_noted([3, 4], bit(3))); + assert!(!completion_noted([70, 100], bit(70))); + assert!(!completion_noted([3, 4], [0; 2])); + } + + #[test] + fn other_queues_do_not_select() { + assert!(!completion_noted([3, 4], bit(5))); + assert!(!completion_noted([3, 68], bit(4))); + let mut both = bit(3); + both[1] |= bit(68)[1]; + assert!(completion_noted([3, 68], both)); + } +} diff --git a/drivers/gpu/drm/asahi/g17/runtime/render.rs b/drivers/gpu/drm/asahi/g17/runtime/render.rs index 22cd1f18876ca..b62f677b014f0 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render.rs @@ -1057,10 +1057,10 @@ impl Registry { |pair| pair.published_in_flight(), ); active - && entry.reservation.ids.into_iter().all(|id| { - let qid = usize::from(id.qid()); - entry.event_masks[qid / 64] & (1u64 << (qid % 64)) != 0 - }) + && crate::g17::queue::render::completion_noted( + entry.reservation.ids.map(kick::Id::qid), + entry.event_masks, + ) }) } pub(in crate::g17) fn defer_render_timeout( diff --git a/drivers/gpu/drm/asahi/g17/runtime/render/observe.rs b/drivers/gpu/drm/asahi/g17/runtime/render/observe.rs index 52bdd964ca4ae..a0a92c2f21b89 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render/observe.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render/observe.rs @@ -26,9 +26,7 @@ impl crate::g17::Firmware { }; let qids = pair.qids(); if !pair.published_in_flight() - || !qids - .into_iter() - .all(|qid| entry.event_masks[usize::from(qid) / 64] & (1u64 << (qid % 64)) != 0) + || !crate::g17::queue::render::completion_noted(qids, entry.event_masks) { continue; } From e105f47bdf1e430e187e04cc794c36ad34fef49c Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Tue, 27 Oct 2026 17:16:26 +1100 Subject: [PATCH 239/388] drm/asahi: set the G17 tiling first-kick word on a pair's first kick only The tiling descriptor has a word at the position of the fragment descriptor's first-kick word, which the fragment stage sets only on a queue pair's first kick. The tiling setup set it on every kick. With it set, the firmware also notifies the host of the tiling queue's completion on every pass, so each render cost two firmware notifications, two mailbox interrupts and two event passes, while retirement needs only the fragment queue's notification (previous commit). Name the word first_kick and set it for ordinal 0 only, as for the fragment stage, on the A18 Pro firmware (26A428). The M4 Pro firmware (25G83) keeps the old value on every kick until it is measured there. Measured on Neo in one boot through a diagnostic switch that toggled only this word (3 rounds each, medians, us per submit): GPU mailbox IRQs per submit (render.light d32) 2.18 -> 1.10 render.light tri queue d1 105.26 -> 104.12 render.light tri queue d8 30.98 -> 29.30 render.light tri queue d32 29.67 -> 27.80 render.light tri queue d64 29.67 -> 27.78 render.light tri procs d2 52.12 -> 49.73 render.light tri queues d2 52.16 -> 49.65 compute.light queue d1 103.39 -> 104.08 sync.signal queues d2 timeline / binary 22.79 / 22.37 -> 22.61 / 22.61 Link: https://github.com/aurora-silicon/linux/commit/5073eac15192ff8aec118ed2dc85a3d632a6f845 Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/asahi/g17/fw/render.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/fw/render.rs b/drivers/gpu/drm/asahi/g17/fw/render.rs index aa8693bf7140b..a6bc1704d1418 100644 --- a/drivers/gpu/drm/asahi/g17/fw/render.rs +++ b/drivers/gpu/drm/asahi/g17/fw/render.rs @@ -1023,7 +1023,11 @@ pub(crate) struct TaDescriptor { /// QID of the tiling queue. pub(crate) qid: u32, pub(crate) unk_8be: u32, - pub(crate) unk_8c2: u32, + /// Non-zero on the first kick of a queue pair, like the fragment descriptor's word at the + /// same position. Set on every kick, the firmware also notifies the host of each pass's + /// tiling completion, which no retirement needs. The 25G83 firmware keeps it set on every + /// kick until this is measured there. + pub(crate) first_kick: u32, /// Completion value of the tiling kick (see /// [`KickTimestamp::completion_value`]). pub(crate) completion: u64, @@ -1073,7 +1077,6 @@ impl TaDescriptor { const ARRAY_OFFSET: u64 = core::mem::offset_of!(Self, registers) as u64; const UNK_876: u32 = u32::MAX; const UNK_892: u32 = 1; - const UNK_8C2: u32 = 1; const UNK_932: u8 = 0x44; const UNK_93C: u8 = 1; const UNK_93E_FIRST_KICK: [u8; 2] = [0xd0, 0x91]; @@ -1127,7 +1130,10 @@ impl TaDescriptor { self.aux_stamp_va = args.ta.aux_stamp_va; self.stamp_value_2 = stamp_value; self.qid = args.ta.qid.into(); - self.unk_8c2 = Self::UNK_8C2; + self.first_kick = match args.programs == RegisterPrograms::V25G83 { + true => 1, + false => u32::from(args.ordinal == 0), + }; self.current_id = resources.ta_ids.current.into(); self.ordinal = args.ordinal as u32; self.timestamp_start_va = resources.timestamp_va; From e4ee19363ec942580be56321b7fca5dee46eb0cc Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 17:17:47 +1100 Subject: [PATCH 240/388] drm/asahi: preserve raw G17 firmware reports for clock and fault qualification G17 discarded statistics payloads and unknown event words before returning ring slots. Keep the latest full statistics sample per role/type with host monotonic time and count in read-only debugfs, and add a static-key raw report tracepoint for complete trace/statistics/event timelines. Copy before publishing the consumer; retain the existing tag and transport quarantine rules. Debugfs owns the session and samples under the device mutex without waking firmware or reading gated core registers. No decoded achieved-clock/power ABI is available for these firmware builds. Do not relabel legacy pstate fields, requested OPPs or advertised maxima as achieved frequency. Preserve raw observations so their semantics can be qualified independently. KUnit covers full payload preservation, role/type separation and invalid-tag bounds. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/asahi/g17/channel.rs | 34 ++++++---- drivers/gpu/drm/asahi/g17/mod.rs | 9 ++- drivers/gpu/drm/asahi/g17/reports.rs | 97 +++++++++++++++++++++++++++ drivers/gpu/drm/asahi/g17/trace.c | 10 +++ drivers/gpu/drm/asahi/g17/trace.h | 21 ++++++ drivers/gpu/drm/asahi/g17/trace.rs | 16 +++++ drivers/gpu/drm/asahi/g17/trace_api.h | 3 + 7 files changed, 177 insertions(+), 13 deletions(-) create mode 100644 drivers/gpu/drm/asahi/g17/reports.rs diff --git a/drivers/gpu/drm/asahi/g17/channel.rs b/drivers/gpu/drm/asahi/g17/channel.rs index b06743abfc446..568647ebb444a 100644 --- a/drivers/gpu/drm/asahi/g17/channel.rs +++ b/drivers/gpu/drm/asahi/g17/channel.rs @@ -43,16 +43,21 @@ impl ReportRing { /// Host-side receive-ring state. A quarantined ring stays stopped until the /// device is destroyed; firmware restart does not clear it. -#[derive(Debug)] pub(crate) struct Rings { quarantined: [[bool; 2]; 2], + reports: super::reports::Reports, } impl Rings { - pub(crate) const fn new() -> Self { - Self { + pub(crate) fn new() -> Result { + Ok(Self { quarantined: [[false; 2]; 2], - } + reports: super::reports::Reports::new()?, + }) + } + + pub(super) fn write_reports(&self, f: &mut kernel::fmt::Formatter<'_>) -> kernel::fmt::Result { + self.reports.write(f) } pub(crate) fn drain_trace(&mut self, dev: &Device, init: &InitData, role: Role) { @@ -68,7 +73,7 @@ impl Rings { if *quarantined { return; } - if let Err(error) = Self::consume_report(init, role, ring) { + if let Err(error) = Self::consume_report(init, role, ring, &mut self.reports) { *quarantined = true; dev_err!( dev.as_ref(), @@ -80,7 +85,7 @@ impl Rings { } } - fn consume_report(init: &InitData, role: Role, ring: ReportRing) -> Result { + fn consume_report(init: &InitData, role: Role, ring: ReportRing, reports: &mut super::reports::Reports) -> Result { let (state, entries, capacity, allowed_types) = ring.geometry(); let consumer_word = init.status_word(role, state)?; let producer_word = init.status_word(role, state + RX_PRODUCER_OFFSET)?; @@ -94,14 +99,17 @@ impl Rings { } while consumer != producer { let offset = entries + consumer as usize * EVENT_RECORD_SIZE; - let tag = match ring { - // Copy the entire trace entry before returning its slot. - ReportRing::Trace => init.status_read_words::(role, offset)?[0], - ReportRing::Statistics => init.status_read_words::<1>(role, offset)?[0], - }; + // Copy all words before returning the slot. Interpretation is a + // separate firmware qualification step, never a scheduling input. + let words = init.status_read_words::(role, offset)?; + super::trace::report(role as u8, ring as u8, &words); + let tag = words[0]; if tag >= u32::BITS || allowed_types & (1 << tag) == 0 { return Err(EIO); } + if matches!(ring, ReportRing::Statistics) { + reports.record(role, words, super::now_ns()); + } consumer = (consumer + 1) & (capacity - 1); fence(Ordering::SeqCst); consumer_word.store(consumer, Ordering::Relaxed); @@ -197,7 +205,9 @@ impl Events { // Observe the producer before reading the record it publishes. fence(Ordering::Acquire); let offset = status::EVENT_RING as usize + consumer as usize * EVENT_RECORD_SIZE; - let record = EventRecord::from_words(init.status_read_words(role, offset)?); + let words = init.status_read_words(role, offset)?; + super::trace::report(role as u8, 2, &words); + let record = EventRecord::from_words(words); let event = record.decode_for(self.graph); fence(Ordering::SeqCst); consumer_word.store((consumer + 1) & (EVENT_SLOTS - 1), Ordering::Relaxed); diff --git a/drivers/gpu/drm/asahi/g17/mod.rs b/drivers/gpu/drm/asahi/g17/mod.rs index 78a924782a1a2..d7a42c5c82bdb 100644 --- a/drivers/gpu/drm/asahi/g17/mod.rs +++ b/drivers/gpu/drm/asahi/g17/mod.rs @@ -50,6 +50,7 @@ mod kick; mod object; mod port; pub(crate) mod power; +mod reports; mod preparation; mod qos; mod queue; @@ -1099,6 +1100,7 @@ pub(crate) struct Gpu { ids: gpu::SequenceIDs, shared: Arc, _capacity: Pin>>, + _reports: Pin>>, } impl Gpu { @@ -1206,6 +1208,10 @@ impl Gpu { directory.read_only_file(c"capacity", capacity::View(shared.clone())), GFP_KERNEL, )?; + let reports = KBox::pin_init( + directory.read_only_file(c"firmware_reports", reports::View(shared.clone())), + GFP_KERNEL, + )?; let gpu = Arc::new( Gpu { cfg, @@ -1215,6 +1221,7 @@ impl Gpu { ids: Default::default(), shared, _capacity: capacity, + _reports: reports, }, GFP_KERNEL, )?; @@ -1356,7 +1363,7 @@ impl Gpu { bridge, port: port::Port::new()?, seen_submissions: 0, - rings: channel::Rings::new(), + rings: channel::Rings::new()?, recovery: recovery::State::new(), effort: power::Effort::new(), hold: power::Hold::new(), diff --git a/drivers/gpu/drm/asahi/g17/reports.rs b/drivers/gpu/drm/asahi/g17/reports.rs new file mode 100644 index 0000000000000..7e1afef70cad2 --- /dev/null +++ b/drivers/gpu/drm/asahi/g17/reports.rs @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT + +//! Preserve the firmware's statistics without assigning unqualified units or +//! treating another firmware's pstate report as an achieved GPU frequency. +//! Writers and readers hold the device mutex; reads never power the GPU up. + +use super::{fw::channels::{EVENT_WORDS, STATISTICS_TYPES}, Role}; +use kernel::prelude::*; + +#[derive(Clone, Copy, Default)] +struct Sample { + count: u64, + host_ns: u64, + words: [u32; EVENT_WORDS], +} + +pub(super) struct Reports { + samples: KVec, +} + +impl Reports { + pub(super) fn new() -> Result { + // Allocate before firmware starts; receive and debugfs paths allocate nothing. + let mut samples = KVec::with_capacity(64, GFP_KERNEL)?; + for _ in 0..64 { + samples.push(Sample::default(), GFP_KERNEL)?; + } + Ok(Self { samples }) + } + + pub(super) fn record(&mut self, role: Role, words: [u32; EVENT_WORDS], host_ns: u64) { + let tag = words[0]; + if tag >= u32::BITS || STATISTICS_TYPES & (1 << tag) == 0 { + return; + } + let sample = &mut self.samples[role as usize * 32 + tag as usize]; + sample.count = sample.count.saturating_add(1); + sample.host_ns = host_ns; + sample.words = words; + } + + pub(super) fn write(&self, f: &mut kernel::fmt::Formatter<'_>) -> kernel::fmt::Result { + writeln!(f, "format=1 units=raw host_time=monotonic_ns achieved_frequency=unqualified")?; + for (index, sample) in self.samples.iter().enumerate().filter(|(_, s)| s.count != 0) { + writeln!(f, "role={} tag={:#x} count={} host_ns={} words={:08x?}", + index / 32, index % 32, sample.count, sample.host_ns, sample.words)?; + } + Ok(()) + } +} + +/// The view owns the session, not a pointer into a detachable Firmware. +pub(super) struct View(pub(super) kernel::sync::Arc); + +impl kernel::debugfs::Writer for View { + fn write(&self, f: &mut kernel::fmt::Formatter<'_>) -> kernel::fmt::Result { + let state = self.0.state.lock(); + let Some(firmware) = state.as_deref() else { return writeln!(f, "online=0"); }; + writeln!(f, "online=1 graph={:?} sampled_ns={}", self.0.variant.graph, super::now_ns())?; + firmware.rings.write_reports(f) + } +} + +#[kernel::macros::kunit_tests(asahi_g17_reports)] +mod tests { + use super::*; + + #[test] + fn retains_all_words_and_separates_role_and_tag() { + let mut reports = Reports::new().unwrap(); + let mut words = [0; EVENT_WORDS]; + words[0] = 16; + words[17] = 0xdeadbeef; + reports.record(Role::Primary, words, 100); + reports.record(Role::Primary, words, 101); + reports.record(Role::Secondary, words, 102); + assert_eq!(reports.samples[16].count, 2); + assert_eq!(reports.samples[16].host_ns, 101); + assert_eq!(reports.samples[16].words, words); + assert_eq!(reports.samples[48].count, 1); + words[0] = 17; + reports.record(Role::Primary, words, 103); + assert_eq!(reports.samples[17].count, 1); + assert_eq!(reports.samples[16].count, 2); + } + + #[test] + fn rejects_invalid_tags_before_indexing() { + let mut reports = Reports::new().unwrap(); + for tag in [0, 15, 29, 31, 32, u32::MAX] { + let mut words = [0; EVENT_WORDS]; + words[0] = tag; + reports.record(Role::Primary, words, 1); + } + assert!(reports.samples.iter().all(|s| s.count == 0)); + } +} diff --git a/drivers/gpu/drm/asahi/g17/trace.c b/drivers/gpu/drm/asahi/g17/trace.c index 9e93291f5114e..59f0094d129e9 100644 --- a/drivers/gpu/drm/asahi/g17/trace.c +++ b/drivers/gpu/drm/asahi/g17/trace.c @@ -60,3 +60,13 @@ void asahi_g17_trace_teardown(u8 kind, u64 a, u64 b, u64 c) if (trace_asahi_g17_teardown_enabled()) trace_asahi_g17_teardown(kind, a, b, c); } + +bool asahi_g17_trace_report_enabled(void) +{ + return trace_asahi_g17_report_enabled(); +} + +void asahi_g17_trace_report(u8 role, u8 ring, const u32 *words) +{ + trace_asahi_g17_report(role, ring, words, __arch_counter_get_cntvct()); +} diff --git a/drivers/gpu/drm/asahi/g17/trace.h b/drivers/gpu/drm/asahi/g17/trace.h index d1529a2775fcc..36e1b445f1b77 100644 --- a/drivers/gpu/drm/asahi/g17/trace.h +++ b/drivers/gpu/drm/asahi/g17/trace.h @@ -158,6 +158,27 @@ TRACE_EVENT(asahi_g17_event, TP_printk("phase=%u counter=%llu", __entry->phase, __entry->counter) ); +/* Raw 18-word records: trace=0, statistics=1, event=2. No unit inference. */ +TRACE_EVENT(asahi_g17_report, + TP_PROTO(u8 role, u8 ring, const u32 *words, u64 counter), + TP_ARGS(role, ring, words, counter), + TP_STRUCT__entry( + __field(u8, role) + __field(u8, ring) + __array(u32, words, 18) + __field(u64, counter) + ), + TP_fast_assign( + __entry->role = role; + __entry->ring = ring; + memcpy(__entry->words, words, sizeof(__entry->words)); + __entry->counter = counter; + ), + TP_printk("role=%u ring=%u words=%s counter=%llu", __entry->role, + __entry->ring, __print_array(__entry->words, 18, sizeof(u32)), + __entry->counter) +); + #endif #undef TRACE_INCLUDE_PATH diff --git a/drivers/gpu/drm/asahi/g17/trace.rs b/drivers/gpu/drm/asahi/g17/trace.rs index f498de82e87c6..cd38c95193585 100644 --- a/drivers/gpu/drm/asahi/g17/trace.rs +++ b/drivers/gpu/drm/asahi/g17/trace.rs @@ -89,3 +89,19 @@ pub(super) fn event(phase: u8) { // SAFETY: Plain values only. unsafe { asahi_g17_trace_event(phase) }; } + +unsafe extern "C" { + fn asahi_g17_trace_report_enabled() -> bool; + fn asahi_g17_trace_report(role: u8, ring: u8, words: *const u32); +} + +/// Copies a complete firmware record while the host still owns its slot. +/// Static-key gated; no MMIO reads or firmware requests are issued. +pub(super) fn report(role: u8, ring: u8, words: &[u32; super::fw::channels::EVENT_WORDS]) { + // SAFETY: Pure static-key query. + if !unsafe { asahi_g17_trace_report_enabled() } { + return; + } + // SAFETY: C synchronously copies the complete initialized 18-word array. + unsafe { asahi_g17_trace_report(role, ring, words.as_ptr()) }; +} diff --git a/drivers/gpu/drm/asahi/g17/trace_api.h b/drivers/gpu/drm/asahi/g17/trace_api.h index 0049aa0079634..be5eff4c8161b 100644 --- a/drivers/gpu/drm/asahi/g17/trace_api.h +++ b/drivers/gpu/drm/asahi/g17/trace_api.h @@ -22,4 +22,7 @@ void asahi_g17_trace_message(u8 kind, u64 value); void asahi_g17_trace_event(u8 phase); void asahi_g17_trace_teardown(u8 kind, u64 a, u64 b, u64 c); +bool asahi_g17_trace_report_enabled(void); +void asahi_g17_trace_report(u8 role, u8 ring, const u32 *words); + #endif /* _ASAHI_G17_TRACE_API_H */ From 22c3c57a7c1597175e9424782b10595e40d0e382 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 17:18:33 +1100 Subject: [PATCH 241/388] drm/asahi: snapshot the blamed render before acknowledging GPU recovery Retain bounded first-fault evidence from the already mapped render graph: queue identities, completion stamps, free-list balance and the oldest four commands' ring cursors, pass timestamps and scheduler identity. Sample while firmware is halted at state 1 before publishing recovery targets and before acknowledging the restart. Explicitly report a borrowed/unavailable pair. The existing recovery reason and blame alone do not distinguish a stalled tiler from fragment execution, lost completion visibility or work that never started. This observation makes that distinction possible without new MMIO addresses, invented reason codes, timeout changes or weaker release rules. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/asahi/g17/queue/render.rs | 13 +++++++++++++ drivers/gpu/drm/asahi/g17/recovery.rs | 5 +++++ drivers/gpu/drm/asahi/g17/runtime/events.rs | 3 +++ .../gpu/drm/asahi/g17/runtime/render/recovery.rs | 14 ++++++++++++++ 4 files changed, 35 insertions(+) diff --git a/drivers/gpu/drm/asahi/g17/queue/render.rs b/drivers/gpu/drm/asahi/g17/queue/render.rs index 9c0b1e4e86f63..921f6eb3fb6eb 100644 --- a/drivers/gpu/drm/asahi/g17/queue/render.rs +++ b/drivers/gpu/drm/asahi/g17/queue/render.rs @@ -1033,6 +1033,19 @@ impl Pair { }, ] } + /// Bounded first-fault evidence while the recovery owner has firmware + /// halted, before it acknowledges state 1. Never changes retirement. + pub(crate) fn report_recovery(&self, stamps: Result<[u32; 2]>) { + pr_warn!("G17: recovery render slot={} qids={:?} active={} prepared={} quarantined={} stamps={:?} freelist={:?}\n", + self.slot, self.qids(), self.active.len(), self.prepared.is_some(), + self.quarantined, stamps, self.free_list.counters()); + for active in self.active.iter().take(4) { + pr_warn!("G17: recovery render ordinal={} published={} ctx={} seq={} snapshot={:?}\n", + active.ticket.ordinal, active.outer_published, active.packet.context.id(), + active.packet.order.sequence, observation::snapshot(&self.memory, active.ticket.ordinal)); + } + } + fn selected(&self, masks: Option<[u64; 2]>) -> bool { masks.is_none_or(|masks| completion_noted(self.qids(), masks)) } diff --git a/drivers/gpu/drm/asahi/g17/recovery.rs b/drivers/gpu/drm/asahi/g17/recovery.rs index 8bffec476c9ac..7787485fa713a 100644 --- a/drivers/gpu/drm/asahi/g17/recovery.rs +++ b/drivers/gpu/drm/asahi/g17/recovery.rs @@ -103,6 +103,8 @@ pub(super) trait Queues { /// partly executed, so it must never report success; see /// `Completion::note_interrupted`. fn note_interrupted(&mut self, qid: u8, stamp: u32) -> Interrupted; + /// Optional diagnostics only; absence cannot alter attribution or release. + fn report_blamed_render(&self, _qid: u8) {} fn settle_blamed_render(&mut self, qid: u8, error: Error); fn settle_cancelled_renders(&mut self, qids: u128); fn rescan_compute(&mut self) -> Result; @@ -245,6 +247,9 @@ pub(super) fn service( } let mut sources = host.fault_sources()?; sources.report(request.generation, super::queue::compute::retirement::stampless_total()); + if let Some(qid) = request.blamed { + host.report_blamed_render(qid); + } let target_error = host.publish_recovery_targets(); host.transition(1, 2)?; let mut recovered = false; diff --git a/drivers/gpu/drm/asahi/g17/runtime/events.rs b/drivers/gpu/drm/asahi/g17/runtime/events.rs index b410a90568d04..26ba3f0219697 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/events.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/events.rs @@ -228,6 +228,9 @@ impl recovery::Queues for Service<'_> { queue.note_interrupted(stamp) }) } + fn report_blamed_render(&self, qid: u8) { + self.firmware.queues.report_render_recovery(qid, &self.firmware.port); + } fn settle_blamed_render(&mut self, qid: u8, error: Error) { let result = self .firmware diff --git a/drivers/gpu/drm/asahi/g17/runtime/render/recovery.rs b/drivers/gpu/drm/asahi/g17/runtime/render/recovery.rs index 3a5b042fff683..20f537ab69598 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render/recovery.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render/recovery.rs @@ -6,6 +6,20 @@ use crate::g17::runtime::Registry; use kernel::prelude::*; impl Registry { + pub(in crate::g17::runtime) fn report_render_recovery(&self, qid: u8, port: &crate::g17::port::Port) { + for entry in self.render.entries.iter().flatten().filter(|entry| { + entry.reservation.ids.iter().any(|id| id.qid() == qid) + }) { + if let Some(pair) = &entry.pair { + let qids = pair.qids(); + pair.report_recovery((|| Ok([port.stamp(qids[0])?, port.stamp(qids[1])?]))()); + } else { + pr_warn!("G17: recovery render queue={} unavailable borrowed={} constructing={}\n", + qid, entry.borrowed_active, entry.constructing); + } + } + } + pub(in crate::g17::runtime) fn render_pass_started(&self, qid: u8, stamp: u64) -> Option { self.render.entries.iter().flatten() .filter_map(|entry| entry.pair.as_ref()) From 99725fd07818ba58a4336e34d3428a1f920a21ab Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 17:18:33 +1100 Subject: [PATCH 242/388] soc: apple: clpc: expose the graphics input holding the memory floor Expose the last sampled graphics PMGR status, whether it asserted demand, and read-error count alongside existing CPU requests and per-rail votes. A greeter's held floor cannot distinguish GPU power demand from CPU activity or failed status reads. Keep the existing fail-open performance policy and 32 ms release unchanged; status reads never add a vote or wake graphics. The debugfs values are independently sampled diagnostics, not one atomic hardware snapshot. Qualify long-idle/no-client release before changing the power-domain input or memory-floor policy. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/soc/apple/clpc.c | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/drivers/soc/apple/clpc.c b/drivers/soc/apple/clpc.c index d51501db045cb..1acfcc0b598dd 100644 --- a/drivers/soc/apple/clpc.c +++ b/drivers/soc/apple/clpc.c @@ -101,6 +101,9 @@ struct apple_clpc { struct dentry *debugfs; struct regmap *gpu_pmgr; u32 gpu_offset; + u32 gpu_status; + bool gpu_active; + unsigned long gpu_read_errors; }; /* cpufreq policies can appear after probe; pick up new ones as they do. */ @@ -254,8 +257,14 @@ static void apple_clpc_sample(struct apple_clpc *clpc) /* Keep the qualified floor while graphics is powered. A failed * read must not silently starve a running graphics consumer. */ - if (regmap_read(clpc->gpu_pmgr, clpc->gpu_offset, &status) || - (status & GENMASK(7, 4)) == GENMASK(7, 4)) + if (regmap_read(clpc->gpu_pmgr, clpc->gpu_offset, &status)) { + clpc->gpu_read_errors++; + clpc->gpu_active = true; + } else { + clpc->gpu_status = status; + clpc->gpu_active = (status & GENMASK(7, 4)) == GENMASK(7, 4); + } + if (clpc->gpu_active) for (rail = 0; rail < APPLE_CLPC_RAILS; rail++) demand[rail] = clpc->hw->map[rail][APPLE_CLPC_LEVELS - 1]; } @@ -299,6 +308,9 @@ static int apple_clpc_status_show(struct seq_file *s, void *unused) clpc->hw->rail[rail]), clpc->hw->map[rail][0], clpc->hw->map[rail][1], clpc->hw->map[rail][2], clpc->hw->map[rail][3]); + seq_printf(s, "gpu: present %u active %u status %#x read_errors %lu\n", + !!clpc->gpu_pmgr, READ_ONCE(clpc->gpu_active), + READ_ONCE(clpc->gpu_status), READ_ONCE(clpc->gpu_read_errors)); seq_printf(s, "samples: %lu update errors: %lu\n", clpc->samples, clpc->update_errors); for (i = 0; i < clpc->nr_clusters; i++) From d6d0c93e679fdb31a290b6d3ffa1fbd334411836 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Tue, 27 Oct 2026 02:19:20 -0400 Subject: [PATCH 243/388] drm/asahi: take a G17 render-only kill's cookie before its admission hold A render-only kill took its admission hold on the context's render pairs and only then its cookie. If the cookie counter was exhausted, the EOVERFLOW returned with the hold still installed, so those pairs stayed held for a kill that was never published. Take the cookie first, as a failed context's kill already does. --- drivers/gpu/drm/asahi/g17/runtime/compute_exit.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/asahi/g17/runtime/compute_exit.rs b/drivers/gpu/drm/asahi/g17/runtime/compute_exit.rs index c8e6de387d91c..cd7f2999d0312 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/compute_exit.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/compute_exit.rs @@ -374,6 +374,9 @@ impl Firmware { refuse_render_kill(&mut self.queues.teardown, index, RENDER_KILL_OWNER); return Ok(()); }; + // As for a failed context's kill: the cookie first, so that running out of cookies + // leaves no admission hold behind. + let cookie = exit::next_cookie()?; if !self .queues .admission @@ -382,7 +385,6 @@ impl Firmware { refuse_render_kill(&mut self.queues.teardown, index, RENDER_KILL_HOLD); return Ok(()); } - let cookie = exit::next_cookie()?; let record = ControlRecord::ContextKill(ContextKill::new(context.scheduler_va(), cookie)); if let Err(error) = Rings::publish_control(&self.init, &record) { self.queues.admission.release_exit_kill_hold(hold); From 4e96b0a44f45bde2f870f812a4057c0324923d9c Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Tue, 27 Oct 2026 02:19:20 -0400 Subject: [PATCH 244/388] drm/asahi: keep a G17 kept object's list reservation with the object retain_until_stop reserved a list entry for an object kept until firmware stop, and its drop took that entry in keep_object. When the reservation failed, keep was still set, so the drop decremented a reservation that belonged to another kept object and could take its entry. reserve_kept now reports whether it reserved, and the kept object carries that. An object kept without a reservation is leaked at drop, as the failed reservation already documented, and leaves the other objects' reservations alone. --- drivers/gpu/drm/asahi/g17/object.rs | 33 +++++++++++++++++++---------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/object.rs b/drivers/gpu/drm/asahi/g17/object.rs index 7cc13dbba705e..87fdb0e8a62ad 100644 --- a/drivers/gpu/drm/asahi/g17/object.rs +++ b/drivers/gpu/drm/asahi/g17/object.rs @@ -499,7 +499,9 @@ struct CacheLease { pub(crate) struct PooledObject { object: core::mem::ManuallyDrop, lease: Option, - keep: Option>, + /// The pool keeping this object until firmware stop, and whether `reserve_kept` took its + /// list entry. + keep: Option<(kernel::sync::Arc, bool)>, } impl core::ops::Deref for PooledObject { @@ -520,8 +522,8 @@ impl PooledObject { /// entry, so the drop, wherever it happens, does not allocate. pub(crate) fn retain_until_stop(&mut self, pool: &kernel::sync::Arc) { if self.keep.is_none() { - pool.reserve_kept(); - self.keep = Some(pool.clone()); + let reserved = pool.reserve_kept(); + self.keep = Some((pool.clone(), reserved)); } } } @@ -531,8 +533,8 @@ impl Drop for PooledObject { // SAFETY: This destructor is the only operation that moves `object` // out; ManuallyDrop prevents a second implicit drop of the field. let object = unsafe { core::mem::ManuallyDrop::take(&mut self.object) }; - if let Some(pool) = self.keep.take() { - pool.keep_object(object, self.lease.take()); + if let Some((pool, reserved)) = self.keep.take() { + pool.keep_object(object, self.lease.take(), reserved); return; } match self.lease.take() { @@ -687,22 +689,25 @@ impl Pool { core::mem::forget(object); } - /// Room for one more kept object, taken by the matching `keep_object`. A failed - /// allocation is reported by that drop, which then leaks the object instead. - fn reserve_kept(&self) { + /// Room for one more kept object, taken by the matching `keep_object`. Returns whether + /// it was reserved; after a failed allocation that drop leaks the object instead. + fn reserve_kept(&self) -> bool { let mut inner = self.inner.lock(); let pending = inner.kept_reserved + 1; if inner.kept.capacity() - inner.kept.len() < pending && inner.kept.reserve(pending, GFP_KERNEL).is_err() { - return; + return false; } inner.kept_reserved = pending; + true } - fn keep_object(&self, object: KernelObject, lease: Option) { + fn keep_object(&self, object: KernelObject, lease: Option, reserved: bool) { let mut inner = self.inner.lock(); - inner.kept_reserved = inner.kept_reserved.saturating_sub(1); + if reserved { + inner.kept_reserved = inner.kept_reserved.saturating_sub(1); + } if let Some(lease) = &lease { // The object leaves the reuse cache and its capacity. inner.reserved[lease.family as usize] -= 1; @@ -717,6 +722,12 @@ impl Pool { if inner.kept.len() == KEPT_WARN { pr_warn!("G17 keeps {} firmware-linked objects until the GPU firmware restarts\n", KEPT_WARN); } + if !reserved { + // Another kept object's reservation must not be used: leak this one instead. + pr_err!("G17 could not record a firmware-linked object; leaking it\n"); + core::mem::forget(object); + return; + } // retain_until_stop reserved the entry; this never allocates. match inner.kept.push_within_capacity(object) { Ok(()) => inner.kept_bytes += size, From 34cf6456523e31a08a1f9c4ecb1184533bc9dfbb Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Tue, 27 Oct 2026 02:19:20 -0400 Subject: [PATCH 245/388] drm/asahi: count kept G17 render graphs and fix two stale comments Render graphs of a detached context that still links work are kept until the firmware stops, like firmware-linked work objects, but had no counter beside firmware_linked_objects. The capacity view now reports them as firmware_linked_render_graphs, with the number leaked because the list could not grow. Two comments described the code before recent changes: render observation needs only the fragment queue's notification now, and a kill hold on render pairs ends when the killed context's scheduler release is consumed, not only when the processors stop. --- drivers/gpu/drm/asahi/g17/queue/admission.rs | 3 ++- drivers/gpu/drm/asahi/g17/runtime/render.rs | 2 ++ drivers/gpu/drm/asahi/g17/runtime/render/observe.rs | 4 ++-- drivers/gpu/drm/asahi/g17/runtime/render/teardown.rs | 5 +++++ 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/queue/admission.rs b/drivers/gpu/drm/asahi/g17/queue/admission.rs index 5c657444be01f..ed73323e34e6f 100644 --- a/drivers/gpu/drm/asahi/g17/queue/admission.rs +++ b/drivers/gpu/drm/asahi/g17/queue/admission.rs @@ -940,7 +940,8 @@ impl State { } impl Pool { /// Hold the departed owner's idle pairs before publishing its exit record. - /// Successful publication retains these bits until both processors stop. + /// Successful publication retains these bits until the owner's scheduler release is + /// consumed (`release_killed_slots` at detach), or until both processors stop. pub(crate) fn hold_render_slots_for_exit_kill(&self, owner: u64, slots: u64, executed: u64) -> bool { self.state.lock().hold_for_exit_kill(owner, slots, executed) } diff --git a/drivers/gpu/drm/asahi/g17/runtime/render.rs b/drivers/gpu/drm/asahi/g17/runtime/render.rs index b62f677b014f0..4e7eaa2d8d246 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render.rs @@ -1145,6 +1145,8 @@ impl Registry { impl RenderState { pub(super) fn write_capacity(&self, f: &mut kernel::fmt::Formatter<'_>) -> kernel::fmt::Result { writeln!(f, "retired_render_bindings used={} total={}", self.retired.len(), self.retired.capacity())?; + writeln!(f, "firmware_linked_render_graphs kept={} leaked={}", self.kept_graphs.len(), + teardown::LEAKED_GRAPHS.load(core::sync::atomic::Ordering::Relaxed))?; self.pools.write_capacity(f)?; for (slot, entry) in self.entries.iter().enumerate() { if let Some(context) = entry.as_ref().and_then(|entry| entry.context.as_ref()) { diff --git a/drivers/gpu/drm/asahi/g17/runtime/render/observe.rs b/drivers/gpu/drm/asahi/g17/runtime/render/observe.rs index b499ae5132c18..51682b43728c0 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render/observe.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render/observe.rs @@ -8,8 +8,8 @@ use core::sync::atomic::Ordering; use kernel::{prelude::*, sync::Arc}; impl crate::g17::Firmware { - /// The first slot from `start` whose pair has news to observe: published work in flight, both - /// queues' notifications seen, and an observation the pair accepts. + /// The first slot from `start` whose pair has news to observe: published work in flight, the + /// fragment queue's notification seen (`completion_noted`), and an observation the pair accepts. fn next_render_observation(&self, start: usize) -> Result> { if self.recovery.pending() { return Ok(None); diff --git a/drivers/gpu/drm/asahi/g17/runtime/render/teardown.rs b/drivers/gpu/drm/asahi/g17/runtime/render/teardown.rs index 18063f0cd6a14..8f3ee6f3b15bd 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render/teardown.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render/teardown.rs @@ -196,15 +196,20 @@ impl Registry { /// Keeps `graph` until the firmware stops; if even that cannot be recorded, it is leaked /// rather than freed. +/// Graphs that could not be recorded in `kept_graphs` and were leaked instead, for debugfs. +pub(super) static LEAKED_GRAPHS: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0); + fn keep_graph( kept: &mut KVec, graph: crate::g17::queue::render::RetiredBinding, ) { if kept.reserve(1, GFP_KERNEL).is_ok() { if let Err(error) = kept.push_within_capacity(graph) { + LEAKED_GRAPHS.fetch_add(1, core::sync::atomic::Ordering::Relaxed); core::mem::forget(error.0); } } else { + LEAKED_GRAPHS.fetch_add(1, core::sync::atomic::Ordering::Relaxed); core::mem::forget(graph); } } From ce7122191c726e180ca7e5829aad0084ea97db3a Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 17:20:54 +1100 Subject: [PATCH 246/388] docs: record g4 runtime applicability and firmware telemetry gates Pin the public #496/#504/#505 ports and distinguish shared retention fixes from firmware-qualified fault witnesses and descriptor changes. Document raw observability and the first-fault/idle-floor evidence needed before claiming achieved-clock, CTS or remaining performance parity. Assisted-by: LLM Signed-off-by: Ryan Murray --- GAPS.md | 42 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/GAPS.md b/GAPS.md index b958e0225f815..7944948712418 100644 --- a/GAPS.md +++ b/GAPS.md @@ -12,8 +12,9 @@ T6040 data, honors the existing DT idle-delay policy, and closes a compute release-witness gap across recovery. GPU voltages, firmware features and the submission algorithm remain unchanged from the reconstructed base. The major T6040 calibration and image-admission changes requested by this audit are -already in the baseline. Software parity does not establish benchmark or CTS -parity. There are no J616s results for this series yet. +already in the baseline. Software parity does not establish all-workload benchmark or CTS +parity. The operator has selected g3 as the measured J616s baseline; g4 +retains it and adds the qualified shared ports and diagnostics below. ## Gap table @@ -30,7 +31,7 @@ parity. There are no J616s results for this series yet. | Periodic preemption | Context-switch timer mask is zero | Same zero mask; two-cluster operand-state allocation differs by hardware layout | Same runtime behavior | `g17/fw/initdata.rs::CONTEXT_SWITCH_TIMER_MASK`, `g17/fw/compute.rs::OperandState`; no evidence supports changing the mask | | High-priority queues | HIGH / MEDIUM / LOW firmware scheduling profiles | Same scheduling-policy selector and priority work rings | Same runtime behavior | `g17/mod.rs::scheduling_policy`, `g17/fw/channels.rs`, `g17/qos.rs` | | Timestamp publication | Retains one CPU mapping for each firmware timestamp buffer | Same shared timestamp buffer and completion publication | Host timestamp suite again compiles and executes the production retained-mapping writer | `timestamp.rs`, `mmu.rs::TimestampVmap`, `tools/testing/selftests/drivers/gpu/asahi-m4/test-timestamps.py` | -| Statistics | Drains firmware statistics; no `agx_stats` snapshot | Same; `stats_snapshot()` returns `None` on both | Same runtime behavior | `g17/channel.rs`, `g17/runtime/events.rs`, `g17/mod.rs`; statistics decoding/export remains a shared limitation | +| Statistics | Drains firmware statistics; no decoded `agx_stats` snapshot | Same; payloads discarded | g4 preserves full per-type/per-role raw snapshots and static-key report tracing | `g17/reports.rs`, `g17/channel.rs`, `g17/trace.*`; achieved-frequency/power decoding remains gated on firmware semantics | | Firmware startup / registration | Two instances, message acknowledgement, POWER_ON_DONE and MMIO render-pair registration; no lead commands | One merged 25G83 graph, status acknowledgement, no POWER_ON_DONE, no MMIO pair registration; lead commands required | Same runtime behavior | `g17/fw/variant.rs`, `g17/initdata/v25g83.rs`; startup differences are ABI requirements, while MMIO pair registration on 25G83 remains unqualified | | Queue pointer / restart layout | Producer at +0x40, recovery target at +0x50 | Producer at +0x50, no recovery-target word | Same runtime behavior | `g17/fw/variant.rs::PointerLayout`, `g17/fw/queue.rs`; do not copy another firmware build's offsets | | Core topology / mapping ownership | One six-core cluster; two firmware roots and fabric bridge | Qualified two-cluster, 20-core layout; reserved firmware tables, one root and no fabric bridge | Same runtime behavior | `hw/t6040.rs`, `hw/t8140.rs`, `g17/mod.rs`; other fused core counts are still refused, and this audit does not expand that admission | @@ -83,7 +84,7 @@ an offset, voltage, descriptor ABI, or Linux residency measurement. | Gated path | Exact facts needed for T6040/25G83 | Current evidence and disposition | | --- | --- | --- | -| Sustained top clock | DT OPP count, both voltage lanes, hardware A/B selector maps, selected logical-state limit; independently decoded achieved frequency or residency, counter units/timebase/wrap, request versus achieved-state semantics and limiting reason under sustained load | Per-chip DT handoff and maps are already mandatory; both maps select the final raw OPP. `max_frequency_khz` is an advertised maximum, not achieved telemetry. G17 `stats_snapshot()` returns None; ring drain alone proves no residency. Keep voltages/maps unchanged and require a measured Linux load trace. | +| Sustained top clock | DT OPP count, both voltage lanes, hardware A/B selector maps, selected logical-state limit; independently decoded achieved frequency or residency, counter units/timebase/wrap, request versus achieved-state semantics and limiting reason under sustained load | Per-chip DT handoff and maps are already mandatory; both maps select the final raw OPP. `max_frequency_khz` is an advertised maximum, not achieved telemetry. G17 `stats_snapshot()` returns None; g4 raw `firmware_reports` and `asahi_g17_report` preserve payloads but establish no decoded residency. Keep voltages/maps unchanged and require a measured Linux load trace. | | Raw versus logical state count | Meaning of the ADT count properties for this firmware, both SGX map banks and number of entries actually consumed by the 25G83 power tables | Public `HwData25G83`, calibration validation and loader handoff select eleven logical states from sixteen raw OPPs. Do not resize the firmware structure from the ADT property name or discard its unsorted OPPs. Compare actual top selection and achieved clock. | | DVFS ramp / idle / fast-die | 25G83 meaning and units of ramp/effort controls, idle/off delays, temperature limits and hysteresis; PMP/AP/CLPC request ownership, achieved floors and release under idle and load | Existing shared submission/idle policy, DT suppliers and T6040 power-state graph are enabled. Power-state presence is not proof of load tracking. Require live requests, actual floor release and thermal traces; opaque power words are not tunable parameters. | | Recovery slot attribution | Exact 25G83 slot-key storage range, lifetime, cache policy and slot stride; key QID/stamp bit fields; slot-mask meanings, progress monitor semantics and association with an actual started render | `recovery_slot_keys=None`; never copy T8140's physical address. Preserve firmware blame. Progress-view reads no longer depend on knowing the key address; bounded diagnostics report unknown ownership explicitly. | @@ -104,6 +105,39 @@ and page-fault fallback. Diagnostic counters explicitly identify lifetime stampless totals; they are not a count of commands retired by one restart. +## g4 shared runtime ports and observability + +The operator has selected g3 as the hardware baseline. This follow-on retains +its CLPC/PMP ports, DT idle hold and recovery-generation release guard. +Individual hardware records remain in the private qualification workspace; +no raw lab capture is a public implementation source. + +| Gap | Public source behavior | T6040 g4 disposition | Qualification | +| --- | --- | --- | --- | +| #504 fragment notification | `de0b22a8370e` selects a render pair on its fragment notification; final retirement still requires both stored stamps | Included for both firmware graphs; KUnit covers each mask bank and unrelated queues | Render-light latency/IRQ rate, growth/partial renders, recovery and CTS | +| #504 tiling first-kick | `5073eac15192` changes the word only for the 26A428 register program | Included unchanged; 25G83 retains 1 on every kick | No T6040 notification-halving claim; requires 25G83 descriptor/notification facts and controlled qualification | +| #496 compute-fault witness | `9a76756bf2d7` uses event 7 words 3/4 as QID/stamp on Neo, with consumed-ring guard | Imported prerequisite, but the existing firmware graph keeps 25G83 event 7 unknown | Exact 25G83 layout, queue identity, stamp semantics and DMA/ring visibility must be established before it can release memory | +| #505 closed-context teardown | `078d744a260c` kills before freeing executed unstamped render graphs, retains firmware-linked work storage, and reuses killed pairs last with QID capacity | Shared safety/retention paths included; compute-fault-driven kill depends on the graph-qualified #496 witness | Closed mixed compute/render faults, acknowledgement ordering, memory pressure and slot/QID reuse; does not claim to cure a live render's first fault | +| First fault | Reason and blame alone cannot identify engine progress | At halted state 1, before acknowledgement, g4 logs the blamed pair's stored stamps, free-list balance and up to four commands' pass/cursor snapshots; borrowed pairs are explicitly unavailable | Correlate exact CTS operation and original error with raw event report and snapshots; do not label reason 2 as page fault without semantics | +| Raw firmware statistics | Complete 18-word statistics records already have checked ring geometry | Read-only `firmware_reports` exposes last full sample, count and host monotonic time for each role/type. Static-key `asahi_g17_report` copies trace/statistics/event words before slot return | Match payloads to independently decoded achieved-clock/power/residency and freshness/units; legacy statistics tags are a different ABI | +| Idle memory floor | PMGR graphics power is a supplier lifetime, not itself an idle-work witness | Existing policy retained; CLPC status now exposes the sampled graphics status/active input and read-error count alongside CPU requests and rail votes | Long idle with no GPU clients, then load and release, must distinguish GPU power, CPU demand, failed reads and other PMC agents; never infer idle from an open client alone | + +Remaining exact facts for telemetry are the 25G83/26A428 tag-to-layout map, +field width/signedness/endianness, sample/timebase/wrap and freshness, +power units/averaging interval, logical/raw/cluster pstate selection, requested +versus achieved-state semantics, and limiting/thermal reason meanings. Raw +samples and the DT maximum are not MHz. Reads issue no firmware request and +never touch gated core registers. `agx_stats` remains unsupported for decoded +G17 data until those semantics are established. + +For the two deterministic CTS cases, retain the first failing operation before +cleanup, full unnormalized recovery logs, raw event trace, and pre-ack render +snapshots. The large-buffer and extreme-point-size cases can stress tiling, +but neither case name proves a TLB, timeout or TVB cause. No timeout, voltage, +feature-enable or buffer-size guess is included in g4. Multi-queue sync needs +its publish/GPU-span/event/fence timeline; #504's compute control does not +promise to halve that path's latency. + ## Source and validation boundaries The implementation references are the licensed public Aurora/Asahi kernel and From 32f7efefcb39734a758d8dbbd3708099f288c067 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 17:35:32 +1100 Subject: [PATCH 247/388] soc: apple: drive T6040 GPU memory demand from publication and continuous idle T6040 keeps the GFX supplier powered for the firmware's lifetime, as its PMGR DT documents. Using that supplier as CLPC demand therefore pins DCS 5 / fabric 2 even after the GPU cores gate. Neo's activity-following supplier is not a valid T6040 idle witness. Add a DT-selected CLPC demand consumer. Every actual GPU publication asserts it, including off-lock direct publication. The existing race-checked 16 ms continuous-idle effort clear releases it; quarantined work does not count as execution. CLPC retains its existing 32 ms hold and CPU arbitration. Firmware shutdown and consumer destruction clear the request. An independent refcounted request allocation is safe across provider removal, and a device link orders the supplier. The hot path uses only a leaf spinlock, with no sleeping call, allocation or GPU callback. Missing requested providers defer probe. Expose last requested floor, reason bits and host timestamps alongside actual PMC readback, and expose safe core-power plus last publication time in the read-only G17 firmware_reports view. Generic runtime PM remains distinct from firmware core gating: do not relabel unsupported generic PM as active/suspended. Keep Neo's existing PMGR input and all voltages, floors and delays unchanged. Assisted-by: LLM Signed-off-by: Ryan Murray --- .../devicetree/bindings/gpu/apple,agx.yaml | 7 + arch/arm64/boot/dts/apple/t6040.dtsi | 3 +- drivers/gpu/drm/asahi/Makefile | 2 +- drivers/gpu/drm/asahi/g17/clpc.c | 16 ++ drivers/gpu/drm/asahi/g17/clpc.h | 9 + drivers/gpu/drm/asahi/g17/mod.rs | 4 +- drivers/gpu/drm/asahi/g17/port.rs | 19 ++- drivers/gpu/drm/asahi/g17/power.rs | 40 +++++ drivers/gpu/drm/asahi/g17/reports.rs | 5 + drivers/soc/apple/clpc.c | 158 +++++++++++++++++- include/linux/soc/apple/clpc.h | 24 +++ 11 files changed, 279 insertions(+), 8 deletions(-) create mode 100644 drivers/gpu/drm/asahi/g17/clpc.c create mode 100644 drivers/gpu/drm/asahi/g17/clpc.h create mode 100644 include/linux/soc/apple/clpc.h diff --git a/Documentation/devicetree/bindings/gpu/apple,agx.yaml b/Documentation/devicetree/bindings/gpu/apple,agx.yaml index 145e277d6be4d..f5b1afd7d7dd3 100644 --- a/Documentation/devicetree/bindings/gpu/apple,agx.yaml +++ b/Documentation/devicetree/bindings/gpu/apple,agx.yaml @@ -72,6 +72,13 @@ properties: - enum: [hw-cal-b, fw-text, l2-pagetables] - enum: [globals, fw-data, fw-text] + apple,clpc: + $ref: /schemas/types.yaml#/definitions/phandle + description: | + Host performance controller receiving GPU submission and idle demand. + GFX supplier power cannot represent GPU work on firmware that keeps + its coprocessor domain powered while the cores are idle. + apple,idle-off-delay-ms: minimum: 2 maximum: 1000 diff --git a/arch/arm64/boot/dts/apple/t6040.dtsi b/arch/arm64/boot/dts/apple/t6040.dtsi index 1cbc2a97a51c1..01486e0f5ac65 100644 --- a/arch/arm64/boot/dts/apple/t6040.dtsi +++ b/arch/arm64/boot/dts/apple/t6040.dtsi @@ -617,6 +617,7 @@ reg-names = "asc", "sgx"; mboxes = <&gfx_mbox>; power-domains = <&pmp_gfx>; + apple,clpc = <&clpc>; status = "disabled"; }; @@ -1025,7 +1026,7 @@ clpc: performance-controller@1 { compatible = "apple,t6041-clpc"; reg = <1>; - apple,gpu-power-state = <&ps_gfx>; + }; }; diff --git a/drivers/gpu/drm/asahi/Makefile b/drivers/gpu/drm/asahi/Makefile index db46090abd943..fd8421f27590d 100644 --- a/drivers/gpu/drm/asahi/Makefile +++ b/drivers/gpu/drm/asahi/Makefile @@ -1,7 +1,7 @@ # SPDX-License-Identifier: GPL-2.0 obj-$(CONFIG_DRM_ASAHI) += asahi.o -asahi-y := asahi_core.o fwid.o sysfs.o g17/sync_input.o g17/sync_output.o g17/sync_scope.o g17/trace.o +asahi-y := asahi_core.o fwid.o sysfs.o g17/sync_input.o g17/sync_output.o g17/sync_scope.o g17/trace.o g17/clpc.o CFLAGS_g17/trace.o := -I$(src)/g17 asahi-$(CONFIG_DRM_ASAHI_G15) += g15/glue.o diff --git a/drivers/gpu/drm/asahi/g17/clpc.c b/drivers/gpu/drm/asahi/g17/clpc.c new file mode 100644 index 0000000000000..b8ae1242cd8aa --- /dev/null +++ b/drivers/gpu/drm/asahi/g17/clpc.c @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +#include +#include "clpc.h" + +int asahi_clpc_gpu_get(struct device *dev, struct apple_clpc_gpu **out) +{ + return apple_clpc_gpu_get(dev, out); +} +void asahi_clpc_gpu_set_active(struct apple_clpc_gpu *gpu, bool active) +{ + apple_clpc_gpu_set_active(gpu, active); +} +void asahi_clpc_gpu_put(struct apple_clpc_gpu *gpu) +{ + apple_clpc_gpu_put(gpu); +} diff --git a/drivers/gpu/drm/asahi/g17/clpc.h b/drivers/gpu/drm/asahi/g17/clpc.h new file mode 100644 index 0000000000000..3fc6f75473454 --- /dev/null +++ b/drivers/gpu/drm/asahi/g17/clpc.h @@ -0,0 +1,9 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ +#ifndef _ASAHI_G17_CLPC_H +#define _ASAHI_G17_CLPC_H +struct device; +struct apple_clpc_gpu; +int asahi_clpc_gpu_get(struct device *dev, struct apple_clpc_gpu **out); +void asahi_clpc_gpu_set_active(struct apple_clpc_gpu *gpu, bool active); +void asahi_clpc_gpu_put(struct apple_clpc_gpu *gpu); +#endif diff --git a/drivers/gpu/drm/asahi/g17/mod.rs b/drivers/gpu/drm/asahi/g17/mod.rs index d7a42c5c82bdb..a3c83bca59b21 100644 --- a/drivers/gpu/drm/asahi/g17/mod.rs +++ b/drivers/gpu/drm/asahi/g17/mod.rs @@ -644,6 +644,7 @@ struct Firmware { impl Drop for Firmware { fn drop(&mut self) { + self.port.set_effort_full(false); if let Some(secondary) = self.secondary.as_mut() { secondary.stop(); } @@ -694,6 +695,7 @@ impl Firmware { /// Called at actual submission publication, not while a queued job is merely being built. fn note_submission(&mut self) -> Result { + self.port.note_gpu_activity(); self.effort.submit(); self.apply_effort(power::FULL_EFFORT) } @@ -1361,7 +1363,7 @@ impl Gpu { secondary, primary, bridge, - port: port::Port::new()?, + port: port::Port::new(power::Demand::new(pdev.as_ref().as_raw())?)?, seen_submissions: 0, rings: channel::Rings::new()?, recovery: recovery::State::new(), diff --git a/drivers/gpu/drm/asahi/g17/port.rs b/drivers/gpu/drm/asahi/g17/port.rs index db043ef9d6f91..243fbfce07f9e 100644 --- a/drivers/gpu/drm/asahi/g17/port.rs +++ b/drivers/gpu/drm/asahi/g17/port.rs @@ -105,6 +105,8 @@ pub(super) struct Port { compute_announced: [AtomicU64; PRIORITIES], /// Full firmware effort is applied; cleared before an idle clear rechecks `submissions`. effort_full: AtomicBool, + demand: super::power::Demand, + last_submission_ns: AtomicU64, submissions: AtomicU64, } @@ -142,7 +144,7 @@ impl Drop for OuterClaim<'_> { } impl Port { - pub(super) fn new() -> Result> { + pub(super) fn new(demand: super::power::Demand) -> Result> { Arc::pin_init( pin_init!(Self { views: SetOnce::new(), @@ -155,6 +157,8 @@ impl Port { compute_written: [const { AtomicU64::new(0) }; PRIORITIES], compute_announced: [const { AtomicU64::new(0) }; PRIORITIES], effort_full: AtomicBool::new(false), + demand, + last_submission_ns: AtomicU64::new(0), submissions: AtomicU64::new(0), }), GFP_KERNEL, @@ -453,6 +457,7 @@ impl Port { /// clear sees this submission or this submission sees the flag withdrawn. pub(super) fn note_submission(&self) -> bool { self.submissions.fetch_add(1, Ordering::SeqCst); + self.note_gpu_activity(); self.effort_full.load(Ordering::SeqCst) } @@ -462,5 +467,17 @@ impl Port { pub(super) fn set_effort_full(&self, full: bool) { self.effort_full.store(full, Ordering::SeqCst); + self.demand.set_active(full); + } + + pub(super) fn note_gpu_activity(&self) { + self.last_submission_ns.store(super::now_ns(), Ordering::Relaxed); + self.demand.set_active(true); + } + + pub(super) fn write_power(&self, f: &mut kernel::fmt::Formatter<'_>) -> kernel::fmt::Result { + writeln!(f, "effort_full={} direct_submissions={} last_submission_ns={}", + self.effort_full.load(Ordering::SeqCst), self.submissions(), + self.last_submission_ns.load(Ordering::Relaxed)) } } diff --git a/drivers/gpu/drm/asahi/g17/power.rs b/drivers/gpu/drm/asahi/g17/power.rs index bbd10b204edd7..075d2829e4243 100644 --- a/drivers/gpu/drm/asahi/g17/power.rs +++ b/drivers/gpu/drm/asahi/g17/power.rs @@ -300,3 +300,43 @@ mod tests { assert_eq!(hold.step(1, false, 2, FIRMWARE_IDLE_OFF_MS), HoldAction::Release); } } + +unsafe extern "C" { + fn asahi_clpc_gpu_get(dev: *mut kernel::bindings::device, out: *mut *mut core::ffi::c_void) -> core::ffi::c_int; + fn asahi_clpc_gpu_set_active(gpu: *mut core::ffi::c_void, active: bool); + fn asahi_clpc_gpu_put(gpu: *mut core::ffi::c_void); +} + +/// DT-selected memory-demand provider. Its independent allocation outlives a +/// detached provider; a device link orders supplier removal and suspend. +pub(super) struct Demand(*mut core::ffi::c_void); + +// SAFETY: the provider's leaf spinlock serializes all access; no GPU callback. +unsafe impl Send for Demand {} +// SAFETY: shared access only changes that locked request, never its lifetime. +unsafe impl Sync for Demand {} + +impl Demand { + pub(super) fn new(dev: *mut kernel::bindings::device) -> kernel::error::Result { + let mut out = core::ptr::null_mut(); + // SAFETY: live platform device and writable result; C returns an owned reference. + let ret = unsafe { asahi_clpc_gpu_get(dev, &mut out) }; + if ret < 0 { + Err(kernel::error::Error::from_errno(ret)) + } else { + Ok(Self(out)) + } + } + + pub(super) fn set_active(&self, active: bool) { + // SAFETY: the retained reference is live; NULL denotes no DT provider. + unsafe { asahi_clpc_gpu_set_active(self.0, active) }; + } +} + +impl Drop for Demand { + fn drop(&mut self) { + // SAFETY: releases exactly the reference returned by get, clearing demand first. + unsafe { asahi_clpc_gpu_put(self.0) }; + } +} diff --git a/drivers/gpu/drm/asahi/g17/reports.rs b/drivers/gpu/drm/asahi/g17/reports.rs index 7e1afef70cad2..62ed791322ad9 100644 --- a/drivers/gpu/drm/asahi/g17/reports.rs +++ b/drivers/gpu/drm/asahi/g17/reports.rs @@ -57,6 +57,11 @@ impl kernel::debugfs::Writer for View { let state = self.0.state.lock(); let Some(firmware) = state.as_deref() else { return writeln!(f, "online=0"); }; writeln!(f, "online=1 graph={:?} sampled_ns={}", self.0.variant.graph, super::now_ns())?; + let core_power = firmware.bridge.regs.g17_core_power(); + writeln!(f, "power_management=firmware core_power={:#x} cores_powered={} idle_hold_ms={}", + core_power, core_power & 0xf != 0, + super::power::IDLE_HOLD_MS.load(core::sync::atomic::Ordering::Relaxed))?; + firmware.port.write_power(f)?; firmware.rings.write_reports(f) } } diff --git a/drivers/soc/apple/clpc.c b/drivers/soc/apple/clpc.c index 1acfcc0b598dd..53318ec78ced8 100644 --- a/drivers/soc/apple/clpc.c +++ b/drivers/soc/apple/clpc.c @@ -21,6 +21,12 @@ */ #include +#include +#include +#include +#include +#include +#include #include #include #include @@ -78,6 +84,15 @@ struct apple_clpc_vote { unsigned long last_demand; }; +/* Independent lifetime: a late consumer put never dereferences a removed provider. */ +struct apple_clpc_gpu { + struct kref ref; + spinlock_t lock; + bool active; + u64 updated_ns; + u64 sequence; +}; + struct apple_clpc { struct device *dev; struct device *pmc; @@ -100,12 +115,102 @@ struct apple_clpc { unsigned long update_errors; struct dentry *debugfs; struct regmap *gpu_pmgr; + struct apple_clpc_gpu *gpu; + u32 last_floor; + u32 last_reason; + u64 last_vote_ns; u32 gpu_offset; u32 gpu_status; bool gpu_active; unsigned long gpu_read_errors; }; +static void apple_clpc_gpu_free(struct kref *ref) +{ + kfree(container_of(ref, struct apple_clpc_gpu, ref)); +} + +void apple_clpc_gpu_set_active(struct apple_clpc_gpu *gpu, bool active) +{ + unsigned long flags; + + if (!gpu) + return; + spin_lock_irqsave(&gpu->lock, flags); + gpu->active = active; + gpu->updated_ns = ktime_get_mono_fast_ns(); + gpu->sequence++; + spin_unlock_irqrestore(&gpu->lock, flags); +} +EXPORT_SYMBOL_GPL(apple_clpc_gpu_set_active); + +void apple_clpc_gpu_put(struct apple_clpc_gpu *gpu) +{ + if (gpu) { + apple_clpc_gpu_set_active(gpu, false); + kref_put(&gpu->ref, apple_clpc_gpu_free); + } +} +EXPORT_SYMBOL_GPL(apple_clpc_gpu_put); + +int apple_clpc_gpu_get(struct device *dev, struct apple_clpc_gpu **out) +{ + struct device_node *np; + struct platform_device *provider; + struct apple_clpc *clpc; + struct device_link *link; + int ret = -EPROBE_DEFER; + + *out = NULL; + np = of_parse_phandle(dev->of_node, "apple,clpc", 0); + if (!np) + return of_find_property(dev->of_node, "apple,clpc", NULL) ? -EINVAL : 0; + if (!of_device_is_compatible(np, "apple,t6040-clpc")) { + of_node_put(np); + return -EINVAL; + } + provider = of_find_device_by_node(np); + of_node_put(np); + if (!provider) + return ret; + link = device_link_add(dev, &provider->dev, DL_FLAG_AUTOREMOVE_CONSUMER); + if (!link) { + put_device(&provider->dev); + return -ENOMEM; + } + device_lock(&provider->dev); + clpc = device_is_bound(&provider->dev) ? dev_get_drvdata(&provider->dev) : NULL; + if (clpc && clpc->gpu && refcount_read(&clpc->gpu->ref.refcount) == 1) { + kref_get(&clpc->gpu->ref); + *out = clpc->gpu; + ret = 0; + } + device_unlock(&provider->dev); + put_device(&provider->dev); + return ret; +} +EXPORT_SYMBOL_GPL(apple_clpc_gpu_get); + +static bool apple_clpc_gpu_active(struct apple_clpc_gpu *gpu, u64 *updated, u64 *sequence) +{ + unsigned long flags; + bool active; + + spin_lock_irqsave(&gpu->lock, flags); + active = gpu->active; + *updated = gpu->updated_ns; + *sequence = gpu->sequence; + spin_unlock_irqrestore(&gpu->lock, flags); + return active; +} + +static void apple_clpc_gpu_owner_put(void *data) +{ + struct apple_clpc_gpu *gpu = data; + + kref_put(&gpu->ref, apple_clpc_gpu_free); +} + /* cpufreq policies can appear after probe; pick up new ones as they do. */ static void apple_clpc_find_clusters(struct apple_clpc *clpc) { @@ -236,6 +341,7 @@ static void apple_clpc_sample(struct apple_clpc *clpc) unsigned int state[APPLE_CLPC_RAILS]; unsigned long now = jiffies; unsigned int i, rail; + u32 reason = 0, floor; clpc->samples++; if (!cpumask_equal(clpc->covered, cpu_possible_mask) && @@ -251,6 +357,18 @@ static void apple_clpc_sample(struct apple_clpc *clpc) for (rail = 0; rail < APPLE_CLPC_RAILS; rail++) demand[rail] = max(demand[rail], cl->request[rail]); } + for (rail = 0; rail < APPLE_CLPC_RAILS; rail++) + if (demand[rail]) + reason |= 1; + if (clpc->gpu) { + u64 updated, sequence; + + if (apple_clpc_gpu_active(clpc->gpu, &updated, &sequence)) { + reason |= 2; + for (rail = 0; rail < APPLE_CLPC_RAILS; rail++) + demand[rail] = clpc->hw->map[rail][APPLE_CLPC_LEVELS - 1]; + } + } if (clpc->gpu_pmgr) { u32 status; @@ -259,11 +377,14 @@ static void apple_clpc_sample(struct apple_clpc *clpc) */ if (regmap_read(clpc->gpu_pmgr, clpc->gpu_offset, &status)) { clpc->gpu_read_errors++; + reason |= 8; clpc->gpu_active = true; } else { clpc->gpu_status = status; clpc->gpu_active = (status & GENMASK(7, 4)) == GENMASK(7, 4); } + if (clpc->gpu_active) + reason |= 4; if (clpc->gpu_active) for (rail = 0; rail < APPLE_CLPC_RAILS; rail++) demand[rail] = clpc->hw->map[rail][APPLE_CLPC_LEVELS - 1]; @@ -273,8 +394,13 @@ static void apple_clpc_sample(struct apple_clpc *clpc) state[rail] = apple_clpc_vote(&clpc->vote[rail], demand[rail], now); - if (apple_pmc_floor_update(clpc->pmc, clpc->agent, clpc->mask, - apple_clpc_floor(clpc, state))) + floor = apple_clpc_floor(clpc, state); + if (!reason && floor) + reason = 16; + clpc->last_floor = floor; + clpc->last_reason = reason; + clpc->last_vote_ns = ktime_get_mono_fast_ns(); + if (apple_pmc_floor_update(clpc->pmc, clpc->agent, clpc->mask, floor)) clpc->update_errors++; apple_clpc_queue(clpc, !apple_clpc_floor(clpc, state)); @@ -308,6 +434,16 @@ static int apple_clpc_status_show(struct seq_file *s, void *unused) clpc->hw->rail[rail]), clpc->hw->map[rail][0], clpc->hw->map[rail][1], clpc->hw->map[rail][2], clpc->hw->map[rail][3]); + seq_printf(s, "last_vote: %#x reason_bits %#x sampled_ns %llu (cpu=1 host_gpu=2 pmgr_gpu=4 read_error=8 hold=16)\n", + READ_ONCE(clpc->last_floor), READ_ONCE(clpc->last_reason), + READ_ONCE(clpc->last_vote_ns)); + if (clpc->gpu) { + u64 updated, sequence; + bool active = apple_clpc_gpu_active(clpc->gpu, &updated, &sequence); + + seq_printf(s, "host_gpu: active %u request_ns %llu sequence %llu\n", + active, updated, sequence); + } seq_printf(s, "gpu: present %u active %u status %#x read_errors %lu\n", !!clpc->gpu_pmgr, READ_ONCE(clpc->gpu_active), READ_ONCE(clpc->gpu_status), READ_ONCE(clpc->gpu_read_errors)); @@ -377,6 +513,19 @@ static int apple_clpc_probe(struct platform_device *pdev) clpc->dev = dev; clpc->pmc = dev->parent; clpc->hw = of_device_get_match_data(dev); + /* T6040 firmware keeps GFX powered for its whole lifetime. A separate + * host publication/idle input, not that supplier state, drives demand. + */ + if (of_device_is_compatible(dev->of_node, "apple,t6040-clpc")) { + clpc->gpu = kzalloc_obj(*clpc->gpu); + if (!clpc->gpu) + return -ENOMEM; + kref_init(&clpc->gpu->ref); + spin_lock_init(&clpc->gpu->lock); + ret = devm_add_action_or_reset(dev, apple_clpc_gpu_owner_put, clpc->gpu); + if (ret) + return ret; + } if (clpc->hw->gpu_demand) { struct device_node *gpu = of_parse_phandle(dev->of_node, "apple,gpu-power-state", 0); @@ -521,7 +670,8 @@ static const struct apple_clpc_hw apple_clpc_hw_t8140 = { * a single busy sample raised the floor about 60 % of the time, from short * bursts of background work; three in a row bring that to about 11 %. * GPU demand bypasses the CPU streak and uses the same qualified DCS 5 / - * fabric 2 states until the graphics state powers off and HOLD_MS expires. + * fabric 2 states until publication/in-flight demand ends and HOLD_MS expires. + * GFX supplier power cannot drive this input: firmware keeps it always on. */ static const struct apple_clpc_hw apple_clpc_hw_t6040 = { .rail = { [APPLE_CLPC_DCS] = 0, [APPLE_CLPC_SOC] = 1 }, @@ -530,7 +680,7 @@ static const struct apple_clpc_hw apple_clpc_hw_t6040 = { [APPLE_CLPC_SOC] = { 0, 1, 2, 2 }, }, .sustain = 3, - .gpu_demand = true, + .gpu_demand = false, }; static const struct of_device_id apple_clpc_of_match[] = { diff --git a/include/linux/soc/apple/clpc.h b/include/linux/soc/apple/clpc.h new file mode 100644 index 0000000000000..37b2cfce5594a --- /dev/null +++ b/include/linux/soc/apple/clpc.h @@ -0,0 +1,24 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ +#ifndef _LINUX_SOC_APPLE_CLPC_H +#define _LINUX_SOC_APPLE_CLPC_H + +#include +#include +#include + +struct apple_clpc_gpu; + +#if IS_REACHABLE(CONFIG_APPLE_CLPC) +int apple_clpc_gpu_get(struct device *dev, struct apple_clpc_gpu **out); +void apple_clpc_gpu_set_active(struct apple_clpc_gpu *gpu, bool active); +void apple_clpc_gpu_put(struct apple_clpc_gpu *gpu); +#else +static inline int apple_clpc_gpu_get(struct device *dev, struct apple_clpc_gpu **out) +{ + *out = NULL; + return device_property_present(dev, "apple,clpc") ? -ENODEV : 0; +} +static inline void apple_clpc_gpu_set_active(struct apple_clpc_gpu *gpu, bool active) {} +static inline void apple_clpc_gpu_put(struct apple_clpc_gpu *gpu) {} +#endif +#endif From 07a2cd1bc82b22ad1781342c1aa49e2b6a66817f Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 17:36:05 +1100 Subject: [PATCH 248/388] soc: apple: qualify the T6040 host-demand input and release lifetime Use the existing t6041-clpc compatible selected by the T6040 DT, rather than a nonexistent compatible. Extend the production CLPC host fixture with an always-powered supplier and explicit GPU activity/idle input, reason/timestamp checks, CPU arbitration and negative release controls. KUnit exercises the real request's locking/reference lifetime and clears on consumer removal, including a late put after provider removal. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/soc/apple/clpc.c | 59 +++++++++++++++++++++++++++- tools/asahi/clpc-gpu-demand-tests.py | 39 ++++++++++++++++-- 2 files changed, 92 insertions(+), 6 deletions(-) diff --git a/drivers/soc/apple/clpc.c b/drivers/soc/apple/clpc.c index 53318ec78ced8..3491030c670dd 100644 --- a/drivers/soc/apple/clpc.c +++ b/drivers/soc/apple/clpc.c @@ -165,7 +165,7 @@ int apple_clpc_gpu_get(struct device *dev, struct apple_clpc_gpu **out) np = of_parse_phandle(dev->of_node, "apple,clpc", 0); if (!np) return of_find_property(dev->of_node, "apple,clpc", NULL) ? -EINVAL : 0; - if (!of_device_is_compatible(np, "apple,t6040-clpc")) { + if (!of_device_is_compatible(np, "apple,t6041-clpc")) { of_node_put(np); return -EINVAL; } @@ -516,7 +516,7 @@ static int apple_clpc_probe(struct platform_device *pdev) /* T6040 firmware keeps GFX powered for its whole lifetime. A separate * host publication/idle input, not that supplier state, drives demand. */ - if (of_device_is_compatible(dev->of_node, "apple,t6040-clpc")) { + if (of_device_is_compatible(dev->of_node, "apple,t6041-clpc")) { clpc->gpu = kzalloc_obj(*clpc->gpu); if (!clpc->gpu) return -ENOMEM; @@ -683,6 +683,61 @@ static const struct apple_clpc_hw apple_clpc_hw_t6040 = { .gpu_demand = false, }; +#if IS_BUILTIN(CONFIG_KUNIT) && !defined(MODULE) +#include + +static void apple_clpc_gpu_request_lifetime_test(struct kunit *test) +{ + struct apple_clpc_gpu *gpu = kzalloc_obj(*gpu); + u64 updated, sequence; + + KUNIT_ASSERT_NOT_NULL(test, gpu); + kref_init(&gpu->ref); + spin_lock_init(&gpu->lock); + KUNIT_EXPECT_FALSE(test, apple_clpc_gpu_active(gpu, &updated, &sequence)); + KUNIT_EXPECT_EQ(test, sequence, 0ull); + /* Provider and one consumer own independent references. */ + kref_get(&gpu->ref); + apple_clpc_gpu_set_active(gpu, true); + KUNIT_EXPECT_TRUE(test, apple_clpc_gpu_active(gpu, &updated, &sequence)); + KUNIT_EXPECT_GT(test, updated, 0ull); + KUNIT_EXPECT_EQ(test, sequence, 1ull); + apple_clpc_gpu_put(gpu); + KUNIT_EXPECT_FALSE(test, apple_clpc_gpu_active(gpu, &updated, &sequence)); + KUNIT_EXPECT_EQ(test, sequence, 2ull); + kref_put(&gpu->ref, apple_clpc_gpu_free); +} + +static void apple_clpc_gpu_provider_removal_test(struct kunit *test) +{ + struct apple_clpc_gpu *gpu = kzalloc_obj(*gpu); + u64 updated, sequence; + + KUNIT_ASSERT_NOT_NULL(test, gpu); + kref_init(&gpu->ref); + spin_lock_init(&gpu->lock); + kref_get(&gpu->ref); + /* Provider removal must not leave a late consumer with freed storage. */ + kref_put(&gpu->ref, apple_clpc_gpu_free); + apple_clpc_gpu_set_active(gpu, true); + KUNIT_EXPECT_TRUE(test, apple_clpc_gpu_active(gpu, &updated, &sequence)); + apple_clpc_gpu_set_active(gpu, false); + KUNIT_EXPECT_FALSE(test, apple_clpc_gpu_active(gpu, &updated, &sequence)); + apple_clpc_gpu_put(gpu); +} + +static struct kunit_case apple_clpc_cases[] = { + KUNIT_CASE(apple_clpc_gpu_request_lifetime_test), + KUNIT_CASE(apple_clpc_gpu_provider_removal_test), + {} +}; +static struct kunit_suite apple_clpc_suite = { + .name = "apple-clpc-gpu", + .test_cases = apple_clpc_cases, +}; +kunit_test_suite(apple_clpc_suite); +#endif + 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 }, diff --git a/tools/asahi/clpc-gpu-demand-tests.py b/tools/asahi/clpc-gpu-demand-tests.py index b39b0848ed74a..379de4946bd19 100644 --- a/tools/asahi/clpc-gpu-demand-tests.py +++ b/tools/asahi/clpc-gpu-demand-tests.py @@ -39,6 +39,7 @@ def main(): #include typedef uint8_t u8; typedef uint32_t u32; +typedef uint64_t u64; #define APPLE_CLPC_LEVELS 4 #define APPLE_CLPC_HOLD_MS 32 #define HZ 1000 @@ -49,6 +50,7 @@ def main(): #define msecs_to_jiffies(ms) (ms) enum apple_clpc_rail { APPLE_CLPC_DCS, APPLE_CLPC_SOC, APPLE_CLPC_RAILS }; ''' + types + r''' +struct apple_clpc_gpu { bool active; }; struct apple_clpc_cluster { unsigned int request[APPLE_CLPC_RAILS]; }; struct apple_clpc { const struct apple_clpc_hw *hw; @@ -57,9 +59,15 @@ def main(): unsigned int nr_clusters, agent; unsigned long samples, next_scan, update_errors; void *covered, *pmc, *gpu_pmgr; - u32 gpu_offset, mask; + u32 gpu_offset, mask, gpu_status, last_floor, last_reason; + bool gpu_active; + unsigned long gpu_read_errors; + u64 last_vote_ns; + struct apple_clpc_gpu *gpu; }; static unsigned long jiffies; +static u64 ktime_get_mono_fast_ns(void) { return jiffies * 1000000ull; } +static bool apple_clpc_gpu_active(struct apple_clpc_gpu *g, u64 *u, u64 *s) { *u=jiffies; *s=1; return g->active; } static void *cpu_possible_mask; static u32 pmgr_status, published; static int read_error, vote_error; @@ -76,6 +84,7 @@ def main(): static void apple_clpc_queue(struct apple_clpc *c, bool idle) { (void)c; queued_idle=idle; } ''' + tables + '\n' + methods + r''' static void check(const struct apple_clpc_hw *hw, u32 top) { + assert(hw->gpu_demand); struct apple_clpc c={.hw=hw,.agent=1,.mask=3,.gpu_pmgr=hw->gpu_demand ? (void *)1 : NULL}; jiffies=0; read_error=vote_error=0; pmgr_status=0; published=~0u; apple_clpc_sample(&c); assert(published==0 && queued_idle); @@ -98,10 +107,31 @@ def main(): vote_error=-EIO; apple_clpc_sample(&c); assert(c.update_errors==1); } int main(void) { - check(&apple_clpc_hw_t6040,0x25); + // T6040's supplier is always powered: it must NEVER substitute for work. + struct apple_clpc_gpu gpu={0}; + struct apple_clpc c={.hw=&apple_clpc_hw_t6040,.agent=1,.mask=3,.gpu=&gpu}; + assert(!c.hw->gpu_demand); + jiffies=0; read_error=vote_error=0; pmgr_status=0xf0; published=~0u; + apple_clpc_sample(&c); assert(published==0 && queued_idle && c.last_reason==0); + gpu.active=true; jiffies=100; apple_clpc_sample(&c); + assert(published==0x25 && c.last_reason==2 && c.last_floor==0x25); + assert(c.last_vote_ns==100000000ull); + // Core/firmware supplier is still powered, but continuous idle ends demand. + gpu.active=false; jiffies=131; apple_clpc_sample(&c); + assert(published==0x25 && c.last_reason==16); + jiffies=132; apple_clpc_sample(&c); + assert(published==0 && queued_idle && c.last_reason==0); + // A new submission reasserts demand; independent CPU requests survive release. + c.nr_clusters=1; c.clusters[0].request[0]=2; c.clusters[0].request[1]=1; + gpu.active=true; jiffies=200; apple_clpc_sample(&c); + assert(published==0x25 && c.last_reason==3); + gpu.active=false; jiffies=232; apple_clpc_sample(&c); + assert(published==0x12 && c.last_reason==1); + c.clusters[0].request[0]=c.clusters[0].request[1]=0; + jiffies=264; apple_clpc_sample(&c); assert(published==0); check(&apple_clpc_hw_t8140,0x34); check(&apple_clpc_hw_t8132,0x35); - puts("PASS: T6040 5/2, T8140 4/3 and T8132 5/3 GPU floors; 32 ms idle release; partial state, read failure, CPU demand and vote-error controls"); + puts("PASS: T6040 5/2, T8140 4/3 and T8132 5/3 GPU floors; host-idle release despite always-on GFX, reason/timestamp, 32 ms hold, legacy PMGR/read-error and CPU controls"); } ''' code = code.replace('#define APPLE_PMC_RAIL_SHIFT(r) ((r)*8)', rail_shift) @@ -109,7 +139,8 @@ def main(): root = Path(tmp) for name, text in [ ('fixed', code), - ('omit-gpu-demand', code.replace('if (clpc->gpu_pmgr) {', 'if (false) {')), + ('omit-host-demand', code.replace('if (clpc->gpu) {', 'if (false) {')), + ('omit-pmgr-demand', code.replace('if (clpc->gpu_pmgr) {', 'if (false) {')), ('release-too-early', code.replace('msecs_to_jiffies(APPLE_CLPC_HOLD_MS)', '0')), ('retain-at-idle', code.replace('vote->state = 0;', '')), ]: From 6c97d25086a07f970ae9bc2266c33b6b2bd98c52 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 17:37:23 +1100 Subject: [PATCH 249/388] tools/asahi: follow the shared fault witness and render teardown interfaces Keep host resource/ownership tests compiling the production methods after #496/#505. Include the actual fault-stamp comparison, model the new queue field, and supply inert tracing/capacity notifications. The owner-ordering fixture models no executed unstamped pair; that predicate and its lifetime are covered in kernel KUnit. Preserve each original negative control. Assisted-by: LLM Signed-off-by: Ryan Murray --- tools/asahi/g17-capacity-fault-tests.py | 8 ++++---- tools/asahi/g17-owner-release-order-tests.py | 2 ++ tools/asahi/g17-pool-release-tests.py | 2 ++ tools/asahi/g17-resource-tests.py | 4 ++-- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/tools/asahi/g17-capacity-fault-tests.py b/tools/asahi/g17-capacity-fault-tests.py index 2d0e755b5b01f..c47d193a74c55 100644 --- a/tools/asahi/g17-capacity-fault-tests.py +++ b/tools/asahi/g17-capacity-fault-tests.py @@ -28,13 +28,13 @@ def fixture(): retirement = (ROOT / 'queue/compute/retirement.rs').read_text() previous = item(source, 'struct Previous<') + '\n' + item(source, 'impl Previous') methods = '\n'.join(item(source, sig) for sig in [ - 'pub(crate) fn release_owner(', 'pub(crate) fn reusable(', + 'fn witness_stamp(', 'pub(crate) fn release_owner(', 'pub(crate) fn reusable(', 'pub(crate) fn recycle_quarantined_idle(', 'pub(crate) fn recycle_killed(', 'fn recycle_settled(', 'fn release_witnessed_inner(', 'pub(crate) fn share_idle(', 'pub(crate) fn shareable(', 'pub(crate) fn can_rearm(', 'pub(crate) fn recycled_pending(']) exit_source = (ROOT / 'queue/compute/exit.rs').read_text() methods += '\n' + item(exit_source, 'pub(crate) fn settle_exit(').replace('super::Previous', 'Previous').replace('crate::g17::qos::Owner', 'qos::Owner') drain = item((ROOT / 'mod.rs').read_text(), 'fn drain_reclaims(') - proof = item(retirement, 'pub(crate) const fn stamp_covers(') + '\n' + item(retirement, 'pub(crate) fn quarantined_retired(') + proof = item(retirement, 'pub(crate) const fn stamp_newer(') + '\n' + item(retirement, 'pub(crate) const fn stamp_covers(') + '\n' + item(retirement, 'pub(crate) fn quarantined_retired(') return r''' #![allow(dead_code)] use std::sync::{Arc, atomic::{AtomicUsize, AtomicU32, Ordering}}; @@ -108,7 +108,7 @@ def fixture(): retired_by_teardown: bool, spared_quarantine: bool, spared_deferred: bool, retire_pending: bool, retirement_proved: bool, replay_retirement: bool, replay_error: Option, replay_prepared: bool, submitted: u32, kick: Kick, - config_cache: Option<()>, source_hint: ReaderHint, source_readers: AtomicU32, claims: u32, + config_cache: Option<()>, source_hint: ReaderHint, source_readers: AtomicU32, claims: u32, fault_stamp: Option, } impl Queue { fn valid_ticket(&self, _: &Active, _: &Ticket) -> Result { Ok(()) } @@ -128,7 +128,7 @@ def fixture(): retired_by_teardown: false, spared_quarantine: false, spared_deferred: false, retire_pending: false, retirement_proved: false, replay_retirement: false, replay_error: None, replay_prepared: false, submitted: 0, kick: Kick, - config_cache: None, source_hint: ReaderHint { possible: false }, source_readers: AtomicU32::new(0), claims: 0, + fault_stamp: None, config_cache: None, source_hint: ReaderHint { possible: false }, source_readers: AtomicU32::new(0), claims: 0, } } fn fault(live: &Arc) -> Queue { diff --git a/tools/asahi/g17-owner-release-order-tests.py b/tools/asahi/g17-owner-release-order-tests.py index ad17fa4048726..3f68e930f9001 100755 --- a/tools/asahi/g17-owner-release-order-tests.py +++ b/tools/asahi/g17-owner-release-order-tests.py @@ -121,6 +121,8 @@ def fixture(): struct Packet { context: Arc } struct Pair { executed: u64, switch: Option<(u64, u64)>, replay: Vec<()>, active: Vec, idle: bool } impl Pair { + // This ordering fixture models no faulted/executed unstamped pair. + fn executed_for_kill(&self, _: &Arc) -> bool { false } SWITCH /// Host stand-in for a successful publication of `ordinal` (stand-in, not production). fn publish(&mut self, packet: &Packet, ordinal: u64) { diff --git a/tools/asahi/g17-pool-release-tests.py b/tools/asahi/g17-pool-release-tests.py index 2ad4c6901cba4..70004fe94ecf9 100644 --- a/tools/asahi/g17-pool-release-tests.py +++ b/tools/asahi/g17-pool-release-tests.py @@ -98,6 +98,8 @@ def fixture(): admission: Admission, } impl Registry { + // Physical-QID capacity notification does not change this pool-release fixture. + fn note_build_room(&self) {} METHODS fn poll(&self) -> bool { self.compute.iter().flatten().filter_map(|entry| entry.queue.as_deref())PREDICATE diff --git a/tools/asahi/g17-resource-tests.py b/tools/asahi/g17-resource-tests.py index cf93704966c33..ae7e53ec6cf6f 100644 --- a/tools/asahi/g17-resource-tests.py +++ b/tools/asahi/g17-resource-tests.py @@ -24,7 +24,7 @@ def item(start): limits='\n'.join(re.search(r'const '+name+r': [^;]+;',mmu).group(0) for name in ['EXECUTION_CONTEXT_MASK','MAX_EXECUTION_CONTEXTS']) header='''#![allow(dead_code, unused_macros)] type Result = std::result::Result; - mod g17 { pub mod capacity { + mod g17 { pub mod trace { pub fn teardown(_:u8,_:u64,_:u64,_:u64) {} } pub mod capacity { pub enum Site { RenderSlots, LogicalQueues, ComputeReserve, PhysicalQids } pub fn refused(_: Site) -> i32 { -28 } }} @@ -137,7 +137,7 @@ def item(s,start): constants=re.search(r'const QID_MAX: [^;]+;',fw).group(0)+'\n'+'\n'.join(re.search(r'const '+name+r': [^;]+;',s).group(0) for name in ['QID_COUNT','RENDER_HEADROOM','FULL_RENDER_RESERVE']) header='''#![allow(dead_code, unused_macros)] type Result = std::result::Result; - mod g17 { pub mod capacity { + mod g17 { pub mod trace { pub fn teardown(_:u8,_:u64,_:u64,_:u64) {} } pub mod capacity { pub enum Site { RenderSlots, LogicalQueues, ComputeReserve, PhysicalQids } pub fn refused(_: Site) -> i32 { -28 } }} From 61a2fec62a709db5b435a9a41ff51270362d4a80 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 17:37:42 +1100 Subject: [PATCH 250/388] drm/asahi: pass an opaque device across the CLPC FFI boundary Keep the C wrapper responsible for the device layout, as the existing sysfs wrappers do. Rust bindings include private configuration-dependent scheduler members and cannot be named in a WERROR-safe extern signature. The live platform device and owned request lifetime remain unchanged. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/asahi/g17/clpc.c | 2 +- drivers/gpu/drm/asahi/g17/clpc.h | 2 +- drivers/gpu/drm/asahi/g17/power.rs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/clpc.c b/drivers/gpu/drm/asahi/g17/clpc.c index b8ae1242cd8aa..58adaba19910f 100644 --- a/drivers/gpu/drm/asahi/g17/clpc.c +++ b/drivers/gpu/drm/asahi/g17/clpc.c @@ -2,7 +2,7 @@ #include #include "clpc.h" -int asahi_clpc_gpu_get(struct device *dev, struct apple_clpc_gpu **out) +int asahi_clpc_gpu_get(void *dev, struct apple_clpc_gpu **out) { return apple_clpc_gpu_get(dev, out); } diff --git a/drivers/gpu/drm/asahi/g17/clpc.h b/drivers/gpu/drm/asahi/g17/clpc.h index 3fc6f75473454..372820ade7eff 100644 --- a/drivers/gpu/drm/asahi/g17/clpc.h +++ b/drivers/gpu/drm/asahi/g17/clpc.h @@ -3,7 +3,7 @@ #define _ASAHI_G17_CLPC_H struct device; struct apple_clpc_gpu; -int asahi_clpc_gpu_get(struct device *dev, struct apple_clpc_gpu **out); +int asahi_clpc_gpu_get(void *dev, struct apple_clpc_gpu **out); void asahi_clpc_gpu_set_active(struct apple_clpc_gpu *gpu, bool active); void asahi_clpc_gpu_put(struct apple_clpc_gpu *gpu); #endif diff --git a/drivers/gpu/drm/asahi/g17/power.rs b/drivers/gpu/drm/asahi/g17/power.rs index 075d2829e4243..a2e293df91510 100644 --- a/drivers/gpu/drm/asahi/g17/power.rs +++ b/drivers/gpu/drm/asahi/g17/power.rs @@ -302,7 +302,7 @@ mod tests { } unsafe extern "C" { - fn asahi_clpc_gpu_get(dev: *mut kernel::bindings::device, out: *mut *mut core::ffi::c_void) -> core::ffi::c_int; + fn asahi_clpc_gpu_get(dev: *mut core::ffi::c_void, out: *mut *mut core::ffi::c_void) -> core::ffi::c_int; fn asahi_clpc_gpu_set_active(gpu: *mut core::ffi::c_void, active: bool); fn asahi_clpc_gpu_put(gpu: *mut core::ffi::c_void); } @@ -320,7 +320,7 @@ impl Demand { pub(super) fn new(dev: *mut kernel::bindings::device) -> kernel::error::Result { let mut out = core::ptr::null_mut(); // SAFETY: live platform device and writable result; C returns an owned reference. - let ret = unsafe { asahi_clpc_gpu_get(dev, &mut out) }; + let ret = unsafe { asahi_clpc_gpu_get(dev.cast(), &mut out) }; if ret < 0 { Err(kernel::error::Error::from_errno(ret)) } else { From 460e4606950d5358128cb09f5b131a476a37be42 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 17:39:16 +1100 Subject: [PATCH 251/388] drm/asahi: expose firmware-managed GPU core power as a read-only view Provide a concise debugfs power view with the existing always-accessible core power witness, host hold state, idle policy, effort, direct-publication count and last publication time. Read under the live firmware owner without waking the GPU. The same values accompany raw firmware reports. Generic Linux runtime PM does not manage this firmware's autonomous core gating and may correctly report unsupported. Report the actual firmware power witness without enabling a fictitious runtime-suspend implementation. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/asahi/g17/mod.rs | 6 ++++++ drivers/gpu/drm/asahi/g17/reports.rs | 25 ++++++++++++++++++++----- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/mod.rs b/drivers/gpu/drm/asahi/g17/mod.rs index a3c83bca59b21..0e49420f044a1 100644 --- a/drivers/gpu/drm/asahi/g17/mod.rs +++ b/drivers/gpu/drm/asahi/g17/mod.rs @@ -1103,6 +1103,7 @@ pub(crate) struct Gpu { shared: Arc, _capacity: Pin>>, _reports: Pin>>, + _power: Pin>>, } impl Gpu { @@ -1214,6 +1215,10 @@ impl Gpu { directory.read_only_file(c"firmware_reports", reports::View(shared.clone())), GFP_KERNEL, )?; + let power = KBox::pin_init( + directory.read_only_file(c"power", reports::PowerView(shared.clone())), + GFP_KERNEL, + )?; let gpu = Arc::new( Gpu { cfg, @@ -1224,6 +1229,7 @@ impl Gpu { shared, _capacity: capacity, _reports: reports, + _power: power, }, GFP_KERNEL, )?; diff --git a/drivers/gpu/drm/asahi/g17/reports.rs b/drivers/gpu/drm/asahi/g17/reports.rs index 62ed791322ad9..b67512f7eec6b 100644 --- a/drivers/gpu/drm/asahi/g17/reports.rs +++ b/drivers/gpu/drm/asahi/g17/reports.rs @@ -57,15 +57,30 @@ impl kernel::debugfs::Writer for View { let state = self.0.state.lock(); let Some(firmware) = state.as_deref() else { return writeln!(f, "online=0"); }; writeln!(f, "online=1 graph={:?} sampled_ns={}", self.0.variant.graph, super::now_ns())?; - let core_power = firmware.bridge.regs.g17_core_power(); - writeln!(f, "power_management=firmware core_power={:#x} cores_powered={} idle_hold_ms={}", - core_power, core_power & 0xf != 0, - super::power::IDLE_HOLD_MS.load(core::sync::atomic::Ordering::Relaxed))?; - firmware.port.write_power(f)?; + write_power(firmware, f)?; firmware.rings.write_reports(f) } } +fn write_power(firmware: &super::Firmware, f: &mut kernel::fmt::Formatter<'_>) -> kernel::fmt::Result { + let core_power = firmware.bridge.regs.g17_core_power(); + writeln!(f, "power_management=firmware sampled_ns={} core_power={:#x} cores_powered={} host_hold={} idle_hold_ms={}", + super::now_ns(), core_power, core_power & 0xf != 0, firmware.hold.active(), + super::power::IDLE_HOLD_MS.load(core::sync::atomic::Ordering::Relaxed))?; + firmware.port.write_power(f) +} + +/// Actual firmware-managed core gating, separate from generic Linux runtime PM. +pub(super) struct PowerView(pub(super) kernel::sync::Arc); + +impl kernel::debugfs::Writer for PowerView { + fn write(&self, f: &mut kernel::fmt::Formatter<'_>) -> kernel::fmt::Result { + let state = self.0.state.lock(); + let Some(firmware) = state.as_deref() else { return writeln!(f, "online=0"); }; + write_power(firmware, f) + } +} + #[kernel::macros::kunit_tests(asahi_g17_reports)] mod tests { use super::*; From e3c8798c137bf697e8efd153af3150b96d1bac78 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 17:39:16 +1100 Subject: [PATCH 252/388] tools/asahi: expose the production fault-stamp helper in the host fixture Re-export the extracted stamp comparison through the fixture module used by the production witness method. Keep the release predicates and negative controls unchanged. Assisted-by: LLM Signed-off-by: Ryan Murray --- tools/asahi/g17-capacity-fault-tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/asahi/g17-capacity-fault-tests.py b/tools/asahi/g17-capacity-fault-tests.py index c47d193a74c55..7c33e92a74812 100644 --- a/tools/asahi/g17-capacity-fault-tests.py +++ b/tools/asahi/g17-capacity-fault-tests.py @@ -63,7 +63,7 @@ def fixture(): } #[derive(Clone, Copy)] struct Cursors { count: u32, producer: u32, consumer: u32 } ''' + proof + r''' -mod retirement { pub(crate) use super::quarantined_retired; } +mod retirement { pub(crate) use super::{quarantined_retired, stamp_newer}; } struct Graph { cursors: Cursors, stamp: u32, fail: bool, resets: usize, owner: u64 } impl Graph { fn set_owner(&mut self, context: &Context) -> Result { if self.fail { Err(EIO) } else { self.owner = context.id; Ok(()) } } From 1fce2b110d1f83ec65a5b04d521226d3b5476dcf Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 17:40:56 +1100 Subject: [PATCH 253/388] docs: record the T6040 supplier-lifetime latch and release qualification Distinguish Neo activity from the T6040 always-powered firmware supplier and document the DT-linked host demand, race/lifetime guards and read-only power/vote evidence. Keep blank-panel release and g3 gain retention as hardware gates. Assisted-by: LLM Signed-off-by: Ryan Murray --- GAPS.md | 45 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/GAPS.md b/GAPS.md index 7944948712418..dcbda1083f7c8 100644 --- a/GAPS.md +++ b/GAPS.md @@ -24,7 +24,7 @@ retains it and adds the qualified shared ports and diagnostics below. | Raw OPP count versus logical states | Firmware consumes 11 logical states | 16 raw GPU OPPs; 11 selectors in each A/B map; both end at raw OPP 15; unused handoff slots must be zero | KUnit now uses valid complete maps and checks the final state's second voltage lane | `g17/calibration.rs`; a count of 15 in an ADT property is not evidence for expanding this firmware layout | | Full clock eligibility | Platform OPPs feed firmware performance tables | DT supplies both cluster voltages and GPU/CS/AFR frequencies; maps and relative-B are checked against the GPU OPP table | Same runtime behavior | `g17/initdata/v25g83.rs`, `g17/fw/initdata/v25g83.rs`; confirm sustained clocks and limiting reasons on hardware | | DVFS ramp / effort | Full effort on publication, cleared after 16 ms of continuous idle | Same `Firmware::note_submission`, direct-port accounting and race-checked idle clear | Same runtime behavior | `g17/mod.rs`, `g17/port.rs`, `g17/power.rs`; opaque firmware power words are not a documented ramp control | -| GPU-driven fabric/memory floor | #440 lifts DCS 4/fabric 3 for GPU demand | T6040 CPU-only demand, gpu_demand=false and no power-state phandle | Port 0d63308820ab enables the shared mechanism with existing T6040 DCS 5/fabric 2 data | clpc.c and T6040 DT; g1/g2 P/C/R/H/I measurements required | +| GPU-driven fabric/memory floor | #440 uses an activity-following graphics PMGR input | T6040 CPU-only demand; g3 port sampled the firmware-lifetime GFX supplier | g4 uses DT-selected publication/continuous-idle demand, preserving DCS 5/fabric 2 and 32 ms CLPC hold | `clpc.c`, `g17/power.rs`, `g17/port.rs`, T6040 DT; blank-panel release and bandwidth retention require hardware qualification | | PMP PMC vote cooperation | PMP/CLPC/AP agents enabled | T6040 static voters omit PMP 0 from pre-working-PMP bring-up | Port 772a9d3104ff adds the existing PMP supplier dependency and voter 0 on T6040 DCS/SOC rails | pmc.c and T6040 DT; live votes, idle release and thermal qualification required | | Idle / off policy | Shared host hold and firmware device-control path | Same host implementation; source documents a 25G83 effective 2 ms off delay and approximately 1 ms wake cost | J616s requests a bounded 16 ms host hold through existing `apple,idle-off-delay-ms`; absent policy retains 2 ms | `g17/power.rs`, `g17/mod.rs`, J616s DT and binding; compare short/long gaps, depth-one latency and idle power | | Fast-die / PMP | Platform-specific firmware power graph | 25G83 AON table and separate power-state target/release (101/96 C) are already populated; DT enables the GFX suppliers | Same runtime behavior | `g17/fw/initdata/v25g83.rs::POWER_STATE_WORDS`, `g17/initdata/v25g83.rs`, J616s DT; verify clocks with PMP running | @@ -58,8 +58,10 @@ for its acknowledgement. It does not encode a DCS/fabric performance request. The GPU firmware owns the PMP's performance demand. Before this series, PMC's static voter list omitted PMP agent 0, and CLPC lacked a GPU power-state input. The two existing ports enable PMP agent 0 on T6040's DCS/SOC rails and make -CLPC request the already qualified DCS 5 / fabric 2 floor while graphics is -powered. They preserve the existing 32 ms memory-floor release. Prove live +CLPC request the already qualified DCS 5 / fabric 2 floor. G3 incorrectly +used the T6040 supplier lifetime as activity. G4 replaces that input with +DT-selected publication/continuous-idle demand and preserves the existing +32 ms memory-floor release. Prove live request, enabled-agent and actual-state agreement; supplier binding alone is not proof of an honoured vote. @@ -120,7 +122,7 @@ no raw lab capture is a public implementation source. | #505 closed-context teardown | `078d744a260c` kills before freeing executed unstamped render graphs, retains firmware-linked work storage, and reuses killed pairs last with QID capacity | Shared safety/retention paths included; compute-fault-driven kill depends on the graph-qualified #496 witness | Closed mixed compute/render faults, acknowledgement ordering, memory pressure and slot/QID reuse; does not claim to cure a live render's first fault | | First fault | Reason and blame alone cannot identify engine progress | At halted state 1, before acknowledgement, g4 logs the blamed pair's stored stamps, free-list balance and up to four commands' pass/cursor snapshots; borrowed pairs are explicitly unavailable | Correlate exact CTS operation and original error with raw event report and snapshots; do not label reason 2 as page fault without semantics | | Raw firmware statistics | Complete 18-word statistics records already have checked ring geometry | Read-only `firmware_reports` exposes last full sample, count and host monotonic time for each role/type. Static-key `asahi_g17_report` copies trace/statistics/event words before slot return | Match payloads to independently decoded achieved-clock/power/residency and freshness/units; legacy statistics tags are a different ABI | -| Idle memory floor | PMGR graphics power is a supplier lifetime, not itself an idle-work witness | Existing policy retained; CLPC status now exposes the sampled graphics status/active input and read-error count alongside CPU requests and rail votes | Long idle with no GPU clients, then load and release, must distinguish GPU power, CPU demand, failed reads and other PMC agents; never infer idle from an open client alone | +| Idle memory floor | Neo graphics PMGR follows activity; T6040 GFX is held for firmware lifetime | g4 replaces T6040 PMGR demand with a DT-linked CLPC request, asserted at publication and released by the existing race-checked 16 ms continuous-idle clear; 32 ms CLPC hold remains | Read-only last vote/reason/time, host request and last submission, safe core power/host hold; require blank-panel quiet release and load/release cycles | Remaining exact facts for telemetry are the 25G83/26A428 tag-to-layout map, field width/signedness/endianness, sample/timebase/wrap and freshness, @@ -138,6 +140,41 @@ feature-enable or buffer-size guess is included in g4. Multi-queue sync needs its publish/GPU-span/event/fence timeline; #504's compute control does not promise to halve that path's latency. +### T6040 idle-floor release correction + +The PMGR DT explicitly documents that GFX cannot power down while firmware +runs. This is a source-established difference from the Neo CLPC input, so +using GFX as T6040 work demand can never establish idle release. G4 removes +that input on T6040 and adds `apple,clpc` from the GPU consumer to the existing +host performance controller. This is a provider relationship, not a feature +flag or user knob. A requested but unavailable provider defers GPU probe. + +Publication asserts the independent, refcounted request through a leaf lock; +o sleeping callback or allocation runs at submission. Both mutex-backed and +off-lock direct paths are covered. The existing effort clear checks in-flight +work and 16 ms continuous idle, withdraws its flag, then rechecks the direct +submission counter. A racing publisher reasserts demand. The CLPC vote keeps +its existing 32 ms hold; CPU arbitration and Neo's PMGR input are unchanged. +Firmware stop and request destruction clear demand. Provider removal cannot +free storage retained by a late consumer, and a device link orders dependencies. + +CLPC status now shows the last requested floor, reason bits and monotonic +time, plus the host request's activity/update sequence. Actual PMC readback +and write errors remain distinct. Read-only `asahi-g17/power` reports the +existing always-accessible core power witness, host hold, effort and last +publication time, without waking firmware. Generic runtime PM may remain +unsupported because autonomous core gating is firmware-managed; no fictitious +runtime-suspend implementation is enabled. Raw stats still do not decode MHz. + +The production CLPC host suite tests an always-powered supplier with absent, +busy and ended GPU demand, 32 ms hold, CPU coexistence, reason/time and legacy +Neo behavior; removing demand or idle release fails its negative controls. +C KUnit checks actual request lifetime and a late consumer after provider +removal. Hardware acceptance requires a locked/blank panel with publications +observably quiet, release after the hold, then three load/idle cycles and +retention of g3's benchmark gains. An active compositor alone proves neither +quiescence nor a latch. Keep failed measurements and rollback candidates. + ## Source and validation boundaries The implementation references are the licensed public Aurora/Asahi kernel and From 7ab77755a0538e685d891449180e82c874587b5a Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 17:41:59 +1100 Subject: [PATCH 254/388] soc: apple: serialize CLPC demand lookup with provider removal Publish and withdraw provider drvdata under a lifetime mutex before devres can free it, taking a request reference while still protected. Avoid nesting the supplier device lock under a probing consumer lock. Submission only uses the independent request allocation and leaf lock. Report a held rail even when another rail has active CPU demand. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/soc/apple/clpc.c | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/drivers/soc/apple/clpc.c b/drivers/soc/apple/clpc.c index 3491030c670dd..90e449d048c97 100644 --- a/drivers/soc/apple/clpc.c +++ b/drivers/soc/apple/clpc.c @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -125,6 +126,9 @@ struct apple_clpc { unsigned long gpu_read_errors; }; +/* Protect publication/removal of provider drvdata, never a submission path. */ +static DEFINE_MUTEX(apple_clpc_clients_lock); + static void apple_clpc_gpu_free(struct kref *ref) { kfree(container_of(ref, struct apple_clpc_gpu, ref)); @@ -178,14 +182,14 @@ int apple_clpc_gpu_get(struct device *dev, struct apple_clpc_gpu **out) put_device(&provider->dev); return -ENOMEM; } - device_lock(&provider->dev); - clpc = device_is_bound(&provider->dev) ? dev_get_drvdata(&provider->dev) : NULL; + mutex_lock(&apple_clpc_clients_lock); + clpc = dev_get_drvdata(&provider->dev); if (clpc && clpc->gpu && refcount_read(&clpc->gpu->ref.refcount) == 1) { kref_get(&clpc->gpu->ref); *out = clpc->gpu; ret = 0; } - device_unlock(&provider->dev); + mutex_unlock(&apple_clpc_clients_lock); put_device(&provider->dev); return ret; } @@ -395,8 +399,9 @@ static void apple_clpc_sample(struct apple_clpc *clpc) now); floor = apple_clpc_floor(clpc, state); - if (!reason && floor) - reason = 16; + for (rail = 0; rail < APPLE_CLPC_RAILS; rail++) + if (state[rail] > demand[rail]) + reason |= 16; clpc->last_floor = floor; clpc->last_reason = reason; clpc->last_vote_ns = ktime_get_mono_fast_ns(); @@ -494,6 +499,9 @@ static void apple_clpc_stop(void *data) { struct apple_clpc *clpc = data; + mutex_lock(&apple_clpc_clients_lock); + dev_set_drvdata(clpc->dev, NULL); + mutex_unlock(&apple_clpc_clients_lock); apple_clpc_halt(clpc); debugfs_remove_recursive(clpc->debugfs); apple_clpc_free_clusters(clpc); @@ -592,7 +600,9 @@ static int apple_clpc_probe(struct platform_device *pdev) if (ret) return ret; + mutex_lock(&apple_clpc_clients_lock); platform_set_drvdata(pdev, clpc); + mutex_unlock(&apple_clpc_clients_lock); apple_clpc_queue(clpc, true); return 0; } From 8d96fbc8ab4458f3cd5d30047588bb3bb6cd2c22 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 17:46:20 +1100 Subject: [PATCH 255/388] soc: apple: wake the CLPC sampler on a cold GPU demand edge An idle vote uses a deferrable timer. A GPU-only publisher can wake another CPU without servicing that timer, so request a nondeferrable immediate sample on the inactive-to-active edge. Keep the submission path atomic: queue work under the request leaf lock, never touch PMC there. Serialize the normal and deferrable sampling passes and collapse their cadence after the edge. Provider removal withdraws the wake pointer under the same request lock before draining work. Late consumers retain only an independent request, never a pointer to freed work storage. KUnit exercises the real cold-demand workqueue wake in addition to the lifetime controls. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/soc/apple/clpc.c | 70 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 4 deletions(-) diff --git a/drivers/soc/apple/clpc.c b/drivers/soc/apple/clpc.c index 90e449d048c97..892b9a8af2646 100644 --- a/drivers/soc/apple/clpc.c +++ b/drivers/soc/apple/clpc.c @@ -92,6 +92,7 @@ struct apple_clpc_gpu { bool active; u64 updated_ns; u64 sequence; + struct delayed_work *wake; }; struct apple_clpc { @@ -102,6 +103,7 @@ struct apple_clpc { /* Samples while a vote is held; idle_work defers to busy CPUs. */ struct delayed_work work; struct delayed_work idle_work; + struct mutex sample_lock; bool stopped; struct apple_clpc_cluster clusters[APPLE_CLPC_MAX_CLUSTERS]; unsigned int nr_clusters; @@ -141,6 +143,11 @@ void apple_clpc_gpu_set_active(struct apple_clpc_gpu *gpu, bool active) if (!gpu) return; spin_lock_irqsave(&gpu->lock, flags); + /* A cold GPU publisher must not wait for a deferrable CPU-idle timer. + * Removal clears wake under this same lock before draining work. + */ + if (active && !gpu->active && gpu->wake) + mod_delayed_work(system_freezable_power_efficient_wq, gpu->wake, 0); gpu->active = active; gpu->updated_ns = ktime_get_mono_fast_ns(); gpu->sequence++; @@ -334,6 +341,10 @@ static void apple_clpc_queue(struct apple_clpc *clpc, bool idle) * stops; with no vote held, sampling can wait for a CPU that wakes up * anyway rather than waking an idle one every period. */ + /* A demand edge can queue work while the deferrable pass is pending. + * Keep one sampling cadence once the serialized pass settles. + */ + cancel_delayed_work(idle ? &clpc->work : &clpc->idle_work); queue_delayed_work(system_freezable_power_efficient_wq, idle ? &clpc->idle_work : &clpc->work, msecs_to_jiffies(APPLE_CLPC_SAMPLE_MS)); @@ -413,14 +424,22 @@ static void apple_clpc_sample(struct apple_clpc *clpc) static void apple_clpc_work(struct work_struct *work) { - apple_clpc_sample(container_of(to_delayed_work(work), struct apple_clpc, - work)); + struct apple_clpc *clpc = container_of(to_delayed_work(work), struct apple_clpc, work); + + mutex_lock(&clpc->sample_lock); + if (!READ_ONCE(clpc->stopped)) + apple_clpc_sample(clpc); + mutex_unlock(&clpc->sample_lock); } static void apple_clpc_idle_work(struct work_struct *work) { - apple_clpc_sample(container_of(to_delayed_work(work), struct apple_clpc, - idle_work)); + struct apple_clpc *clpc = container_of(to_delayed_work(work), struct apple_clpc, idle_work); + + mutex_lock(&clpc->sample_lock); + if (!READ_ONCE(clpc->stopped)) + apple_clpc_sample(clpc); + mutex_unlock(&clpc->sample_lock); } static int apple_clpc_status_show(struct seq_file *s, void *unused) @@ -502,6 +521,13 @@ static void apple_clpc_stop(void *data) mutex_lock(&apple_clpc_clients_lock); dev_set_drvdata(clpc->dev, NULL); mutex_unlock(&apple_clpc_clients_lock); + if (clpc->gpu) { + unsigned long flags; + + spin_lock_irqsave(&clpc->gpu->lock, flags); + clpc->gpu->wake = NULL; + spin_unlock_irqrestore(&clpc->gpu->lock, flags); + } apple_clpc_halt(clpc); debugfs_remove_recursive(clpc->debugfs); apple_clpc_free_clusters(clpc); @@ -589,8 +615,11 @@ static int apple_clpc_probe(struct platform_device *pdev) clpc->mask; apple_clpc_find_clusters(clpc); clpc->next_scan = jiffies + HZ; + mutex_init(&clpc->sample_lock); INIT_DELAYED_WORK(&clpc->work, apple_clpc_work); INIT_DEFERRABLE_WORK(&clpc->idle_work, apple_clpc_idle_work); + if (clpc->gpu) + clpc->gpu->wake = &clpc->work; clpc->debugfs = debugfs_create_dir(dev_name(dev), NULL); debugfs_create_file("status", 0400, clpc->debugfs, clpc, @@ -695,6 +724,7 @@ static const struct apple_clpc_hw apple_clpc_hw_t6040 = { #if IS_BUILTIN(CONFIG_KUNIT) && !defined(MODULE) #include +#include static void apple_clpc_gpu_request_lifetime_test(struct kunit *test) { @@ -736,7 +766,39 @@ static void apple_clpc_gpu_provider_removal_test(struct kunit *test) apple_clpc_gpu_put(gpu); } +struct apple_clpc_wake_test { + struct delayed_work work; + struct completion done; +}; + +static void apple_clpc_wake_complete(struct work_struct *work) +{ + struct apple_clpc_wake_test *wake = container_of(to_delayed_work(work), + struct apple_clpc_wake_test, work); + complete(&wake->done); +} + +static void apple_clpc_gpu_cold_wake_test(struct kunit *test) +{ + struct apple_clpc_gpu *gpu = kzalloc_obj(*gpu); + struct apple_clpc_wake_test wake; + + KUNIT_ASSERT_NOT_NULL(test, gpu); + kref_init(&gpu->ref); + spin_lock_init(&gpu->lock); + INIT_DELAYED_WORK(&wake.work, apple_clpc_wake_complete); + init_completion(&wake.done); + gpu->wake = &wake.work; + apple_clpc_gpu_set_active(gpu, true); + KUNIT_EXPECT_NE(test, wait_for_completion_timeout(&wake.done, HZ), 0ul); + /* Model provider withdrawal before its work storage is destroyed. */ + gpu->wake = NULL; + cancel_delayed_work_sync(&wake.work); + apple_clpc_gpu_put(gpu); +} + static struct kunit_case apple_clpc_cases[] = { + KUNIT_CASE(apple_clpc_gpu_cold_wake_test), KUNIT_CASE(apple_clpc_gpu_request_lifetime_test), KUNIT_CASE(apple_clpc_gpu_provider_removal_test), {} From fa12ae88cc55fbda94349566ca606f78482a6d24 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 17:48:41 +1100 Subject: [PATCH 256/388] drm/asahi: retain memory demand until both firmware processors stop Clear the owned CLPC request after the processor stop sequence, so a slow shutdown does not begin the memory-floor idle hold while firmware can still consume work. Late request destruction remains independently safe and clears the request again without accessing provider storage. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/asahi/g17/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/gpu/drm/asahi/g17/mod.rs b/drivers/gpu/drm/asahi/g17/mod.rs index 0e49420f044a1..c95be04f01222 100644 --- a/drivers/gpu/drm/asahi/g17/mod.rs +++ b/drivers/gpu/drm/asahi/g17/mod.rs @@ -644,11 +644,11 @@ struct Firmware { impl Drop for Firmware { fn drop(&mut self) { - self.port.set_effort_full(false); if let Some(secondary) = self.secondary.as_mut() { secondary.stop(); } self.primary.stop(); + self.port.set_effort_full(false); self.pool.stop_after_firmware(); // RTKit destruction releases the mailbox power references. Clear the bridge while // those references still keep the register block powered. From 2aa5f642c9f7382e46796b524606d0be430baf7c Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Tue, 27 Oct 2026 17:57:00 +1100 Subject: [PATCH 257/388] drm/asahi: keep G17 long jobs alive beside other long jobs A published job's deadline is renewed, up to thirty seconds, while some command retired within the last five seconds: the GPU is then shared, not hung. Two ray traversal dispatches side by side retire nothing until they end, though both progress. Alone each took about 6.3 s; side by side they took 10 to 17 s, past the ten seconds a long job gets without that renewal, so the watchdog failed most of them with ETIMEDOUT and their clients lost the device. Also count the GPU as shared while a long job of another queue runs beside a long job. The thirty-second bound still ends a pair that never finishes. The shared host policy also covers bounded AS-build commands on T6040; its native traversal gate remains unchanged. No firmware watchdog field or numeric host renewal bound changes. This does not classify or cure the reason-2 live-render CTS faults. Link: https://github.com/aurora-silicon/linux/commit/3db24da5c830344e1c1449d38ec9ae0de1396a3d Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/asahi/g17/job.rs | 12 +++--- drivers/gpu/drm/asahi/g17/queue/compute.rs | 10 +++++ drivers/gpu/drm/asahi/g17/runtime/backend.rs | 10 +++++ drivers/gpu/drm/asahi/g17/timeout.rs | 42 ++++++++++++++------ 4 files changed, 57 insertions(+), 17 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/job.rs b/drivers/gpu/drm/asahi/g17/job.rs index 8175c992b8a62..61033aa9e52b4 100644 --- a/drivers/gpu/drm/asahi/g17/job.rs +++ b/drivers/gpu/drm/asahi/g17/job.rs @@ -293,6 +293,8 @@ pub(crate) trait Backend: Send + Sync { fn cancel(&self, packet: Arc); /// Transfer a timeout to an outstanding off-lock graph transaction, if any. fn defer_timeout(&self, packet: &Arc, renewals: u8) -> bool; + /// A long job of another queue is running beside `packet`. + fn long_job_beside(&self, _packet: &Arc) -> bool { false } /// Return true only after taking publication ownership and recording its result. fn try_early(&self, packet: &Arc) -> bool; fn count_backlog(&self, packet: &Packet); @@ -346,11 +348,11 @@ impl sched::JobImpl for Job { ) { return true; } - // Other work retiring means the GPU is shared, not hung. - if super::timeout::renew_shared( - job.packet.is_published(), super::recovery::since_retired(super::now_ns()), - &mut job.shared_renewals, - ) { + // Other work retiring means the GPU is shared, not hung. So does another long job + // running beside a long job, though neither retires until it ends. + let shared = super::timeout::retiring(super::recovery::since_retired(super::now_ns())) + || (job.packet.command.long_job() && job.backend.long_job_beside(&job.packet)); + if super::timeout::renew_shared(job.packet.is_published(), shared, &mut job.shared_renewals) { return true; } let deferred = job.backend.defer_timeout(&job.packet, job.victim_renewals); diff --git a/drivers/gpu/drm/asahi/g17/queue/compute.rs b/drivers/gpu/drm/asahi/g17/queue/compute.rs index 68fd03c42b42b..c40992e50eff6 100644 --- a/drivers/gpu/drm/asahi/g17/queue/compute.rs +++ b/drivers/gpu/drm/asahi/g17/queue/compute.rs @@ -786,6 +786,16 @@ impl Queue { pub(crate) fn in_flight(&self) -> bool { self.active.len != 0 && !self.quarantined } + /// The oldest command is a published long job (ray traversal or AS build) whose + /// completion record the driver has not seen yet. + pub(crate) fn long_job_running(&self) -> bool { + !self.quarantined + && self.active.front().is_some_and(|active| { + active.observed.is_none() + && active.packet.is_published() + && active.packet.command.long_job() + }) + } /// Quarantines without a pending witness no longer describe live pipe order. pub(crate) fn timeout_head(&self) -> Option { if self.quarantined && !self.awaiting_witness { diff --git a/drivers/gpu/drm/asahi/g17/runtime/backend.rs b/drivers/gpu/drm/asahi/g17/runtime/backend.rs index d19f51c08dd29..bf0f989f042a5 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/backend.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/backend.rs @@ -297,6 +297,16 @@ impl job::Backend for Backend { } } + fn long_job_beside(&self, _packet: &Arc) -> bool { + let state = self.shared.state.lock(); + let Some(firmware) = (*state).as_deref() else { + return false; + }; + firmware.queues.compute.iter().flatten() + .filter_map(|entry| entry.queue.as_deref()) + .any(|other| other.owner() != Some(self.owner) && other.long_job_running()) + } + fn defer_timeout(&self, packet: &Arc, renewals: u8) -> bool { if packet.engine() == Engine::Compute { return self.defer_compute_timeout(packet, renewals); diff --git a/drivers/gpu/drm/asahi/g17/timeout.rs b/drivers/gpu/drm/asahi/g17/timeout.rs index 7b4ef4137b795..b83ea5d07f406 100644 --- a/drivers/gpu/drm/asahi/g17/timeout.rs +++ b/drivers/gpu/drm/asahi/g17/timeout.rs @@ -66,13 +66,18 @@ const SHARED_RENEWALS: u8 = 14; /// nothing for this long has stopped. pub(crate) const LIVENESS_NS: u64 = 5_000_000_000; -/// Renews a published job's deadline while some command retired within -/// [`LIVENESS_NS`] (`since_retired`), at most [`SHARED_RENEWALS`] times. -pub(crate) fn renew_shared(published: bool, since_retired: Option, renewals: &mut u8) -> bool { - if !published - || !since_retired.is_some_and(|since| since < LIVENESS_NS) - || *renewals >= SHARED_RENEWALS - { +/// Whether some command retired within [`LIVENESS_NS`] (`since_retired`). +pub(crate) fn retiring(since_retired: Option) -> bool { + since_retired.is_some_and(|since| since < LIVENESS_NS) +} + +/// Renews a published job's deadline while the GPU is `shared`, at most +/// [`SHARED_RENEWALS`] times. It is shared while some command retires +/// ([`retiring`]), or while a long job of another queue runs beside a long job: +/// two ray traversal dispatches of about 6 s each took 10 to 17 s side by side, and +/// neither retires anything until it ends. +pub(crate) fn renew_shared(published: bool, shared: bool, renewals: &mut u8) -> bool { + if !published || !shared || *renewals >= SHARED_RENEWALS { return false; } *renewals += 1; @@ -244,21 +249,34 @@ mod tests { fn a_live_gpu_renews_a_published_job_for_thirty_seconds() { let mut renewals = 0; for poll in 1..=14u8 { - assert!(renew_shared(true, Some(4_900_000_000), &mut renewals)); + assert!(renew_shared(true, retiring(Some(4_900_000_000)), &mut renewals)); assert_eq!(renewals, poll); } - assert!(!renew_shared(true, Some(0), &mut renewals)); + assert!(!renew_shared(true, true, &mut renewals)); assert_eq!(POLL_MS * (u32::from(renewals) + 1), 30_000); } #[test] fn a_stopped_gpu_or_unpublished_job_keeps_the_deadline() { let mut renewals = 0; - assert!(!renew_shared(true, Some(LIVENESS_NS), &mut renewals)); - assert!(!renew_shared(true, None, &mut renewals)); - assert!(!renew_shared(false, Some(0), &mut renewals)); + assert!(!renew_shared(true, retiring(Some(LIVENESS_NS)), &mut renewals)); + assert!(!renew_shared(true, retiring(None), &mut renewals)); + assert!(!renew_shared(false, true, &mut renewals)); assert_eq!(renewals, 0); } #[test] + fn a_long_job_beside_another_shares_the_gpu_without_retirements() { + // Two traversal dispatches side by side: nothing retires, yet both progress. + let mut renewals = 0; + let long_job_beside = true; + let shared = retiring(Some(LIVENESS_NS + 1)) || long_job_beside; + for poll in 1..=14u8 { + assert!(renew_shared(true, shared, &mut renewals)); + assert_eq!(renewals, poll); + } + // The thirty-second bound still ends a pair that never finishes. + assert!(!renew_shared(true, true, &mut renewals)); + } + #[test] fn exhausted_or_invalid_credit_never_wraps() { for value in 4..=u8::MAX { let mut renewals = value; From 9f52d7e79529b4d6aaaec60df0cc64b76b21a930 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 17:57:55 +1100 Subject: [PATCH 258/388] docs: record the shared bounded long-job renewal port Distinguish the host AS-build sharing rule from gated T6040 traversal, firmware watchdog semantics and the unresolved live-render faults. Assisted-by: LLM Signed-off-by: Ryan Murray --- GAPS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/GAPS.md b/GAPS.md index dcbda1083f7c8..ccc3dc4016d59 100644 --- a/GAPS.md +++ b/GAPS.md @@ -120,6 +120,7 @@ no raw lab capture is a public implementation source. | #504 tiling first-kick | `5073eac15192` changes the word only for the 26A428 register program | Included unchanged; 25G83 retains 1 on every kick | No T6040 notification-halving claim; requires 25G83 descriptor/notification facts and controlled qualification | | #496 compute-fault witness | `9a76756bf2d7` uses event 7 words 3/4 as QID/stamp on Neo, with consumed-ring guard | Imported prerequisite, but the existing firmware graph keeps 25G83 event 7 unknown | Exact 25G83 layout, queue identity, stamp semantics and DMA/ring visibility must be established before it can release memory | | #505 closed-context teardown | `078d744a260c` kills before freeing executed unstamped render graphs, retains firmware-linked work storage, and reuses killed pairs last with QID capacity | Shared safety/retention paths included; compute-fault-driven kill depends on the graph-qualified #496 witness | Closed mixed compute/render faults, acknowledgement ordering, memory pressure and slot/QID reuse; does not claim to cure a live render's first fault | +| #499 long-job sharing | `3db24da5c830` recognizes another published, unobserved long compute job as sharing, without changing numeric renewal bounds | Included in the shared host path; T6040 bounded AS-build classification can use it, while native traversal remains gated | KUnit verifies renewals stop at the existing bound; no claim that this cures a firmware reason-2 render fault or makes skipped traversal CTS pass | | First fault | Reason and blame alone cannot identify engine progress | At halted state 1, before acknowledgement, g4 logs the blamed pair's stored stamps, free-list balance and up to four commands' pass/cursor snapshots; borrowed pairs are explicitly unavailable | Correlate exact CTS operation and original error with raw event report and snapshots; do not label reason 2 as page fault without semantics | | Raw firmware statistics | Complete 18-word statistics records already have checked ring geometry | Read-only `firmware_reports` exposes last full sample, count and host monotonic time for each role/type. Static-key `asahi_g17_report` copies trace/statistics/event words before slot return | Match payloads to independently decoded achieved-clock/power/residency and freshness/units; legacy statistics tags are a different ABI | | Idle memory floor | Neo graphics PMGR follows activity; T6040 GFX is held for firmware lifetime | g4 replaces T6040 PMGR demand with a DT-linked CLPC request, asserted at publication and released by the existing race-checked 16 ms continuous-idle clear; 32 ms CLPC hold remains | Read-only last vote/reason/time, host request and last submission, safe core power/host hold; require blank-panel quiet release and load/release cycles | From bc0370654f2c00cd5199c50d36285f24a8da9025 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 18:06:29 +1100 Subject: [PATCH 259/388] hwmon: macsmc: Unlock fan control on firmware that keeps fans in system mode On the M4 Pro MacBook Pro (J616s) both fans start in mode 3, the firmware's own fan policy, and the SMC refuses the manual mode write of the fan_control path with an error, so fan control from Linux did nothing. Public reports for M4 describe the same refusal and the Ftst key that releases the fans to the OS. When userspace first asks for a target on a fan whose mode write is refused while it reads mode 3, set Ftst, wait for the fan to leave system mode and ask again. When the last manual fan goes back to auto, clear Ftst and wait for every fan to return to mode 3. Firmware that takes the mode write directly keeps the mode 0 restore; firmware that refuses it without an Ftst key gets the error as before. Hand every manual fan back on unbind or remove, at shutdown and before suspend, and whenever a write in the sequence fails. Targets are now clamped to the fan's minimum and maximum instead of refused, so a target can never go below the minimum. Everything stays behind the existing fan_control parameter; without it nothing is written. The new KUnit suite models locked and legacy firmware. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/hwmon/Kconfig | 7 +- drivers/hwmon/macsmc-hwmon-test.c | 262 +++++++++++++++++++++++++++++- drivers/hwmon/macsmc-hwmon.c | 261 +++++++++++++++++++++++++---- 3 files changed, 497 insertions(+), 33 deletions(-) diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig index 54eb5745105ff..9b6b0593310da 100644 --- a/drivers/hwmon/Kconfig +++ b/drivers/hwmon/Kconfig @@ -1252,10 +1252,13 @@ config SENSORS_MACSMC_HWMON_KUNIT_TEST depends on SENSORS_MACSMC_HWMON && KUNIT=y default KUNIT_ALL_TESTS help - Unit tests for the channel numbering of the Apple SMC hwmon driver. + Unit tests for the channel numbering and the manual fan control of + the Apple SMC hwmon driver. Thermal zones refer to temperature channels by index, so a sensor that cannot be set up must keep its channel; the tests check that. - No SMC is needed. Only useful for kernel developers. + No SMC is needed: the fan tests model its fan keys and need + MFD_MACSMC_KUNIT_TEST for its read and write stubs. Only useful for + kernel developers. config SENSORS_MAX1111 tristate "Maxim MAX1111 Serial 8-bit ADC chip and compatibles" diff --git a/drivers/hwmon/macsmc-hwmon-test.c b/drivers/hwmon/macsmc-hwmon-test.c index e7239986ce3c9..596e904a6f3e8 100644 --- a/drivers/hwmon/macsmc-hwmon-test.c +++ b/drivers/hwmon/macsmc-hwmon-test.c @@ -1,11 +1,12 @@ // SPDX-License-Identifier: GPL-2.0-only OR MIT /* * KUnit tests for the Apple SMC hwmon driver: channel numbering of the - * temperature sensors, which thermal zones depend on. + * temperature sensors, which thermal zones depend on, and manual fan control. * * Included by macsmc-hwmon.c. */ +#include #include /* What macsmc_hwmon_create_sensor() leaves behind for one node. */ @@ -84,4 +85,261 @@ static struct kunit_suite macsmc_hwmon_test_suite = { .test_cases = macsmc_hwmon_test_cases, }; -kunit_test_suite(macsmc_hwmon_test_suite); +/* + * Manual fan control against a model of the SMC fan keys. In "locked" firmware + * the fans start in system mode (3) and refuse manual mode until Ftst is 1; + * Ftst back to 0 returns them to system mode. "Legacy" firmware takes the mode + * write directly. The model needs the SMC read/write stubs of + * CONFIG_MFD_MACSMC_KUNIT_TEST. + */ +#define FAN_TEST_F1350 0x44a8c000 /* 1350.0f */ +#define FAN_TEST_F2500 0x451c4000 /* 2500.0f */ +#define FAN_TEST_F5777 0x45b48800 /* 5777.0f */ + +struct macsmc_fan_test { + struct apple_smc smc; + struct kunit *test; + struct macsmc_hwmon hwmon; + struct macsmc_hwmon_fan fans[2]; + bool legacy; + u8 mode[2]; + u32 target[2]; + u8 ftst; + int ftst_writes; + int refused; + smc_key fail_key; +}; + +static int fan_test_index(smc_key key) +{ + return ((key >> 16) & 0xff) - '0'; +} + +static int fan_test_read(struct apple_smc *smc, smc_key key, void *buf, size_t size) +{ + struct macsmc_fan_test *t = container_of(smc, struct macsmc_fan_test, smc); + u32 value; + int i = fan_test_index(key); + + switch (key & 0xff00ffff) { + case __SMC_KEY('F', 0, 'M', 'd'): + value = t->mode[i]; + break; + case __SMC_KEY('F', 0, 'M', 'n'): + value = FAN_TEST_F1350; + break; + case __SMC_KEY('F', 0, 'M', 'x'): + value = FAN_TEST_F5777; + break; + case __SMC_KEY('F', 0, 'T', 'g'): + value = t->target[i]; + break; + default: + KUNIT_FAIL(t->test, "unexpected read of %08x", key); + return -EINVAL; + } + memcpy(buf, &value, size); + return size; +} + +static int fan_test_write(struct apple_smc *smc, smc_key key, const void *buf, size_t size) +{ + struct macsmc_fan_test *t = container_of(smc, struct macsmc_fan_test, smc); + u32 value = 0; + int i = fan_test_index(key); + + memcpy(&value, buf, min(size, sizeof(value))); + if (key == t->fail_key) + return -EIO; + + if (key == SMC_KEY(Ftst)) { + t->ftst_writes++; + t->ftst = value; + for (i = 0; i < ARRAY_SIZE(t->mode); i++) { + if (value && t->mode[i] == MACSMC_FAN_MODE_SYSTEM) + t->mode[i] = MACSMC_FAN_MODE_AUTO; + else if (!value) + t->mode[i] = MACSMC_FAN_MODE_SYSTEM; + } + return size; + } + + switch (key & 0xff00ffff) { + case __SMC_KEY('F', 0, 'M', 'd'): + if (!t->legacy && t->mode[i] == MACSMC_FAN_MODE_SYSTEM) { + t->refused++; + return -EIO; + } + t->mode[i] = value; + return size; + case __SMC_KEY('F', 0, 'T', 'g'): + t->target[i] = value; + return size; + } + KUNIT_FAIL(t->test, "unexpected write of %08x", key); + return -EINVAL; +} + +static void fan_test_key(struct macsmc_hwmon_sensor *sensor, const char *key, u32 type) +{ + sensor->macsmc_key = _SMC_KEY(key); + sensor->info.type_code = type; + sensor->info.flags = APPLE_SMC_READABLE | APPLE_SMC_WRITABLE; +} + +static int macsmc_fan_test_init(struct kunit *test) +{ + static const char *const keys[2][5] = { + { "F0Ac", "F0Mn", "F0Mx", "F0Tg", "F0Md" }, + { "F1Ac", "F1Mn", "F1Mx", "F1Tg", "F1Md" }, + }; + const u32 flt = __SMC_KEY('f', 'l', 't', ' '), ui8 = __SMC_KEY('u', 'i', '8', ' '); + struct macsmc_fan_test *t; + int i; + + if (!IS_ENABLED(CONFIG_MFD_MACSMC_KUNIT_TEST)) + kunit_skip(test, "needs CONFIG_MFD_MACSMC_KUNIT_TEST for the SMC stubs"); + + t = kunit_kzalloc(test, sizeof(*t), GFP_KERNEL); + if (!t) + return -ENOMEM; + t->test = test; + for (i = 0; i < 2; i++) { + fan_test_key(&t->fans[i].now, keys[i][0], flt); + fan_test_key(&t->fans[i].min, keys[i][1], flt); + fan_test_key(&t->fans[i].max, keys[i][2], flt); + fan_test_key(&t->fans[i].set, keys[i][3], flt); + fan_test_key(&t->fans[i].mode, keys[i][4], ui8); + t->mode[i] = MACSMC_FAN_MODE_SYSTEM; + } + t->hwmon.smc = &t->smc; + t->hwmon.fan.fans = t->fans; + t->hwmon.fan.count = 2; + t->hwmon.fan.unlock_key = SMC_KEY(Ftst); + mutex_init(&t->hwmon.fan.lock); + test->priv = t; + fan_control = true; + kunit_activate_static_stub(test, apple_smc_read, fan_test_read); + kunit_activate_static_stub(test, apple_smc_write, fan_test_write); + return 0; +} + +static void macsmc_fan_test_exit(struct kunit *test) +{ + fan_control = false; +} + +static void fan_test_released(struct kunit *test, struct macsmc_fan_test *t) +{ + KUNIT_EXPECT_EQ(test, t->ftst, 0); + KUNIT_EXPECT_EQ(test, t->mode[0], MACSMC_FAN_MODE_SYSTEM); + KUNIT_EXPECT_EQ(test, t->mode[1], MACSMC_FAN_MODE_SYSTEM); + KUNIT_EXPECT_EQ(test, t->hwmon.fan.manual, 0); + KUNIT_EXPECT_FALSE(test, t->hwmon.fan.unlocked); + KUNIT_EXPECT_FALSE(test, t->fans[0].manual); +} + +/* Locked firmware: unlock on the first manual request, release on auto. */ +static void macsmc_fan_test_unlock_and_release(struct kunit *test) +{ + struct macsmc_fan_test *t = test->priv; + + KUNIT_ASSERT_EQ(test, macsmc_hwmon_set_fan(&t->hwmon, 0, 2500), 0); + KUNIT_EXPECT_EQ(test, t->ftst, 1); + KUNIT_EXPECT_EQ(test, t->mode[0], MACSMC_FAN_MODE_MANUAL); + KUNIT_EXPECT_EQ(test, t->target[0], FAN_TEST_F2500); + KUNIT_EXPECT_EQ(test, t->hwmon.fan.manual, 1); + + /* A second target needs no new unlock. */ + KUNIT_ASSERT_EQ(test, macsmc_hwmon_set_fan(&t->hwmon, 0, 2500), 0); + KUNIT_EXPECT_EQ(test, t->ftst_writes, 1); + + KUNIT_ASSERT_EQ(test, macsmc_hwmon_set_fan(&t->hwmon, 0, 0), 0); + fan_test_released(test, t); + KUNIT_EXPECT_EQ(test, t->ftst_writes, 2); +} + +/* Targets are clamped to [min, max]; negative ones are refused. */ +static void macsmc_fan_test_clamp(struct kunit *test) +{ + struct macsmc_fan_test *t = test->priv; + + KUNIT_ASSERT_EQ(test, macsmc_hwmon_set_fan(&t->hwmon, 0, 100), 0); + KUNIT_EXPECT_EQ(test, t->target[0], FAN_TEST_F1350); + KUNIT_ASSERT_EQ(test, macsmc_hwmon_set_fan(&t->hwmon, 0, 100000), 0); + KUNIT_EXPECT_EQ(test, t->target[0], FAN_TEST_F5777); + KUNIT_EXPECT_EQ(test, macsmc_hwmon_set_fan(&t->hwmon, 0, -1), -EINVAL); + KUNIT_ASSERT_EQ(test, macsmc_hwmon_set_fan(&t->hwmon, 0, 0), 0); + fan_test_released(test, t); +} + +/* A failed target write hands the fan straight back. */ +static void macsmc_fan_test_target_write_fails(struct kunit *test) +{ + struct macsmc_fan_test *t = test->priv; + + t->fail_key = SMC_KEY(F0Tg); + KUNIT_EXPECT_EQ(test, macsmc_hwmon_set_fan(&t->hwmon, 0, 2500), -EIO); + fan_test_released(test, t); +} + +/* Two manual fans: the unlock is released only with the last one. */ +static void macsmc_fan_test_two_fans(struct kunit *test) +{ + struct macsmc_fan_test *t = test->priv; + + KUNIT_ASSERT_EQ(test, macsmc_hwmon_set_fan(&t->hwmon, 0, 2500), 0); + KUNIT_ASSERT_EQ(test, macsmc_hwmon_set_fan(&t->hwmon, 1, 2500), 0); + KUNIT_EXPECT_EQ(test, t->hwmon.fan.manual, 2); + KUNIT_ASSERT_EQ(test, macsmc_hwmon_set_fan(&t->hwmon, 0, 0), 0); + KUNIT_EXPECT_EQ(test, t->ftst, 1); + KUNIT_EXPECT_EQ(test, t->mode[1], MACSMC_FAN_MODE_MANUAL); + /* Removal, shutdown and suspend all restore through this. */ + macsmc_hwmon_fans_restore(&t->hwmon); + fan_test_released(test, t); +} + +/* Locked firmware without the unlock key: refused, nothing else written. */ +static void macsmc_fan_test_no_unlock_key(struct kunit *test) +{ + struct macsmc_fan_test *t = test->priv; + + t->hwmon.fan.unlock_key = 0; + KUNIT_EXPECT_EQ(test, macsmc_hwmon_set_fan(&t->hwmon, 0, 2500), -EIO); + KUNIT_EXPECT_EQ(test, t->ftst_writes, 0); + KUNIT_EXPECT_EQ(test, t->mode[0], MACSMC_FAN_MODE_SYSTEM); + KUNIT_EXPECT_EQ(test, t->hwmon.fan.manual, 0); +} + +/* Firmware that takes the mode directly keeps the mode 0 restore. */ +static void macsmc_fan_test_legacy(struct kunit *test) +{ + struct macsmc_fan_test *t = test->priv; + + t->legacy = true; + t->mode[0] = MACSMC_FAN_MODE_AUTO; + KUNIT_ASSERT_EQ(test, macsmc_hwmon_set_fan(&t->hwmon, 0, 2500), 0); + KUNIT_EXPECT_EQ(test, t->mode[0], MACSMC_FAN_MODE_MANUAL); + KUNIT_ASSERT_EQ(test, macsmc_hwmon_set_fan(&t->hwmon, 0, 0), 0); + KUNIT_EXPECT_EQ(test, t->mode[0], MACSMC_FAN_MODE_AUTO); + KUNIT_EXPECT_EQ(test, t->ftst_writes, 0); + KUNIT_EXPECT_EQ(test, t->refused, 0); +} + +static struct kunit_case macsmc_fan_test_cases[] = { + KUNIT_CASE(macsmc_fan_test_unlock_and_release), + KUNIT_CASE(macsmc_fan_test_clamp), + KUNIT_CASE(macsmc_fan_test_target_write_fails), + KUNIT_CASE(macsmc_fan_test_two_fans), + KUNIT_CASE(macsmc_fan_test_no_unlock_key), + KUNIT_CASE(macsmc_fan_test_legacy), + {} +}; + +static struct kunit_suite macsmc_fan_test_suite = { + .name = "macsmc-hwmon-fan", + .init = macsmc_fan_test_init, + .exit = macsmc_fan_test_exit, + .test_cases = macsmc_fan_test_cases, +}; +kunit_test_suites(&macsmc_hwmon_test_suite, &macsmc_fan_test_suite); diff --git a/drivers/hwmon/macsmc-hwmon.c b/drivers/hwmon/macsmc-hwmon.c index 06d2c6e2d0ac3..76f07add003d3 100644 --- a/drivers/hwmon/macsmc-hwmon.c +++ b/drivers/hwmon/macsmc-hwmon.c @@ -23,12 +23,15 @@ #include #include #include +#include #include #include #include #include +#include #include #include +#include #include #include #include @@ -189,10 +192,30 @@ struct macsmc_hwmon_sensors { u32 count; }; +/* Fan mode key values */ +#define MACSMC_FAN_MODE_AUTO 0 +#define MACSMC_FAN_MODE_MANUAL 1 +/* The firmware's own fan policy; it refuses a manual mode request. */ +#define MACSMC_FAN_MODE_SYSTEM 3 + +/* How long the firmware gets to leave or re-enter its system mode */ +#define MACSMC_FAN_MODE_TIMEOUT_MS 10000 +#define MACSMC_FAN_MODE_POLL_MS 100 + struct macsmc_hwmon_fans { struct hwmon_channel_info channel_info; struct macsmc_hwmon_fan *fans; u32 count; + /* Serialises manual fan control and its unlock. */ + struct mutex lock; + /* + * Firmware that keeps its fans in system mode releases them to the + * OS only while this key is 1. Zero if the SMC has no such key. + */ + smc_key unlock_key; + bool unlocked; + /* Fans in manual mode */ + u32 manual; }; struct macsmc_hwmon { @@ -466,57 +489,199 @@ static int macsmc_hwmon_read_fan(struct macsmc_hwmon *hwmon, u32 attr, int chan, } } -static int macsmc_hwmon_write_fan(struct device *dev, u32 attr, int channel, - long val) +static int macsmc_hwmon_fan_read_mode(struct macsmc_hwmon *hwmon, + struct macsmc_hwmon_fan *fan, u8 *mode) { - struct macsmc_hwmon *hwmon = dev_get_drvdata(dev); + return apple_smc_read_u8(hwmon->smc, fan->mode.macsmc_key, mode); +} + +/* + * Hand the fans back to the firmware once no fan is manual: clear the unlock + * key and wait for every fan to return to system mode. Called with the fan + * lock held. + */ +static int macsmc_hwmon_fans_relock(struct macsmc_hwmon *hwmon) +{ + unsigned long timeout; + u32 i; + u8 mode; + int ret; + + ret = apple_smc_write_u8(hwmon->smc, hwmon->fan.unlock_key, 0); + if (ret < 0) { + dev_err(hwmon->dev, "cannot hand the fans back to the SMC (%d)\n", ret); + return ret; + } + hwmon->fan.unlocked = false; + + timeout = jiffies + msecs_to_jiffies(MACSMC_FAN_MODE_TIMEOUT_MS); + for (i = 0; i < hwmon->fan.count; i++) { + struct macsmc_hwmon_fan *fan = &hwmon->fan.fans[i]; + + if (!fan->mode.macsmc_key) + continue; + for (;;) { + ret = macsmc_hwmon_fan_read_mode(hwmon, fan, &mode); + if (ret) + return ret; + if (mode == MACSMC_FAN_MODE_SYSTEM) + break; + if (time_after(jiffies, timeout)) { + dev_warn(hwmon->dev, "%s stays in mode %u after release\n", + fan->label, mode); + return -ETIMEDOUT; + } + msleep(MACSMC_FAN_MODE_POLL_MS); + } + } + + return 0; +} + +/* Return a fan to firmware control. Called with the fan lock held. */ +static int macsmc_hwmon_fan_auto(struct macsmc_hwmon *hwmon, + struct macsmc_hwmon_fan *fan) +{ + int ret, relock; + + if (!fan->manual) + return 0; + + ret = macsmc_hwmon_write_key(hwmon->smc, &fan->mode, MACSMC_FAN_MODE_AUTO); + fan->manual = false; + hwmon->fan.manual--; + + if (hwmon->fan.unlocked && !hwmon->fan.manual) { + relock = macsmc_hwmon_fans_relock(hwmon); + if (relock) + return relock; + } + + return ret < 0 ? ret : 0; +} + +/* + * Put a fan in manual mode. Firmware in system mode refuses that until the + * unlock key is set; it then leaves system mode and accepts the request. + * Called with the fan lock held. + */ +static int macsmc_hwmon_fan_manual(struct macsmc_hwmon *hwmon, + struct macsmc_hwmon_fan *fan) +{ + unsigned long timeout; + u8 mode; + int ret; + + if (fan->manual) + return 0; + + ret = macsmc_hwmon_write_key(hwmon->smc, &fan->mode, MACSMC_FAN_MODE_MANUAL); + if (ret < 0 && hwmon->fan.unlock_key && !hwmon->fan.unlocked && + !macsmc_hwmon_fan_read_mode(hwmon, fan, &mode) && + mode == MACSMC_FAN_MODE_SYSTEM) { + ret = apple_smc_write_u8(hwmon->smc, hwmon->fan.unlock_key, 1); + if (ret < 0) { + /* The key may have been set anyway; clear it. */ + apple_smc_write_u8(hwmon->smc, hwmon->fan.unlock_key, 0); + return ret; + } + hwmon->fan.unlocked = true; + + /* Ask again once the fan has left system mode. */ + timeout = jiffies + msecs_to_jiffies(MACSMC_FAN_MODE_TIMEOUT_MS); + for (;;) { + ret = macsmc_hwmon_fan_read_mode(hwmon, fan, &mode); + if (!ret && mode == MACSMC_FAN_MODE_SYSTEM) + ret = -EBUSY; + else if (!ret) + ret = macsmc_hwmon_write_key(hwmon->smc, &fan->mode, + MACSMC_FAN_MODE_MANUAL); + if (ret >= 0 || time_after(jiffies, timeout)) + break; + msleep(MACSMC_FAN_MODE_POLL_MS); + } + if (ret < 0) { + dev_err(hwmon->dev, "%s refused manual mode after unlock (%d)\n", + fan->label, ret); + if (!hwmon->fan.manual) + macsmc_hwmon_fans_relock(hwmon); + return ret; + } + } + if (ret < 0) + return ret; + + fan->manual = true; + hwmon->fan.manual++; + return 0; +} + +static int macsmc_hwmon_set_fan(struct macsmc_hwmon *hwmon, int channel, long val) +{ + struct macsmc_hwmon_fan *fan = &hwmon->fan.fans[channel]; long min, max; int ret; - if (!fan_control || hwmon->fan.fans[channel].mode.macsmc_key == 0) + if (!fan_control || fan->mode.macsmc_key == 0) return -EOPNOTSUPP; + if (val < 0) + return -EINVAL; + + guard(mutex)(&hwmon->fan.lock); + + /* 0 hands the fan back to the firmware. */ + if (!val) + return macsmc_hwmon_fan_auto(hwmon, fan); /* * The SMC does no sanity checks on requested fan speeds, so we need to. */ - ret = macsmc_hwmon_read_key(hwmon->smc, &hwmon->fan.fans[channel].min, - 1, &min); + ret = macsmc_hwmon_read_key(hwmon->smc, &fan->min, 1, &min); if (ret) return ret; - ret = macsmc_hwmon_read_key(hwmon->smc, &hwmon->fan.fans[channel].max, - 1, &max); + ret = macsmc_hwmon_read_key(hwmon->smc, &fan->max, 1, &max); if (ret) return ret; - if (val >= min && val <= max) { - if (!hwmon->fan.fans[channel].manual) { - /* Write 1 to mode key for manual control */ - ret = macsmc_hwmon_write_key(hwmon->smc, - &hwmon->fan.fans[channel].mode, 1); - if (ret < 0) - return ret; + if (min <= 0 || max < min) + return -EIO; + val = clamp(val, min, max); - hwmon->fan.fans[channel].manual = true; - } - return macsmc_hwmon_write_key(hwmon->smc, - &hwmon->fan.fans[channel].set, val); - } else if (!val) { - if (hwmon->fan.fans[channel].manual) { - ret = macsmc_hwmon_write_key(hwmon->smc, - &hwmon->fan.fans[channel].mode, 0); - if (ret < 0) - return ret; + ret = macsmc_hwmon_fan_manual(hwmon, fan); + if (ret) + return ret; - hwmon->fan.fans[channel].manual = false; - } - } else { - return -EINVAL; + ret = macsmc_hwmon_write_key(hwmon->smc, &fan->set, val); + if (ret < 0) { + macsmc_hwmon_fan_auto(hwmon, fan); + return ret; } return 0; } +static int macsmc_hwmon_write_fan(struct device *dev, u32 attr, int channel, + long val) +{ + return macsmc_hwmon_set_fan(dev_get_drvdata(dev), channel, val); +} + +/* Hand every manual fan back to the firmware. */ +static void macsmc_hwmon_fans_restore(struct macsmc_hwmon *hwmon) +{ + u32 i; + + guard(mutex)(&hwmon->fan.lock); + for (i = 0; i < hwmon->fan.count; i++) + macsmc_hwmon_fan_auto(hwmon, &hwmon->fan.fans[i]); +} + +static void macsmc_hwmon_fans_restore_action(void *data) +{ + macsmc_hwmon_fans_restore(data); +} + static int macsmc_hwmon_read(struct device *dev, enum hwmon_sensor_types type, u32 attr, int channel, long *val) { @@ -1343,6 +1508,11 @@ static int macsmc_hwmon_probe(struct platform_device *pdev) hwmon->dev = &pdev->dev; hwmon->smc = smc; hwmon->register_thermal_zones = true; + platform_set_drvdata(pdev, hwmon); + + ret = devm_mutex_init(&pdev->dev, &hwmon->fan.lock); + if (ret) + return ret; ret = macsmc_hwmon_populate_sensors(hwmon, hwmon->dev->of_node); if (ret) { @@ -1372,6 +1542,21 @@ static int macsmc_hwmon_probe(struct platform_device *pdev) if (ret) return ret; + if (hwmon->fan.count) { + struct apple_smc_key_info info; + + if (!apple_smc_get_key_info(smc, SMC_KEY(Ftst), &info) && + info.type_code == __SMC_KEY('u', 'i', '8', ' ') && + (info.flags & APPLE_SMC_WRITABLE)) + hwmon->fan.unlock_key = SMC_KEY(Ftst); + + /* Runs after the hwmon device is gone, so no write races it. */ + ret = devm_add_action_or_reset(&pdev->dev, + macsmc_hwmon_fans_restore_action, hwmon); + if (ret) + return ret; + } + hwmon->chip_info.ops = &macsmc_hwmon_ops; hwmon->chip_info.info = (const struct hwmon_channel_info *const *)&hwmon->channel_infos; @@ -1407,11 +1592,29 @@ static const struct of_device_id macsmc_hwmon_of_table[] = { }; MODULE_DEVICE_TABLE(of, macsmc_hwmon_of_table); +static void macsmc_hwmon_shutdown(struct platform_device *pdev) +{ + struct macsmc_hwmon *hwmon = platform_get_drvdata(pdev); + + if (hwmon) + macsmc_hwmon_fans_restore(hwmon); +} + +static int macsmc_hwmon_suspend(struct device *dev) +{ + macsmc_hwmon_fans_restore(dev_get_drvdata(dev)); + return 0; +} + +static DEFINE_SIMPLE_DEV_PM_OPS(macsmc_hwmon_pm_ops, macsmc_hwmon_suspend, NULL); + static struct platform_driver macsmc_hwmon_driver = { .probe = macsmc_hwmon_probe, + .shutdown = macsmc_hwmon_shutdown, .driver = { .name = "macsmc-hwmon", .of_match_table = macsmc_hwmon_of_table, + .pm = pm_sleep_ptr(&macsmc_hwmon_pm_ops), }, }; module_platform_driver(macsmc_hwmon_driver); From 7a9ebf019036a7fae177e3cc626625e903f4332d Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Tue, 27 Oct 2026 03:27:37 -0400 Subject: [PATCH 260/388] drm/asahi: kill a G17 context at most once A closed context qualifies for a render-only kill while one of its pairs holds executed but unstamped commands and no compute queue still runs it, switches away from it or killed it. A context killed through its compute queue normally leaves that state at once, since the acknowledged kill releases the pair in the same service pass, but only that timing kept the render-only path from killing it again: the compute queue's own record of the kill ends when the queue is rebound. A second kill of a killed context is harmful. In a diagnostic run that held the pair's release back for 100 ms after the kill, 31 contexts were killed twice, 0.1 to 2.5 ms apart, and twice the firmware recovered the GPU 0.3 ms after acknowledging the second kill, on a page fault in the killed pair's own tiling queue. The 35 contexts killed once in that run caused no recovery. Record on the context that a kill was published, by either path, and never publish a render-only kill for a context that has one. --- drivers/gpu/drm/asahi/g17/context.rs | 14 ++++++++ .../gpu/drm/asahi/g17/runtime/compute_exit.rs | 35 +++++++++++++++---- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/context.rs b/drivers/gpu/drm/asahi/g17/context.rs index ba74140d4f243..0b3bd97fb42fe 100644 --- a/drivers/gpu/drm/asahi/g17/context.rs +++ b/drivers/gpu/drm/asahi/g17/context.rs @@ -114,6 +114,9 @@ pub(crate) struct Context { scheduler: Arc, scheduler_generation: u32, published: AtomicBool, + /// A kill of this context was published, through a compute queue or on its render pairs' + /// behalf. A context is killed at most once. + kill_published: AtomicBool, /// Advisory only: publication still checks the physical binding under the /// device mutex. False skips an inline attempt known to need admission. compute_binding_hint: AtomicBool, @@ -197,6 +200,7 @@ impl Context { scheduler, scheduler_generation, published: AtomicBool::new(false), + kill_published: AtomicBool::new(false), compute_binding_hint: AtomicBool::new(false), render_seen: AtomicBool::new(false), qos, @@ -316,6 +320,16 @@ impl Context { Ok(head) } + /// Record that a kill of this context was published (see `kill_published`). + pub(crate) fn note_kill_published(&self) { + self.kill_published.store(true, Ordering::Release); + } + + /// A kill of this context was published. + pub(crate) fn kill_published(&self) -> bool { + self.kill_published.load(Ordering::Acquire) + } + pub(crate) fn is_published(&self) -> bool { self.published.load(Ordering::Acquire) } diff --git a/drivers/gpu/drm/asahi/g17/runtime/compute_exit.rs b/drivers/gpu/drm/asahi/g17/runtime/compute_exit.rs index cd7f2999d0312..41960369b04a3 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/compute_exit.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/compute_exit.rs @@ -52,6 +52,16 @@ fn render_kill_target(listed: u128, own: u128) -> Option { (candidates != 0).then(|| candidates.trailing_zeros() as u8) } +/// A closed context may be killed on its render pairs' behalf once it has been closed for +/// [`RENDER_KILL_DELAY_NS`], it was never killed, a pair of it holds executed but unstamped +/// commands and no compute queue still runs it, switches away from it or killed it. A second kill +/// of a killed context made the firmware fault on the killed pair's own tiling queue within a +/// millisecond of acknowledging it: with the pair's release held back, a context killed through +/// its compute queue still looked like a render-only candidate. +fn render_kill_eligible(age: u64, killed: bool, executed: bool, compute_clear: bool) -> bool { + age >= RENDER_KILL_DELAY_NS && !killed && executed && compute_clear +} + /// The one admission owner of every pair a kill holds, if there is one. fn hold_owner(owners: impl Iterator>) -> Option { let mut owner = None; @@ -296,6 +306,7 @@ impl Firmware { PRESSED_KILL_NS.store(now.max(1), Ordering::Relaxed); } queue.record_exit(cookie, now); + context.note_kill_published(); crate::g17::trace::teardown(3, cookie, u64::from(context.id()), hold | u64::from(beside)); fence(Ordering::SeqCst); if let Err(error) = queue.announce_exit(now, || self.primary.notify(MSG_CONTROL_NOTIFY)) { @@ -315,12 +326,14 @@ impl Firmware { /// yet those pairs are released only after one: without this the context, its pairs and /// its memory stay until the processors stop. fn render_kill_candidate(&self, index: usize, context: &Arc, age: u64) -> bool { - age >= RENDER_KILL_DELAY_NS - && !self.queues.teardown.render_kill_published(index) - && self.queues.render.executed_slots(context) != 0 - && self.queues.compute.iter().flatten().filter_map(|entry| entry.queue.as_deref()).all(|queue| { + render_kill_eligible( + age, + self.queues.teardown.render_kill_published(index) || context.kill_published(), + self.queues.render.executed_slots(context) != 0, + self.queues.compute.iter().flatten().filter_map(|entry| entry.queue.as_deref()).all(|queue| { queue.idle_for_context(context) && !queue.switching_from(context) && !queue.exit_published(context) - }) + }), + ) } /// Kill a render-only candidate (`render_kill_candidate`). The rules of a failed @@ -393,6 +406,7 @@ impl Firmware { // Publication is irrevocable. The held pairs stay held until this context's // scheduler release is consumed (`release_killed_slots` at detach). self.queues.teardown.record_render_kill(index, cookie, now)?; + context.note_kill_published(); record_kill_wait(self.queues.teardown.take_kill_wait(index, now)); if beside { *pressed = false; @@ -491,7 +505,16 @@ impl Firmware { #[kernel::macros::kunit_tests(asahi_g17_render_kill_target)] mod render_kill_target_tests { - use super::{hold_owner, render_kill_target}; + use super::{hold_owner, render_kill_eligible, render_kill_target, RENDER_KILL_DELAY_NS}; + + #[test] + fn a_killed_context_is_never_killed_again() { + assert!(render_kill_eligible(RENDER_KILL_DELAY_NS, false, true, true)); + assert!(!render_kill_eligible(RENDER_KILL_DELAY_NS, true, true, true)); + assert!(!render_kill_eligible(RENDER_KILL_DELAY_NS - 1, false, true, true)); + assert!(!render_kill_eligible(RENDER_KILL_DELAY_NS, false, false, true)); + assert!(!render_kill_eligible(RENDER_KILL_DELAY_NS, false, true, false)); + } #[test] fn a_render_kill_holds_pairs_of_one_owner_only() { From 8bce44c04ac01e54b8425c881ba9cf3155fd23e5 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 18:35:35 +1100 Subject: [PATCH 261/388] drm/asahi: freeze render progress before acknowledging G17 recovery Resolve slot attribution while firmware is halted in state 1, alongside its key and monitor snapshot. Previously classify() read pass timestamps after the 3->0 acknowledgement, when resumed work could change them and make a later-started peer appear responsible for the original fault. Keep the captured attribution through the handshake. Unknown 25G83 slot keys still preserve firmware blame; no new retirement or release witness is introduced. This fixes the shared sampling race, not the unqualified T6040 reason-2 first faults. KUnit drives the real service() handshake and rejects a pass read after the first acknowledgement. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/asahi/g17/recovery.rs | 109 +++++++++++++++++++++++++- 1 file changed, 106 insertions(+), 3 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/recovery.rs b/drivers/gpu/drm/asahi/g17/recovery.rs index 7787485fa713a..dcd8d59e4c412 100644 --- a/drivers/gpu/drm/asahi/g17/recovery.rs +++ b/drivers/gpu/drm/asahi/g17/recovery.rs @@ -250,6 +250,11 @@ pub(super) fn service( if let Some(qid) = request.blamed { host.report_blamed_render(qid); } + // The keys, monitor and pass-start evidence must describe the same halted + // instant. Firmware may update pass timestamps as soon as state 3 is + // acknowledged; sampling them during classification can blame a bystander. + let attribution = sources.attribute(request.blamed, + |qid, stamp| host.render_pass_started(qid, stamp)); let target_error = host.publish_recovery_targets(); host.transition(1, 2)?; let mut recovered = false; @@ -280,7 +285,7 @@ pub(super) fn service( host.recovery_mut().pending = None; let (listed, exact, fallback) = (entries.listed, entries.exact, entries.fallback); - let (cancelled, blame) = classify(host, entries, request.blamed, &mut sources); + let (cancelled, blame) = classify(host, entries, request.blamed, attribution); if listed != 0 { pr_warn!( "G17: GPU recovery: {} commands were in flight ({} matched by stamp, {} by queue); they fail instead of completing\n", @@ -335,10 +340,9 @@ fn read_entries(host: &mut H) -> Entries { /// Also returns whether the firmware's blamed QID is still guilty after attribution: a blamed /// render that had not started while others had (or with no culprit at all) is innocent, and its /// work is spared like that of every other innocent VM instead of failing with EIO. -fn classify(queues: &mut Q, entries: Entries, blamed: Option, sources: &mut Sources) -> (u128, bool) { +fn classify(queues: &mut Q, entries: Entries, blamed: Option, attribution: Attribution) -> (u128, bool) { let mut guilty_qids = entries.guilty | blamed.map_or(0, |qid| 1u128 << qid); let cancelled = entries.cancelled; - let attribution = sources.attribute(blamed, |qid, stamp| queues.render_pass_started(qid, stamp)); match attribution { Attribution::Firmware => {} Attribution::Queues(qids) => guilty_qids = qids, @@ -398,3 +402,102 @@ fn classify(queues: &mut Q, entries: Entries, blamed: Option, sou }); (cancelled & !(guilty_qids & !collateral) & spared, blame) } + + +#[kernel::macros::kunit_tests(asahi_g17_recovery_snapshot)] +mod tests { + use super::*; + use core::cell::Cell; + + struct Halted { + recovery: State, + state: Cell, + reads: Cell, + known_keys: bool, + blamed: bool, + } + + impl Halted { + fn new(known_keys: bool) -> Self { + let mut recovery = State::new(); + assert!(recovery.request(0, 38).unwrap()); + Self { recovery, state: Cell::new(1), reads: Cell::new(0), + known_keys, blamed: false } + } + } + + impl Memory for Halted { + fn recovery_state(&self) -> Result { Ok(self.state.get()) } + fn host_recovery(&self) -> Result { Ok(0) } + fn fault_sources(&self) -> Result { + assert_eq!(self.state.get(), 1); + let mut sources = Sources::sample(3, |slot| { + Some((if self.known_keys { ((38 + slot as u64) << 40) | 1 } else { 0 }, 0)) + }); + sources.reason = Some(2); + Ok(sources) + } + fn transition(&self, expected: u32, next: u32) -> Result { + assert_eq!(self.state.get(), expected); + self.state.set(next); + Ok(()) + } + fn clear_timestamps(&self) -> Result { panic!("active render scheduler") } + fn entry(&self, _index: usize) -> Result<(u32, u32)> { + assert_eq!(self.state.get(), 3); + Ok((0, 0)) + } + fn wait_tick(&self) { + assert_eq!(self.state.get(), 2); + self.state.set(3); + } + } + + impl Queues for Halted { + fn render_scheduler_active(&self) -> bool { true } + fn render_pass_started(&self, qid: u8, _stamp: u64) -> Option { + // This is the regression oracle: state 0/2/3 is too late. A + // resumed peer may have started after the fault we are attributing. + assert_eq!(self.state.get(), 1); + self.reads.set(self.reads.get() + 1); + Some(qid == 39) + } + fn publish_recovery_targets(&self) -> Option { None } + fn oldest_spared(&self, _qid: u8) -> Option { None } + fn visit_owners(&self, _qid: u8, _visit: &mut dyn FnMut(&Arc)) {} + fn classify_work(&mut self, _class: &dyn Fn(&Arc) -> Class) -> u128 { 0 } + fn note_interrupted(&mut self, _qid: u8, _stamp: u32) -> Interrupted { Interrupted::None } + fn settle_blamed_render(&mut self, qid: u8, _error: Error) { + assert_eq!(qid, 38); + self.blamed = true; + } + fn settle_cancelled_renders(&mut self, _qids: u128) {} + fn rescan_compute(&mut self) -> Result { Ok(()) } + fn release_retired_compute(&mut self) {} + fn announce_withheld(&mut self) {} + } + + impl Host for Halted { + fn recovery(&self) -> &State { &self.recovery } + fn recovery_mut(&mut self) -> &mut State { &mut self.recovery } + } + + #[test] + fn pass_evidence_is_frozen_before_the_first_acknowledgement() { + let mut host = Halted::new(true); + service(&mut host, || false).unwrap(); + assert_eq!(host.reads.get(), 2); + assert_eq!(host.state.get(), 0); + assert_eq!(host.recovery.generation(), 1); + assert!(!host.blamed); + } + + #[test] + fn unknown_25g83_keys_still_preserve_firmware_blame() { + let mut host = Halted::new(false); + service(&mut host, || false).unwrap(); + assert_eq!(host.reads.get(), 0); + assert!(host.blamed); + assert_eq!(host.state.get(), 0); + } +} From 0d753834db34dc82c6f4e7198cc72d48e8e87673 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 18:37:11 +1100 Subject: [PATCH 262/388] drm/asahi: record halted tiler capacity alongside G17 recovery progress Record the declared TVB state, block inventory and high-water values, plus the blamed pair's bounded render geometry/GTP layout and pending parameter-buffer growth. All reads run in the existing state-1 snapshot; there is no allocation, MMIO access or high-water reset. These observations distinguish a full tiler, a delayed grow request and an unstamped unstarted pass in the large point/barrier CTS cases. They are diagnostics, not a timeout extension or a claim that reason 2 means TVB exhaustion. Keep raw captures in private qualification evidence. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/asahi/g17/buffer.rs | 16 ++++++++++++++++ drivers/gpu/drm/asahi/g17/queue/render.rs | 6 ++++++ drivers/gpu/drm/asahi/g17/runtime/render/grow.rs | 4 ++-- .../gpu/drm/asahi/g17/runtime/render/recovery.rs | 2 ++ 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/buffer.rs b/drivers/gpu/drm/asahi/g17/buffer.rs index 8f3a03fbb8dba..fd6460db2d1fb 100644 --- a/drivers/gpu/drm/asahi/g17/buffer.rs +++ b/drivers/gpu/drm/asahi/g17/buffer.rs @@ -677,6 +677,22 @@ impl Manager { pub(crate) fn blocks(&self) -> usize { self.tvb.blocks() } + /// Read-only diagnostic while firmware is halted. In particular this + /// does not reset the high-water counter as automatic_target() does. + pub(crate) fn report_recovery(&self) { + let read = |offset| -> Result { + Ok(self.graph.word(offset)?.load(Ordering::Relaxed)) + }; + pr_warn!("G17: recovery tvb id={} blocks={} cursors={:?} pages={:?} capacity={:?} producer={:?} consumer={:?} max_pages={:?} reset={:?} initialized={}\n", + self.buffer_id, self.blocks(), self.cursors(), + read(core::mem::offset_of!(fw::State, pages)), + read(core::mem::offset_of!(fw::State, block_capacity)), + read(core::mem::offset_of!(fw::State, producer)), + read(core::mem::offset_of!(fw::State, consumer)), + read(fw::CONTROL + core::mem::offset_of!(fw::Control, max_pages)), + read(fw::CONTROL + core::mem::offset_of!(fw::Control, reset)), + self.initialized); + } pub(crate) fn state_va(&self) -> u64 { self.graph.gpu_va() } diff --git a/drivers/gpu/drm/asahi/g17/queue/render.rs b/drivers/gpu/drm/asahi/g17/queue/render.rs index 921f6eb3fb6eb..e6123d39e4957 100644 --- a/drivers/gpu/drm/asahi/g17/queue/render.rs +++ b/drivers/gpu/drm/asahi/g17/queue/render.rs @@ -1039,7 +1039,13 @@ impl Pair { pr_warn!("G17: recovery render slot={} qids={:?} active={} prepared={} quarantined={} stamps={:?} freelist={:?}\n", self.slot, self.qids(), self.active.len(), self.prepared.is_some(), self.quarantined, stamps, self.free_list.counters()); + self.memory.manager.report_recovery(); for active in self.active.iter().take(4) { + if let Validated::Render { pass, .. } = &active.packet.command { + pr_warn!("G17: recovery render geometry ordinal={} width={} height={} layers={} layout={:?}\n", + active.ticket.ordinal, pass.width, pass.height, pass.layers, + target::Layout::new(pass, self.clusters, self.variant.programs)); + } pr_warn!("G17: recovery render ordinal={} published={} ctx={} seq={} snapshot={:?}\n", active.ticket.ordinal, active.outer_published, active.packet.context.id(), active.packet.order.sequence, observation::snapshot(&self.memory, active.ticket.ordinal)); diff --git a/drivers/gpu/drm/asahi/g17/runtime/render/grow.rs b/drivers/gpu/drm/asahi/g17/runtime/render/grow.rs index 20569af84bc7b..277534328406e 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render/grow.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render/grow.rs @@ -14,8 +14,8 @@ use kernel::prelude::*; const RETRY_LIMIT: u16 = 1000; pub(super) struct Grow { - requests: KVec, - active: Option, + pub(super) requests: KVec, + pub(super) active: Option, pub(super) reply: Option<(TvbGrowRequest, bool, u16)>, release_attempts: u16, } diff --git a/drivers/gpu/drm/asahi/g17/runtime/render/recovery.rs b/drivers/gpu/drm/asahi/g17/runtime/render/recovery.rs index 20f537ab69598..7a6d987591c76 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render/recovery.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render/recovery.rs @@ -7,6 +7,8 @@ use kernel::prelude::*; impl Registry { pub(in crate::g17::runtime) fn report_render_recovery(&self, qid: u8, port: &crate::g17::port::Port) { + pr_warn!("G17: recovery grow pending={} active={:?} reply={:?}\n", + self.render.grow.requests.len(), self.render.grow.active, self.render.grow.reply); for entry in self.render.entries.iter().flatten().filter(|entry| { entry.reservation.ids.iter().any(|id| id.qid() == qid) }) { From 9a9ba9ba3cead0c0e3bb56d370dce474f943ee07 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 23:30:09 +1100 Subject: [PATCH 263/388] dt-bindings: spi: apple: Add the T6041 SPI controller The M4 Pro (T6040/T6041) SPI controllers are register compatible with T8103, like the T6020 and T8140 ones. Assisted-by: LLM Signed-off-by: Ryan Murray --- Documentation/devicetree/bindings/spi/apple,spi.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/Documentation/devicetree/bindings/spi/apple,spi.yaml b/Documentation/devicetree/bindings/spi/apple,spi.yaml index 4b30265686fa1..be47c5f9ff075 100644 --- a/Documentation/devicetree/bindings/spi/apple,spi.yaml +++ b/Documentation/devicetree/bindings/spi/apple,spi.yaml @@ -18,6 +18,7 @@ properties: - items: - enum: - apple,t6020-spi + - apple,t6041-spi - apple,t8140-spi - const: apple,t8103-spi - items: From d5235866cdcd53ba6a5f5ca4f4bc0cd8230adb7c Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 23:30:09 +1100 Subject: [PATCH 264/388] dt-bindings: input: apple,mesa-fingerprint: Name the signed root key Sensors whose enclave session needs a signed root key (the J700 and J616s part, 0x3356) take it as per-device factory data, like the calibration. Let firmware-name carry it as an optional second entry so the board description names it. Assisted-by: LLM Signed-off-by: Ryan Murray --- .../bindings/input/apple,mesa-fingerprint.yaml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Documentation/devicetree/bindings/input/apple,mesa-fingerprint.yaml b/Documentation/devicetree/bindings/input/apple,mesa-fingerprint.yaml index 53b304bcec7ff..631ba47a5a3ca 100644 --- a/Documentation/devicetree/bindings/input/apple,mesa-fingerprint.yaml +++ b/Documentation/devicetree/bindings/input/apple,mesa-fingerprint.yaml @@ -57,11 +57,17 @@ properties: description: Rail selector words, in power-on order. firmware-name: - maxItems: 1 + minItems: 1 + items: + - description: + Calibration blob for this sensor. + - description: + Signed root-key container, for sensors whose enclave session needs + one. description: - Calibration blob for this sensor. It is per-device factory data written - at manufacture, so it is neither redistributable nor derivable from the - compatible; the board description has to name it. + Per-device factory data written at manufacture, so neither + redistributable nor derivable from the compatible; the board + description has to name it. spi-cpha: true From c737a230556b23e043a26100e3b785db28f99e2b Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 23:30:10 +1100 Subject: [PATCH 265/388] soc: apple: sep: Take the sensor root key name from the device tree The signed root-key container is per-device data, but its name was fixed to J700's. Read it from the sensor node's second firmware-name entry and keep J700's name when the board gives only the calibration, so the J700 device tree and its installed file keep working unchanged. Name the file in the error when it cannot be loaded. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/soc/apple/sbio.rs | 9 +++++---- drivers/soc/apple/sensor.rs | 15 +++++++++++++-- drivers/soc/apple/sensor_shim.c | 24 ++++++++++++++---------- drivers/soc/apple/sep.rs | 3 +++ drivers/soc/apple/shim.h | 2 +- 5 files changed, 36 insertions(+), 17 deletions(-) diff --git a/drivers/soc/apple/sbio.rs b/drivers/soc/apple/sbio.rs index 88be672817b1c..5a81a370dda38 100644 --- a/drivers/soc/apple/sbio.rs +++ b/drivers/soc/apple/sbio.rs @@ -2183,14 +2183,15 @@ impl SepData { self.sensor_trace(c"SENSOR_NONCE"); if !self.sensor_key_loaded.load(Relaxed) { - let firmware = match kernel::firmware::Firmware::request( - c"apple/msrk-j700.bin", &self.dev, - ) { + // Per-device data, so the board names it; J700 predates that. + let name = sensor::root_key_firmware_name().unwrap_or(ROOT_KEY_FIRMWARE); + let firmware = match kernel::firmware::Firmware::request(name, &self.dev) { Ok(firmware) => firmware, Err(error) => { dev_err!( self.dev, - "sensor: signed root-key container unavailable: {:?}\n", + "sensor: signed root-key container {} unavailable: {:?}\n", + name, error ); return false; diff --git a/drivers/soc/apple/sensor.rs b/drivers/soc/apple/sensor.rs index 63a4ae28fbc6d..13bb470c89b3b 100644 --- a/drivers/soc/apple/sensor.rs +++ b/drivers/soc/apple/sensor.rs @@ -25,15 +25,26 @@ extern "C" { fn sep_sensor_irq_available() -> c_int; fn sep_sensor_irq_arm(); fn sep_sensor_irq_wait(timeout_ms: c_uint) -> c_int; - fn sep_sensor_firmware_name() -> *const c_char; + fn sep_sensor_firmware_name(index: c_uint) -> *const c_char; } /// The calibration blob's name from the board description, or `None` when the /// sensor node has no `firmware-name` property (the caller then uses a default). pub(crate) fn firmware_name() -> Option<&'static CStr> { + firmware_name_at(0) +} + +/// The signed root-key container's name from the board description: the +/// second `firmware-name` entry, or `None` when the board gives only the +/// calibration (the caller then uses a default). +pub(crate) fn root_key_firmware_name() -> Option<&'static CStr> { + firmware_name_at(1) +} + +fn firmware_name_at(index: c_uint) -> Option<&'static CStr> { // SAFETY: the shim returns NULL or a pointer to a NUL-terminated string // owned by the device property and valid for the device's lifetime. - let ptr = unsafe { sep_sensor_firmware_name() }; + let ptr = unsafe { sep_sensor_firmware_name(index) }; if ptr.is_null() { return None; } diff --git a/drivers/soc/apple/sensor_shim.c b/drivers/soc/apple/sensor_shim.c index 68405e0a74507..bed228b4d1ca3 100644 --- a/drivers/soc/apple/sensor_shim.c +++ b/drivers/soc/apple/sensor_shim.c @@ -568,19 +568,23 @@ static int sep_sensor_power_line_locked(void) } /* - * The per-device calibration blob name, as the board description gives it in - * the sensor node's "firmware-name" property. NULL when absent, so the caller - * can fall back to a default. + * A per-device blob name, as the board description gives it in the sensor + * node's "firmware-name" list: index 0 is the calibration, index 1 the signed + * root key. NULL when that entry is absent, so the caller can fall back to a + * default. */ -static const char *sep_sensor_firmware_name_locked(void) +static const char *sep_sensor_firmware_name_locked(unsigned int index) { - const char *name = NULL; + const char *names[2] = { NULL, NULL }; + int count; - if (!sep_spi) + if (!sep_spi || index >= ARRAY_SIZE(names)) return NULL; - if (device_property_read_string(&sep_spi->dev, "firmware-name", &name)) + count = device_property_read_string_array(&sep_spi->dev, "firmware-name", + names, ARRAY_SIZE(names)); + if (count <= (int)index) return NULL; - return name; + return names[index]; } /* Powers the sensor on/off, holding the hardware settling delay. */ @@ -871,14 +875,14 @@ int sep_sensor_power_line(void) return result; } -const char *sep_sensor_firmware_name(void) +const char *sep_sensor_firmware_name(unsigned int index) { struct sep_sensor_lifetime *sensor = sep_lock_sensor(); const char *result; if (!sensor) return NULL; - result = sep_sensor_firmware_name_locked(); + result = sep_sensor_firmware_name_locked(index); mutex_unlock(&sensor->lock); return result; } diff --git a/drivers/soc/apple/sep.rs b/drivers/soc/apple/sep.rs index df71496a564fe..7d9f4f2af1869 100644 --- a/drivers/soc/apple/sep.rs +++ b/drivers/soc/apple/sep.rs @@ -449,6 +449,9 @@ const PATCH_POLL_MS: u32 = 20; const PATCH_POLL_ATTEMPTS: u32 = 250; const CALIBRATION_FIRMWARE: &CStr = c"apple/mesa_calibration.bin"; +/// The signed root-key container on boards whose sensor node names only the +/// calibration (J700). +const ROOT_KEY_FIRMWARE: &CStr = c"apple/msrk-j700.bin"; const SBIO_PROBE_USER_ID: i32 = 1000; static_assert!(SBIO_PROBE_USER_ID >= crate::sks::SKS_DESIGNATE_USER_MIN); diff --git a/drivers/soc/apple/shim.h b/drivers/soc/apple/shim.h index 0425bf2cdaa6e..99d2661e2cb1c 100644 --- a/drivers/soc/apple/shim.h +++ b/drivers/soc/apple/shim.h @@ -132,7 +132,7 @@ int sep_sensor_cs_timing_mode(void); int sep_sensor_power_cycle(void); int sep_sensor_power_source(void); int sep_sensor_power(int on); -const char *sep_sensor_firmware_name(void); +const char *sep_sensor_firmware_name(unsigned int index); int sep_sensor_xfer(const void *tx, void *rx, size_t len); int sep_sensor_xfer_tx(const void *tx, size_t len); int sep_sensor_xfer2(const void *tx, size_t tx_len, void *rx, size_t rx_len); From 20cf8265749b808dfb9302d060458991a6887519 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 23:30:10 +1100 Subject: [PATCH 266/388] soc: apple: sep: Add the T6040 (J616s) platform profile The J616s ADT (26.6.2) describes a SEP that iBoot boots before the AP OS, an iop-sep,ascwrap-v6 whose DART window starts at the BIT(40) alias, and the Mesa sensor J700 carries (0x3356) on spi2 at 0x429108000, 8 MHz, chip select 0. So this is a warm-registration platform with T8140's authenticated sensor session, and the xART guard on from the first boot because macOS shares the enclave. Only J616s is matched; the other T6040 Macs' sensor wiring is unchecked. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/soc/apple/profile.rs | 40 ++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/drivers/soc/apple/profile.rs b/drivers/soc/apple/profile.rs index 0ba0eb510122b..0e632c65da81f 100644 --- a/drivers/soc/apple/profile.rs +++ b/drivers/soc/apple/profile.rs @@ -9,7 +9,7 @@ //! here once. The driver reads addresses from the device tree, never scans for //! them, and never patches properties at runtime. //! -//! Five bring-up targets are modelled: +//! These bring-up targets are modelled: //! //! * `T8103` / J313 (MacBook Air, M1): the host boots the SEP with the boot //! endpoint handshake over a 0x30000 shared-memory window. @@ -24,6 +24,8 @@ //! a powered sensor, so the driver uses the warm registration path. //! * `T8122` / J613, J615 (MacBook Air, M3): the T8112 warm registration with //! the T8140 SEP DART aperture above 4 GiB. +//! * `T6040` / J616s (MacBook Pro 16", M4 Pro): iBoot boots sepOS, so the +//! driver uses the warm registration path; the sensor is the J700 part. // The boot handshake, identity source, sensor and DART fields are consumed by // the boot-endpoint, identity and sensor-transport paths. @@ -426,6 +428,28 @@ pub(crate) const T8122: PlatformProfile = PlatformProfile { ..T8112 }; +/// MacBook Pro 16" M4 Pro (J616s). The J616s ADT (26.6.2, 25G83) records a +/// pre-booted SEP (`sepfw-load-at-boot`), an `iop-sep,ascwrap-v6` SEP whose +/// DART aperture starts at the BIT(40) alias (`vm-base` 0x10000004000), and +/// the Mesa sensor J700 carries (`sensor-id` 0x3356) on spi2 at +/// `0x429108000`, 8 MHz, chip select 0. Its power is NUB GPIO 53 and its +/// data-ready line AP GPIO 116. `/defaults` carries `cpx-encryption-mode = 2`. +/// +/// A warm-registration platform like T8140, with T8140's authenticated +/// sensor session: the enclave serves this sensor only after the nonce and +/// the signed root key. Device-tree nodes remain authoritative for addresses. +pub(crate) const T6040: PlatformProfile = PlatformProfile { + name: "T6040/J616s", + sensor: SensorProfile { + controller_base: 0x4_2910_8000, + ..T8140.sensor + }, + // macOS shares this enclave and Linux has no records here yet: keep + // Linux to its own xART records from the first boot. + xart_guard: 1, + ..T8140 +}; + static_assert!(T8103.shmem_capacity == 0x30000); static_assert!(T6020.shmem_capacity == 0x40000); static_assert!(T8140.shmem_capacity == 0x40000); @@ -441,6 +465,8 @@ static_assert!(T8112.sensor.controller_base == T8103.sensor.controller_base); static_assert!(T8122.shmem_capacity == T8112.shmem_capacity); static_assert!(T8122.sensor.controller_base == 0x2a1108000); static_assert!(T8122.wide_dma_mask); +static_assert!(T6040.sensor.controller_base == 0x429108000); +static_assert!(T6040.sensor.expected_id == T8140.sensor.expected_id); /// Whether the machine root declares `compatible`. /// @@ -587,6 +613,11 @@ pub(crate) fn detect() -> Result<&'static PlatformProfile> { if machine_has(b"apple,t8122") && (machine_has(b"apple,j613") || machine_has(b"apple,j615")) { return Ok(&T8122); } + // Only the board whose ADT was read: J616s. The other T6040 Macs share + // the SoC but their sensor wiring is unchecked. + if machine_has(b"apple,t6040") && machine_has(b"apple,j616s") { + return Ok(&T6040); + } Err(ENODEV) } @@ -637,6 +668,11 @@ mod tests { assert!(T8122.wide_dma_mask); assert!(T8122.shmem_capacity == T8112.shmem_capacity); assert!(T8122.sensor.controller_base == 0x2_a110_8000); + assert!(T6040.nonce_session); + assert!(T6040.wide_dma_mask); + assert!(T6040.bootstrap == Bootstrap::WarmRegister); + assert!(T6040.identity == IdentitySource::Chosen); + assert!(T6040.xart_guard == 1); } } @@ -667,7 +703,7 @@ mod dart_window_tests { #[test] fn the_other_profiles_keep_the_bit40_window() { - for profile in [&T8103, &T6000, &T6020, &T8140, &T6020_SEPOS13, &T8122] { + for profile in [&T8103, &T6000, &T6020, &T8140, &T6020_SEPOS13, &T8122, &T6040] { assert_eq!(offset(profile.dart_window), 1 << 40); assert_eq!(range(profile.dart_window), (0, 1 << 32)); } From a35d348cbe97f390cc075d70124f9d8fa3f853d5 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 23:30:10 +1100 Subject: [PATCH 267/388] arm64: dts: apple: t6040: Add the SEP, its DART and mailbox, and spi2 From the J616s ADT: the SEP at 0x406600000 with its mailbox at +0x8000 (AIC 666-669), its DART at 0x404c80000 (AIC 676) with the window iBoot programmed (vm-base 0x10000004000, vm-size 0x3ff00000), and spi2 at 0x429108000 (AIC 1569) with the Mesa sensor child. Add the sep, spi_p and spi2 power states from the ADT pmgr devices. The SEP and its DART stay disabled; apple_sep enables them when it loads. Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/boot/dts/apple/t6040-pmgr.dtsi | 28 ++++++++++ arch/arm64/boot/dts/apple/t6040.dtsi | 66 +++++++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/arch/arm64/boot/dts/apple/t6040-pmgr.dtsi b/arch/arm64/boot/dts/apple/t6040-pmgr.dtsi index 8c05fb4d4d5d3..bf357745d581d 100644 --- a/arch/arm64/boot/dts/apple/t6040-pmgr.dtsi +++ b/arch/arm64/boot/dts/apple/t6040-pmgr.dtsi @@ -140,6 +140,24 @@ power-domains = <&ps_sio>; }; + ps_spi_p: power-controller@198 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x198 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "spi_p"; + power-domains = <&ps_sio>; + }; + + ps_spi2: power-controller@1c8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1c8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "spi2"; + power-domains = <&ps_spi_p>; + }; + ps_ans: power-controller@370 { compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; reg = <0x370 4>; @@ -178,6 +196,16 @@ power-domains = <&ps_ans>, <&ps_apcie_st1>; apple,always-on; }; + + ps_sep: power-controller@c00 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0xc00 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "sep"; + power-domains = <&ps_fab3_soc>; + apple,always-on; + }; }; /* diff --git a/arch/arm64/boot/dts/apple/t6040.dtsi b/arch/arm64/boot/dts/apple/t6040.dtsi index 01486e0f5ac65..c054f5accf531 100644 --- a/arch/arm64/boot/dts/apple/t6040.dtsi +++ b/arch/arm64/boot/dts/apple/t6040.dtsi @@ -702,6 +702,48 @@ status = "disabled"; }; + /* + * iBoot hands over a running SEP. Keep Linux off its DART at boot; + * apple_sep enables it when the driver binds. The SEP only accepts + * IOVAs in the window iBoot programmed (ADT vm-base 0x10000004000, + * vm-size 0x3ff00000), which carries the BIT(40) alias the DART + * translates. + */ + sep_dart: iommu@404c80000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x4 0x04c80000 0x0 0x20000>; + interrupt-parent = <&aic>; + interrupts = ; + #iommu-cells = <1>; + apple,dma-range = /bits/ 64 <0x10000004000 0x3ff00000>; + apple,dma-offset = /bits/ 64 <0x0>; + status = "disabled"; + }; + + sep: sep@406600000 { + compatible = "apple,sep"; + reg = <0x4 0x06600000 0x0 0x88000>; + mboxes = <&sep_mbox>; + mbox-names = "mbox"; + iommus = <&sep_dart 0>; + power-domains = <&ps_sep>; + status = "disabled"; + }; + + sep_mbox: mailbox@406608000 { + compatible = "apple,t6041-asc-mailbox", "apple,asc-mailbox-v4"; + reg = <0x4 0x06608000 0x0 0x4000>; + interrupt-parent = <&aic>; + interrupts = , + , + , + ; + interrupt-names = "send-empty", "send-not-empty", + "recv-empty", "recv-not-empty"; + #mbox-cells = <0>; + status = "disabled"; + }; + ans_mbox: mailbox@409608000 { compatible = "apple,t6041-asc-mailbox", "apple,asc-mailbox-v4"; reg = <0x4 0x09608000 0x0 0x4000>; @@ -827,6 +869,30 @@ status = "disabled"; }; + /* + * Touch ID. iBoot leaves the pads muxed and the controller uses its + * native chip select, so there is no pinctrl here. + */ + spi2: spi@429108000 { + compatible = "apple,t6041-spi", "apple,t8103-spi"; + reg = <0x4 0x29108000 0x0 0x4000>; + interrupt-parent = <&aic>; + interrupts = ; + clocks = <&clkref>; + power-domains = <&ps_spi2>; + #address-cells = <1>; + #size-cells = <0>; + status = "disabled"; + + mesa: fingerprint@0 { + compatible = "apple,mesa-fingerprint"; + reg = <0>; + spi-max-frequency = <8000000>; + spi-cpol; + status = "disabled"; + }; + }; + nvme: nvme@44dcc0000 { compatible = "apple,t6041-nvme-ans2"; reg = <0x4 0x4dcc0000 0x0 0x10000>, From a1153e73f1002a56bca81d564b10af77b1a280ba Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 23:30:10 +1100 Subject: [PATCH 268/388] arm64: dts: apple: t6040-j616s: Enable Touch ID Give the boot loader the sep alias it reserves the SEP firmware through, enable the SEP mailbox and spi2, and describe the sensor: NUB GPIO 53 powers it, AP GPIO 116 is its data-ready line, and its sealed calibration and root key come from this Mac's own iBoot system container. Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/boot/dts/apple/t6040-j616s.dts | 29 +++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/arch/arm64/boot/dts/apple/t6040-j616s.dts b/arch/arm64/boot/dts/apple/t6040-j616s.dts index c47e71ed1e258..500d9fe370ea9 100644 --- a/arch/arm64/boot/dts/apple/t6040-j616s.dts +++ b/arch/arm64/boot/dts/apple/t6040-j616s.dts @@ -29,6 +29,8 @@ disp0-piodma = &disp0_piodma; /* the boot loader finds the GPU node through this alias */ gpu = &gpu; + /* the boot loader reserves the SEP firmware through this alias */ + sep = &sep; }; chosen { @@ -1350,6 +1352,33 @@ status = "okay"; }; +/* apple_sep enables the SEP and its DART when it loads. */ +&sep_mbox { + status = "okay"; +}; + +/* + * Touch ID sensor (Mesa 0x3356). Its power is NUB GPIO 53 and its + * data-ready line is AP GPIO 116, as the J616s ADT describes them. + */ +&spi2 { + status = "okay"; +}; + +&mesa { + status = "okay"; + enable-gpios = <&pinctrl_nub 53 GPIO_ACTIVE_HIGH>; + interrupt-parent = <&pinctrl>; + interrupts = <116 IRQ_TYPE_EDGE_RISING>; + spi-cs-setup-delay-ns = <20>; + spi-cs-hold-delay-ns = <20>; + /* + * Per-device factory data from this Mac's own iBoot system container: + * the sealed calibration and the sealed root key. + */ + firmware-name = "apple/mesacal-j616s.bin", "apple/msrk-j616s.bin"; +}; + /* The Type-C retimers (ADT atcrt at 0x18/0x19/0x1a) sit on this bus; no driver yet. */ &i2c6 { status = "okay"; From 65f748be0452ad6d80a41d1fc063eeac9867db32 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 15:02:20 +1100 Subject: [PATCH 269/388] arm64: dts: apple: describe the remaining T6040 PMGR power states Describe the power-state registers and their parent graph. Leave providers without a qualified Linux consumer disabled, since registering an unused domain may power down hardware still owned by firmware. Keep the existing tested providers unchanged. Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/boot/dts/apple/t6040-pmgr.dtsi | 1762 +++++++++++++++++++++ arch/arm64/boot/dts/apple/t6040.dtsi | 21 + 2 files changed, 1783 insertions(+) diff --git a/arch/arm64/boot/dts/apple/t6040-pmgr.dtsi b/arch/arm64/boot/dts/apple/t6040-pmgr.dtsi index bf357745d581d..18ec5df7d33bf 100644 --- a/arch/arm64/boot/dts/apple/t6040-pmgr.dtsi +++ b/arch/arm64/boot/dts/apple/t6040-pmgr.dtsi @@ -506,3 +506,1765 @@ power-domains = <&ps_atc2_phymxwrap>; }; }; + +/* + * Remaining ADT power states. Keep providers without a qualified consumer + * disabled: registering an unused genpd can power down firmware-owned blocks. + * Enable the provider and its parents together with the consumer. + */ +&pmgr { + ps_apcie_gp: power-controller@100 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x100 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "apcie_gp"; + status = "disabled"; + }; + + ps_fab4_soc: power-controller@128 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x128 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "fab4_soc"; + power-domains = <&ps_afc>; + apple,always-on; + status = "disabled"; + }; + + ps_sio_cpu: power-controller@130 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x130 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "sio_cpu"; + power-domains = <&ps_sio>; + apple,always-on; + status = "disabled"; + }; + + ps_fpwm1: power-controller@140 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x140 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "fpwm1"; + power-domains = <&ps_sio>; + status = "disabled"; + }; + + ps_fpwm2: power-controller@148 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x148 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "fpwm2"; + power-domains = <&ps_sio>; + status = "disabled"; + }; + + ps_i2c0: power-controller@150 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x150 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "i2c0"; + power-domains = <&ps_sio>; + status = "disabled"; + }; + + ps_i2c5: power-controller@178 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x178 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "i2c5"; + power-domains = <&ps_sio>; + status = "disabled"; + }; + + ps_i2c7: power-controller@188 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x188 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "i2c7"; + power-domains = <&ps_sio>; + status = "disabled"; + }; + + ps_spi_p: power-controller@198 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x198 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "spi_p"; + power-domains = <&ps_sio>; + status = "disabled"; + }; + + ps_uart_p: power-controller@1a0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1a0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "uart_p"; + power-domains = <&ps_sio>; + status = "disabled"; + }; + + ps_dpa_p: power-controller@1a8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1a8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dpa_p"; + power-domains = <&ps_sio>; + apple,always-on; + status = "disabled"; + }; + + ps_aes: power-controller@1b0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1b0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "aes"; + power-domains = <&ps_sio>; + apple,always-on; + status = "disabled"; + }; + + ps_spi0: power-controller@1b8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1b8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "spi0"; + power-domains = <&ps_spi_p>; + status = "disabled"; + }; + + ps_spi1: power-controller@1c0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1c0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "spi1"; + power-domains = <&ps_spi_p>; + status = "disabled"; + }; + + ps_spi2: power-controller@1c8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1c8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "spi2"; + power-domains = <&ps_spi_p>; + status = "disabled"; + }; + + ps_spi3: power-controller@1d0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1d0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "spi3"; + power-domains = <&ps_spi_p>; + status = "disabled"; + }; + + ps_spi4: power-controller@1d8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1d8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "spi4"; + power-domains = <&ps_spi_p>; + status = "disabled"; + }; + + ps_spi5: power-controller@1e0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1e0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "spi5"; + power-domains = <&ps_spi_p>; + status = "disabled"; + }; + + ps_qspi: power-controller@1e8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1e8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "qspi"; + power-domains = <&ps_spi_p>; + status = "disabled"; + }; + + ps_uart_n: power-controller@1f0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1f0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "uart_n"; + power-domains = <&ps_uart_p>; + status = "disabled"; + }; + + ps_uart0: power-controller@1f8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1f8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "uart0"; + power-domains = <&ps_uart_p>; + status = "disabled"; + }; + + ps_uart1: power-controller@200 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x200 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "uart1"; + power-domains = <&ps_uart_p>; + status = "disabled"; + }; + + ps_uart2: power-controller@208 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x208 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "uart2"; + power-domains = <&ps_uart_p>; + status = "disabled"; + }; + + ps_uart3: power-controller@210 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x210 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "uart3"; + power-domains = <&ps_uart_p>; + status = "disabled"; + }; + + ps_uart4: power-controller@218 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x218 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "uart4"; + power-domains = <&ps_uart_p>; + status = "disabled"; + }; + + ps_uart5: power-controller@220 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x220 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "uart5"; + power-domains = <&ps_uart_p>; + status = "disabled"; + }; + + ps_uart6: power-controller@228 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x228 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "uart6"; + power-domains = <&ps_uart_p>; + status = "disabled"; + }; + + ps_dpa0: power-controller@230 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x230 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dpa0"; + power-domains = <&ps_dpa_p>; + status = "disabled"; + }; + + ps_dpa1: power-controller@238 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x238 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dpa1"; + power-domains = <&ps_dpa_p>; + status = "disabled"; + }; + + ps_dpa2: power-controller@240 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x240 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dpa2"; + power-domains = <&ps_dpa_p>; + status = "disabled"; + }; + + ps_dpa3: power-controller@248 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x248 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dpa3"; + power-domains = <&ps_dpa_p>; + status = "disabled"; + }; + + ps_dpa4: power-controller@250 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x250 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dpa4"; + power-domains = <&ps_dpa_p>; + status = "disabled"; + }; + + ps_disp_sys: power-controller@258 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x258 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "disp_sys"; + power-domains = <&ps_fab6_soc>; + status = "disabled"; + }; + + ps_amcc4: power-controller@260 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x260 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "amcc4"; + power-domains = <&ps_afc>; + apple,always-on; + status = "disabled"; + }; + + ps_amcc5: power-controller@268 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x268 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "amcc5"; + power-domains = <&ps_afc>; + apple,always-on; + status = "disabled"; + }; + + ps_amcc6: power-controller@270 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x270 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "amcc6"; + power-domains = <&ps_afc>; + apple,always-on; + status = "disabled"; + }; + + ps_amcc7: power-controller@278 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x278 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "amcc7"; + power-domains = <&ps_afc>; + apple,always-on; + status = "disabled"; + }; + + ps_amcc12: power-controller@280 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x280 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "amcc12"; + power-domains = <&ps_afc>; + apple,always-on; + status = "disabled"; + }; + + ps_amcc13: power-controller@288 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x288 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "amcc13"; + power-domains = <&ps_afc>; + apple,always-on; + status = "disabled"; + }; + + ps_amcc14: power-controller@290 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x290 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "amcc14"; + power-domains = <&ps_afc>; + apple,always-on; + status = "disabled"; + }; + + ps_amcc15: power-controller@298 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x298 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "amcc15"; + power-domains = <&ps_afc>; + apple,always-on; + status = "disabled"; + }; + + ps_dcs_04: power-controller@2e0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x2e0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dcs_04"; + power-domains = <&ps_amcc4>; + apple,always-on; + status = "disabled"; + }; + + ps_dcs_05: power-controller@2e8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x2e8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dcs_05"; + power-domains = <&ps_amcc5>; + apple,always-on; + status = "disabled"; + }; + + ps_dcs_06: power-controller@2f0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x2f0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dcs_06"; + power-domains = <&ps_amcc6>; + apple,always-on; + status = "disabled"; + }; + + ps_dcs_07: power-controller@2f8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x2f8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dcs_07"; + power-domains = <&ps_amcc7>; + apple,always-on; + status = "disabled"; + }; + + ps_dcs_12: power-controller@300 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x300 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dcs_12"; + power-domains = <&ps_amcc12>; + apple,always-on; + status = "disabled"; + }; + + ps_dcs_13: power-controller@308 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x308 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dcs_13"; + power-domains = <&ps_amcc13>; + apple,always-on; + status = "disabled"; + }; + + ps_dcs_14: power-controller@310 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x310 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dcs_14"; + power-domains = <&ps_amcc14>; + apple,always-on; + status = "disabled"; + }; + + ps_dcs_15: power-controller@318 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x318 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dcs_15"; + power-domains = <&ps_amcc15>; + apple,always-on; + status = "disabled"; + }; + + ps_disp_fe: power-controller@368 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x368 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "disp_fe"; + power-domains = <&ps_disp_sys>; + status = "disabled"; + }; + + ps_apcie_sys_gp: power-controller@390 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x390 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "apcie_sys_gp"; + power-domains = <&ps_fab3_soc>, <&ps_apcie_gp>; + status = "disabled"; + }; + + ps_apcie_phy_sw: power-controller@398 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x398 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "apcie_phy_sw"; + power-domains = <&ps_apcie_sys_st0>, <&ps_apcie_sys_st1>; + status = "disabled"; + }; + + ps_jpg: power-controller@3a0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x3a0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "jpg"; + power-domains = <&ps_fab4_soc>; + status = "disabled"; + }; + + ps_ane_sys: power-controller@3c0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x3c0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "ane_sys"; + power-domains = <&ps_fab5_soc>, <&ps_fab4_soc>; + status = "disabled"; + }; + + ps_sep: power-controller@c00 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0xc00 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "sep"; + power-domains = <&ps_fab3_soc>; + apple,always-on; + status = "disabled"; + }; +}; + +&pmgr_aux { + ps_ecpu_0: power-controller@0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "ecpu_0"; + status = "disabled"; + }; + + ps_ecpu_1: power-controller@8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "ecpu_1"; + status = "disabled"; + }; + + ps_ecpu_2: power-controller@10 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x10 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "ecpu_2"; + status = "disabled"; + }; + + ps_ecpu_3: power-controller@18 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x18 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "ecpu_3"; + status = "disabled"; + }; + + ps_pcpu0_0: power-controller@20 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x20 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "pcpu0_0"; + status = "disabled"; + }; + + ps_pcpu0_1: power-controller@28 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x28 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "pcpu0_1"; + status = "disabled"; + }; + + ps_pcpu0_2: power-controller@30 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x30 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "pcpu0_2"; + status = "disabled"; + }; + + ps_pcpu0_3: power-controller@38 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x38 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "pcpu0_3"; + status = "disabled"; + }; + + ps_pcpu0_4: power-controller@40 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x40 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "pcpu0_4"; + status = "disabled"; + }; + + ps_pcpu0_5: power-controller@48 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x48 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "pcpu0_5"; + status = "disabled"; + }; + + ps_pcpu1_0: power-controller@50 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x50 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "pcpu1_0"; + status = "disabled"; + }; + + ps_pcpu1_1: power-controller@58 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x58 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "pcpu1_1"; + status = "disabled"; + }; + + ps_pcpu1_2: power-controller@60 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x60 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "pcpu1_2"; + status = "disabled"; + }; + + ps_pcpu1_3: power-controller@68 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x68 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "pcpu1_3"; + status = "disabled"; + }; + + ps_pcpu1_4: power-controller@70 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x70 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "pcpu1_4"; + status = "disabled"; + }; + + ps_pcpu1_5: power-controller@78 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x78 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "pcpu1_5"; + status = "disabled"; + }; + + ps_ecpm: power-controller@80 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x80 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "ecpm"; + status = "disabled"; + }; + + ps_pcpm0: power-controller@88 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x88 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "pcpm0"; + status = "disabled"; + }; + + ps_pcpm1: power-controller@90 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x90 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "pcpm1"; + status = "disabled"; + }; + + ps_dwi: power-controller@108 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x108 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dwi"; + status = "disabled"; + }; + + ps_aic: power-controller@118 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x118 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "aic"; + power-domains = <&ps_pms>; + status = "disabled"; + }; + + ps_pmc: power-controller@120 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x120 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "pmc"; + power-domains = <&ps_pms>; + apple,always-on; + status = "disabled"; + }; + + ps_pms_c1ppt: power-controller@128 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x128 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "pms_c1ppt"; + power-domains = <&ps_pms>; + apple,always-on; + status = "disabled"; + }; + + ps_soc_dpe: power-controller@130 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x130 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "soc_dpe"; + power-domains = <&ps_pms>; + apple,always-on; + status = "disabled"; + }; + + ps_pmgr_soc_ocla: power-controller@138 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x138 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "pmgr_soc_ocla"; + status = "disabled"; + }; + + ps_pms_fpwm0: power-controller@140 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x140 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "pms_fpwm0"; + power-domains = <&ps_pms>; + apple,always-on; + status = "disabled"; + }; + + ps_pms_fpwm1: power-controller@148 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x148 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "pms_fpwm1"; + power-domains = <&ps_pms>; + apple,always-on; + status = "disabled"; + }; + + ps_pms_fpwm2: power-controller@150 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x150 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "pms_fpwm2"; + power-domains = <&ps_pms>; + apple,always-on; + status = "disabled"; + }; + + ps_pms_fpwm3: power-controller@158 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x158 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "pms_fpwm3"; + power-domains = <&ps_pms>; + apple,always-on; + status = "disabled"; + }; + + ps_pms_fpwm4: power-controller@160 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x160 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "pms_fpwm4"; + power-domains = <&ps_pms>; + apple,always-on; + status = "disabled"; + }; + + ps_fab0_soc: power-controller@168 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x168 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "fab0_soc"; + power-domains = <&ps_afi>; + apple,always-on; + status = "disabled"; + }; + + ps_amcc0: power-controller@180 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x180 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "amcc0"; + power-domains = <&ps_afc>; + apple,always-on; + status = "disabled"; + }; + + ps_dcs_00: power-controller@188 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x188 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dcs_00"; + power-domains = <&ps_amcc0>; + apple,always-on; + status = "disabled"; + }; + + ps_rom: power-controller@190 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x190 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "rom"; + power-domains = <&ps_amcc0>; + status = "disabled"; + }; + + ps_amcc1: power-controller@198 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x198 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "amcc1"; + power-domains = <&ps_afc>; + apple,always-on; + status = "disabled"; + }; + + ps_dcs_01: power-controller@1a0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1a0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dcs_01"; + power-domains = <&ps_amcc1>; + apple,always-on; + status = "disabled"; + }; + + ps_amcc2: power-controller@1a8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1a8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "amcc2"; + power-domains = <&ps_afc>; + apple,always-on; + status = "disabled"; + }; + + ps_dcs_02: power-controller@1b0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1b0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dcs_02"; + power-domains = <&ps_amcc2>; + apple,always-on; + status = "disabled"; + }; + + ps_amcc3: power-controller@1b8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1b8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "amcc3"; + power-domains = <&ps_afc>; + apple,always-on; + status = "disabled"; + }; + + ps_dcs_03: power-controller@1c0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1c0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dcs_03"; + power-domains = <&ps_amcc3>; + apple,always-on; + status = "disabled"; + }; + + ps_amcc8: power-controller@1c8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1c8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "amcc8"; + power-domains = <&ps_afc>; + apple,always-on; + status = "disabled"; + }; + + ps_dcs_08: power-controller@1d0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1d0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dcs_08"; + power-domains = <&ps_amcc8>; + apple,always-on; + status = "disabled"; + }; + + ps_amcc9: power-controller@1d8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1d8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "amcc9"; + power-domains = <&ps_afc>; + apple,always-on; + status = "disabled"; + }; + + ps_dcs_09: power-controller@1e0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1e0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dcs_09"; + power-domains = <&ps_amcc9>; + apple,always-on; + status = "disabled"; + }; + + ps_amcc10: power-controller@1e8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1e8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "amcc10"; + power-domains = <&ps_afc>; + apple,always-on; + status = "disabled"; + }; + + ps_dcs_10: power-controller@1f0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1f0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dcs_10"; + power-domains = <&ps_amcc10>; + apple,always-on; + status = "disabled"; + }; + + ps_amcc11: power-controller@1f8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1f8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "amcc11"; + power-domains = <&ps_afc>; + apple,always-on; + status = "disabled"; + }; + + ps_dcs_11: power-controller@200 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x200 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dcs_11"; + power-domains = <&ps_amcc11>; + apple,always-on; + status = "disabled"; + }; + + ps_venc0_sys: power-controller@278 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x278 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "venc0_sys"; + power-domains = <&ps_fab0_soc>; + status = "disabled"; + }; + + ps_fab_gw_0: power-controller@288 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x288 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "fab_gw_0"; + apple,always-on; + status = "disabled"; + }; + + ps_fab_gw_1: power-controller@290 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x290 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "fab_gw_1"; + apple,always-on; + status = "disabled"; + }; + + ps_fab_afr: power-controller@298 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x298 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "fab_afr"; + apple,always-on; + status = "disabled"; + }; + + ps_fab1_soc: power-controller@2d0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x2d0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "fab1_soc"; + power-domains = <&ps_afi>; + apple,always-on; + status = "disabled"; + }; + + ps_scodec: power-controller@328 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x328 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "scodec"; + power-domains = <&ps_fab1_soc>; + status = "disabled"; + }; + + ps_scodec_streaming: power-controller@330 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x330 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "scodec_streaming"; + power-domains = <&ps_scodec>; + status = "disabled"; + }; + + ps_fab2_soc: power-controller@338 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x338 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "fab2_soc"; + power-domains = <&ps_afi>; + apple,always-on; + status = "disabled"; + }; + + ps_dispext0_sys: power-controller@340 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x340 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dispext0_sys"; + power-domains = <&ps_fab2_soc>; + status = "disabled"; + }; + + ps_dispext0_fe: power-controller@348 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x348 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dispext0_fe"; + power-domains = <&ps_dispext0_sys>; + status = "disabled"; + }; + + ps_dispext0_cpu: power-controller@350 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x350 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dispext0_cpu"; + power-domains = <&ps_dispext0_fe>; + status = "disabled"; + }; + + ps_atc0_cio: power-controller@398 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x398 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc0_cio"; + power-domains = <&ps_atc0_common>; + status = "disabled"; + }; + + ps_atc0_pcie: power-controller@3a0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x3a0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc0_pcie"; + power-domains = <&ps_atc0_common>; + status = "disabled"; + }; + + ps_atc0_cio_pcie: power-controller@3a8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x3a8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc0_cio_pcie"; + power-domains = <&ps_atc0_cio>; + status = "disabled"; + }; + + ps_atc0_cio_usb: power-controller@3b0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x3b0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc0_cio_usb"; + power-domains = <&ps_atc0_cio>; + status = "disabled"; + }; + + ps_atc1_cio: power-controller@3c0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x3c0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc1_cio"; + power-domains = <&ps_atc1_common>; + status = "disabled"; + }; + + ps_atc1_pcie: power-controller@3c8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x3c8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc1_pcie"; + power-domains = <&ps_atc1_common>; + status = "disabled"; + }; + + ps_atc1_cio_pcie: power-controller@3d0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x3d0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc1_cio_pcie"; + power-domains = <&ps_atc1_cio>; + status = "disabled"; + }; + + ps_atc1_cio_usb: power-controller@3d8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x3d8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc1_cio_usb"; + power-domains = <&ps_atc1_cio>; + status = "disabled"; + }; + + ps_atc2_cio: power-controller@3e8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x3e8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc2_cio"; + power-domains = <&ps_atc2_common>; + status = "disabled"; + }; + + ps_atc2_pcie: power-controller@3f0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x3f0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc2_pcie"; + power-domains = <&ps_atc2_common>; + status = "disabled"; + }; + + ps_atc2_cio_pcie: power-controller@3f8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x3f8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc2_cio_pcie"; + power-domains = <&ps_atc2_cio>; + status = "disabled"; + }; + + ps_atc2_cio_usb: power-controller@400 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x400 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc2_cio_usb"; + power-domains = <&ps_atc2_cio>; + status = "disabled"; + }; + + ps_atc3_common: power-controller@408 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x408 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc3_common"; + power-domains = <&ps_fab2_soc>; + status = "disabled"; + }; + + ps_atc3_cio: power-controller@420 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x420 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc3_cio"; + power-domains = <&ps_atc3_common>; + status = "disabled"; + }; + + ps_atc3_pcie: power-controller@428 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x428 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc3_pcie"; + power-domains = <&ps_atc3_common>; + status = "disabled"; + }; + + ps_atc3_cio_pcie: power-controller@430 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x430 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc3_cio_pcie"; + power-domains = <&ps_atc3_cio>; + status = "disabled"; + }; + + ps_atc3_cio_usb: power-controller@438 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x438 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc3_cio_usb"; + power-domains = <&ps_atc3_cio>; + status = "disabled"; + }; + + ps_fab5_soc: power-controller@440 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x440 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "fab5_soc"; + power-domains = <&ps_afi>; + apple,always-on; + status = "disabled"; + }; + + ps_sbr: power-controller@448 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x448 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "sbr"; + power-domains = <&ps_fab5_soc>; + apple,always-on; + status = "disabled"; + }; + + ps_gpio: power-controller@450 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x450 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "gpio"; + power-domains = <&ps_sbr>; + status = "disabled"; + }; + + ps_msg: power-controller@458 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x458 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "msg"; + power-domains = <&ps_sbr>; + status = "disabled"; + }; + + ps_ap_tmm: power-controller@468 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x468 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "ap_tmm"; + power-domains = <&ps_sbr>; + status = "disabled"; + }; + + ps_msr0: power-controller@470 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x470 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "msr0"; + power-domains = <&ps_fab5_soc>; + status = "disabled"; + }; + + ps_msr0_ase_core: power-controller@478 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x478 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "msr0_ase_core"; + power-domains = <&ps_msr0>; + status = "disabled"; + }; + + ps_fab7_soc: power-controller@488 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x488 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "fab7_soc"; + power-domains = <&ps_afi>; + apple,always-on; + status = "disabled"; + }; + + ps_dispext1_sys: power-controller@490 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x490 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dispext1_sys"; + power-domains = <&ps_fab7_soc>; + status = "disabled"; + }; + + ps_dispext1_fe: power-controller@498 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x498 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dispext1_fe"; + power-domains = <&ps_dispext1_sys>; + status = "disabled"; + }; + + ps_dispext1_cpu: power-controller@4a0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x4a0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dispext1_cpu"; + power-domains = <&ps_dispext1_fe>; + status = "disabled"; + }; +}; + +&pmgr_ane { + ps_ane_mpm: power-controller@0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "ane_mpm"; + power-domains = <&ps_ane_sys>; + status = "disabled"; + }; + + ps_ane_cpu: power-controller@8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "ane_cpu"; + power-domains = <&ps_ane_sys>; + status = "disabled"; + }; + + ps_ane_td: power-controller@10 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x10 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "ane_td"; + power-domains = <&ps_ane_sys>; + status = "disabled"; + }; + + ps_ane_base: power-controller@18 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x18 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "ane_base"; + power-domains = <&ps_ane_td>; + status = "disabled"; + }; +}; + +&pmgr_mini { + ps_debug_gated: power-controller@0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "debug_gated"; + apple,always-on; + status = "disabled"; + }; + + ps_nub_spmi_ahb: power-controller@58 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x58 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "nub_spmi_ahb"; + apple,always-on; + status = "disabled"; + }; + + ps_nub_spmi0: power-controller@60 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x60 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "nub_spmi0"; + power-domains = <&ps_nub_spmi_ahb>; + apple,always-on; + status = "disabled"; + }; + + ps_nub_spmi_a2: power-controller@78 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x78 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "nub_spmi_a2"; + apple,always-on; + status = "disabled"; + }; + + ps_nub_spi0: power-controller@80 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x80 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "nub_spi0"; + apple,always-on; + status = "disabled"; + }; + + ps_nub_aon: power-controller@88 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x88 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "nub_aon"; + apple,always-on; + status = "disabled"; + }; + + ps_nub_ocla: power-controller@98 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x98 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "nub_ocla"; + apple,always-on; + status = "disabled"; + }; + + ps_atc3_common_dp: power-controller@b8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0xb8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc3_common_dp"; + status = "disabled"; + }; + + ps_nub_spmi1: power-controller@c0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0xc0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "nub_spmi1"; + power-domains = <&ps_nub_spmi_ahb>; + apple,always-on; + status = "disabled"; + }; + + ps_atc3_usb_aon: power-controller@e0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0xe0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc3_usb_aon"; + status = "disabled"; + }; + + ps_nub_spmi2: power-controller@e8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0xe8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "nub_spmi2"; + power-domains = <&ps_nub_spmi_ahb>; + apple,always-on; + status = "disabled"; + }; + + ps_nub_spmi3: power-controller@f0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0xf0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "nub_spmi3"; + power-domains = <&ps_nub_spmi_ahb>; + apple,always-on; + status = "disabled"; + }; + + ps_nub_spmi4: power-controller@f8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0xf8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "nub_spmi4"; + power-domains = <&ps_nub_spmi_ahb>; + apple,always-on; + status = "disabled"; + }; + + ps_nub_fabric: power-controller@100 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x100 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "nub_fabric"; + apple,always-on; + status = "disabled"; + }; + + ps_nub_sram: power-controller@108 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x108 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "nub_sram"; + power-domains = <&ps_nub_fabric>; + apple,always-on; + status = "disabled"; + }; + + ps_debug_switch: power-controller@110 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x110 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "debug_switch"; + apple,always-on; + status = "disabled"; + }; + + ps_atc3_usb: power-controller@148 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x148 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc3_usb"; + power-domains = <&ps_atc3_common>, <&ps_atc3_usb_aon>; + status = "disabled"; + }; + + ps_atc3_phymxwrap: power-controller@168 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x168 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc3_phymxwrap"; + power-domains = <&ps_atc3_common>, <&ps_atc3_usb_aon>; + status = "disabled"; + }; +}; + +&pmgr_disp { + ps_disp_cpu: power-controller@0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "disp_cpu"; + power-domains = <&ps_disp_fe>; + status = "disabled"; + }; +}; + +&pmgr_gfx { + ps_gpx: power-controller@0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "gpx"; + apple,always-on; + status = "disabled"; + }; +}; + +&pmgr_venc { + ps_venc0_dma: power-controller@0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "venc0_dma"; + power-domains = <&ps_venc0_sys>; + status = "disabled"; + }; + + ps_venc0_pipe4: power-controller@8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "venc0_pipe4"; + power-domains = <&ps_venc0_dma>; + status = "disabled"; + }; + + ps_venc0_pipe5: power-controller@10 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x10 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "venc0_pipe5"; + power-domains = <&ps_venc0_dma>; + status = "disabled"; + }; + + ps_venc0_me0: power-controller@18 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x18 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "venc0_me0"; + power-domains = <&ps_venc0_dma>; + status = "disabled"; + }; + + ps_venc0_me1: power-controller@20 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x20 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "venc0_me1"; + power-domains = <&ps_venc0_me0>; + status = "disabled"; + }; +}; diff --git a/arch/arm64/boot/dts/apple/t6040.dtsi b/arch/arm64/boot/dts/apple/t6040.dtsi index c054f5accf531..ba79030262ad6 100644 --- a/arch/arm64/boot/dts/apple/t6040.dtsi +++ b/arch/arm64/boot/dts/apple/t6040.dtsi @@ -1030,6 +1030,27 @@ /* child nodes are added in t6040-pmgr.dtsi */ }; + pmgr_ane: power-management@50228c000 { + compatible = "apple,t6041-pmgr", "apple,t8103-pmgr", "syscon", "simple-mfd"; + reg = <0x5 0x0228c000 0x0 0x4000>; + #address-cells = <1>; + #size-cells = <1>; + }; + + pmgr_disp: power-management@502290000 { + compatible = "apple,t6041-pmgr", "apple,t8103-pmgr", "syscon", "simple-mfd"; + reg = <0x5 0x02290000 0x0 0x4000>; + #address-cells = <1>; + #size-cells = <1>; + }; + + pmgr_venc: power-management@502808000 { + compatible = "apple,t6041-pmgr", "apple,t8103-pmgr", "syscon", "simple-mfd"; + reg = <0x5 0x02808000 0x0 0x4000>; + #address-cells = <1>; + #size-cells = <1>; + }; + pmgr_isp: power-management@502284000 { compatible = "apple,t6041-pmgr", "apple,t8103-pmgr", "syscon", "simple-mfd"; #address-cells = <1>; From 327ef44386db8a77ce53edce35b6317ef1cb3259 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 15:04:10 +1100 Subject: [PATCH 270/388] arm64: dts: apple: describe the remaining T6040 DART banks Describe the separate ANE and AVE translation banks, PCIe port 1, SIO, external displays, scaler, APR and JPEG IOMMUs. Keep each disabled until its DMA consumer owns its power and streams. Shared DAPF and remap apertures are not independent IOMMU instances. Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/boot/dts/apple/t6040.dtsi | 173 +++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/arch/arm64/boot/dts/apple/t6040.dtsi b/arch/arm64/boot/dts/apple/t6040.dtsi index ba79030262ad6..fb4cde463cc2d 100644 --- a/arch/arm64/boot/dts/apple/t6040.dtsi +++ b/arch/arm64/boot/dts/apple/t6040.dtsi @@ -1808,6 +1808,179 @@ phy-names = "usb2-phy", "usb3-phy"; status = "disabled"; }; + /* Additional DARTs; enabled together with their DMA consumers. */ + ave0_dart1: iommu@283080000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x2 0x83080000 0x0 0xc000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + power-domains = <&ps_venc0_sys>; + status = "disabled"; + }; + + ave0_dart0: iommu@2830a0000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x2 0x830a0000 0x0 0xc000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + power-domains = <&ps_venc0_sys>; + status = "disabled"; + }; + + apr0_dart: iommu@301020000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x3 0x01020000 0x0 0xc000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + power-domains = <&ps_scodec>; + status = "disabled"; + }; + + apciec0_dart: iommu@384000000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x3 0x84000000 0x0 0x20000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + status = "disabled"; + }; + + apciec1_dart: iommu@38c000000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x3 0x8c000000 0x0 0x20000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + status = "disabled"; + }; + + apciec2_dart: iommu@394000000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x3 0x94000000 0x0 0x20000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + status = "disabled"; + }; + + dispext0_dart: iommu@3a2300000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x3 0xa2300000 0x0 0xc000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + status = "disabled"; + }; + + dcpext0_dart: iommu@3a2340000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x3 0xa2340000 0x0 0x20000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + status = "disabled"; + }; + + apcie1_dart: iommu@411000000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x4 0x11000000 0x0 0x20000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + status = "disabled"; + }; + + sio_dart: iommu@429060000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x4 0x29060000 0x0 0x20000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + power-domains = <&ps_sio>; + status = "disabled"; + }; + + ane0_dart0: iommu@485800000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x4 0x85800000 0x0 0xc000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + power-domains = <&ps_ane_sys>; + status = "disabled"; + }; + + ane0_dart1: iommu@485820000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x4 0x85820000 0x0 0xc000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + power-domains = <&ps_ane_sys>; + status = "disabled"; + }; + + ane0_dart2: iommu@485840000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x4 0x85840000 0x0 0xc000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + power-domains = <&ps_ane_sys>; + status = "disabled"; + }; + + jpeg0_dart: iommu@487020000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x4 0x87020000 0x0 0xc000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + power-domains = <&ps_jpg>; + status = "disabled"; + }; + + jpeg1_dart: iommu@487080000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x4 0x87080000 0x0 0xc000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + power-domains = <&ps_jpg>; + status = "disabled"; + }; + + scaler0_dart: iommu@521260000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x5 0x21260000 0x0 0xc000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + power-domains = <&ps_msr0>; + status = "disabled"; + }; + + dispext1_dart: iommu@602300000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x6 0x02300000 0x0 0xc000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + status = "disabled"; + }; + + dcpext1_dart: iommu@602340000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x6 0x02340000 0x0 0xc000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + status = "disabled"; + }; + }; #include "t6040-pmgr.dtsi" From c5da358f20e09da54d058cf08c9451dc64ba464e Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 15:06:22 +1100 Subject: [PATCH 271/388] arm64: dts: apple: describe the remaining T6040 GPIO and SPMI controllers Add the AOP and SMC pin banks with the ADT pin counts and interrupt groups, and the four remaining NUB SPMI buses plus the AOP SPMI bus. Leave coprocessor-owned banks disabled until their ownership can be transferred. Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/boot/dts/apple/t6040.dtsi | 89 ++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/arch/arm64/boot/dts/apple/t6040.dtsi b/arch/arm64/boot/dts/apple/t6040.dtsi index fb4cde463cc2d..0a2d7104a103b 100644 --- a/arch/arm64/boot/dts/apple/t6040.dtsi +++ b/arch/arm64/boot/dts/apple/t6040.dtsi @@ -1981,6 +1981,95 @@ status = "disabled"; }; + pinctrl_aop: pinctrl@510824000 { + compatible = "apple,t6041-pinctrl", "apple,t8103-pinctrl"; + reg = <0x5 0x10824000 0x0 0x4000>; + clocks = <&clkref>; + gpio-controller; + #gpio-cells = <2>; + gpio-ranges = <&pinctrl_aop 0 0 85>; + apple,npins = <85>; + interrupt-controller; + #interrupt-cells = <2>; + interrupt-parent = <&aic>; + interrupts = , + , + , + , + , + , + ; + /* Coprocessor pins and interrupt groups need an ownership handoff. */ + status = "disabled"; + }; + + pinctrl_smc: pinctrl@50c824000 { + compatible = "apple,t6041-pinctrl", "apple,t8103-pinctrl"; + reg = <0x5 0x0c824000 0x0 0x4000>; + clocks = <&clkref>; + gpio-controller; + #gpio-cells = <2>; + gpio-ranges = <&pinctrl_smc 0 0 24>; + apple,npins = <24>; + interrupt-controller; + #interrupt-cells = <2>; + interrupt-parent = <&aic>; + interrupts = , + , + , + , + , + , + ; + /* Coprocessor pins and interrupt groups need an ownership handoff. */ + status = "disabled"; + }; + + nub_spmi1: spmi@509054000 { + compatible = "apple,t6041-spmi", "apple,t8103-spmi"; + reg = <0x5 0x09054000 0x0 0x100>; + #address-cells = <2>; + #size-cells = <0>; + power-domains = <&ps_nub_spmi1>; + status = "disabled"; + }; + + nub_spmi2: spmi@509094000 { + compatible = "apple,t6041-spmi", "apple,t8103-spmi"; + reg = <0x5 0x09094000 0x0 0x100>; + #address-cells = <2>; + #size-cells = <0>; + power-domains = <&ps_nub_spmi2>; + status = "disabled"; + }; + + nub_spmi3: spmi@5090d4000 { + compatible = "apple,t6041-spmi", "apple,t8103-spmi"; + reg = <0x5 0x090d4000 0x0 0x100>; + #address-cells = <2>; + #size-cells = <0>; + power-domains = <&ps_nub_spmi3>; + status = "disabled"; + }; + + nub_spmi4: spmi@509114000 { + compatible = "apple,t6041-spmi", "apple,t8103-spmi"; + reg = <0x5 0x09114000 0x0 0x100>; + #address-cells = <2>; + #size-cells = <0>; + power-domains = <&ps_nub_spmi4>; + status = "disabled"; + }; + + aop_spmi0: spmi@510914000 { + compatible = "apple,t6041-spmi", "apple,t8103-spmi"; + reg = <0x5 0x10914000 0x0 0x100>; + #address-cells = <2>; + #size-cells = <0>; + /* Shared with AOP firmware; no AP bus ownership handoff yet. */ + status = "disabled"; + }; + }; #include "t6040-pmgr.dtsi" From 90e3fa40ecf297a0ecefd74f9a3a57428bb2f55e Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 15:06:33 +1100 Subject: [PATCH 272/388] arm64: dts: apple: describe the J616s additional Abbey PMICs Expose the Abbey PMICs at USID 14 on NUB SPMI buses 1 and 2 through the existing NVMEM binding. Enable their bus power states and shared parent. Use polled bus access as on NUB SPMI0 so firmware-owned peripheral interrupts remain untouched. Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/boot/dts/apple/t6040-j616s.dts | 31 +++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/arch/arm64/boot/dts/apple/t6040-j616s.dts b/arch/arm64/boot/dts/apple/t6040-j616s.dts index 500d9fe370ea9..8b5156308068d 100644 --- a/arch/arm64/boot/dts/apple/t6040-j616s.dts +++ b/arch/arm64/boot/dts/apple/t6040-j616s.dts @@ -4360,3 +4360,34 @@ }; #include "t6040-j616s-usb3.dtsi" + +/* Additional Abbey PMICs on the two independent NUB SPMI buses. */ +&nub_spmi1 { + status = "okay"; + + pmic@e { + compatible = "apple,abbey-pmic", "apple,spmi-nvmem"; + reg = <0xe SPMI_USID>; + }; +}; + +&nub_spmi2 { + status = "okay"; + + pmic@e { + compatible = "apple,abbey-pmic", "apple,spmi-nvmem"; + reg = <0xe SPMI_USID>; + }; +}; + +&ps_nub_spmi_ahb { + status = "okay"; +}; + +&ps_nub_spmi1 { + status = "okay"; +}; + +&ps_nub_spmi2 { + status = "okay"; +}; From 706908f638494abe6d378f96db2b8a0716738e0a Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 15:09:30 +1100 Subject: [PATCH 273/388] arm64: dts: apple: describe T6040 SIO, panel SPI and QSPIMC Describe the SIO DMA coprocessor and its mailbox, panel SPI controller and firmware NOR controller with their register, interrupt, clock and power resources. Extend the SIO and QSPIMC bindings for T6041. Keep them disabled pending firmware-table, panel-ownership and flash-write-policy qualification. Assisted-by: LLM Signed-off-by: Ryan Murray --- .../devicetree/bindings/dma/apple,sio.yaml | 1 + .../devicetree/bindings/spi/apple,qspi.yaml | 1 + arch/arm64/boot/dts/apple/t6040.dtsi | 61 +++++++++++++++++++ 3 files changed, 63 insertions(+) diff --git a/Documentation/devicetree/bindings/dma/apple,sio.yaml b/Documentation/devicetree/bindings/dma/apple,sio.yaml index dbaaa2d3d1a1a..ab6bdf3686971 100644 --- a/Documentation/devicetree/bindings/dma/apple,sio.yaml +++ b/Documentation/devicetree/bindings/dma/apple,sio.yaml @@ -23,6 +23,7 @@ properties: - enum: - apple,t6000-sio - apple,t6030-sio + - apple,t6041-sio - apple,t8103-sio - const: apple,sio diff --git a/Documentation/devicetree/bindings/spi/apple,qspi.yaml b/Documentation/devicetree/bindings/spi/apple,qspi.yaml index 39924afc9a8b6..0b98af1ad68d2 100644 --- a/Documentation/devicetree/bindings/spi/apple,qspi.yaml +++ b/Documentation/devicetree/bindings/spi/apple,qspi.yaml @@ -22,6 +22,7 @@ properties: enum: - apple,t8122-qspi - apple,t8132-qspi + - apple,t6041-qspi - apple,t8140-qspi reg: diff --git a/arch/arm64/boot/dts/apple/t6040.dtsi b/arch/arm64/boot/dts/apple/t6040.dtsi index 0a2d7104a103b..1be0417d0a8b8 100644 --- a/arch/arm64/boot/dts/apple/t6040.dtsi +++ b/arch/arm64/boot/dts/apple/t6040.dtsi @@ -2070,6 +2070,67 @@ status = "disabled"; }; + /* The QSPIMC input is ADT clock 434 (200 MHz). */ + clk_200m: clock-200m { + compatible = "fixed-clock"; + #clock-cells = <0>; + clock-frequency = <200000000>; + clock-output-names = "clk_200m"; + }; + + spi4: spi@429110000 { + compatible = "apple,t6041-spi", "apple,t8103-spi"; + reg = <0x4 0x29110000 0x0 0x4000>; + interrupt-parent = <&aic>; + interrupts = ; + clocks = <&clkref>; + power-domains = <&ps_spi4>; + #address-cells = <1>; + #size-cells = <0>; + /* Panel/TCON control belongs to the display firmware. */ + status = "disabled"; + }; + + qspi: spi@429118000 { + compatible = "apple,t6041-qspi"; + reg = <0x4 0x29118000 0x0 0x4800>; + interrupt-parent = <&aic>; + interrupts = ; + clocks = <&clk_200m>; + power-domains = <&ps_qspi>; + #address-cells = <1>; + #size-cells = <0>; + /* The firmware NOR needs a qualified partition/write policy. */ + status = "disabled"; + }; + + /* The loader supplies the SIO firmware regions and protocol tables. */ + sio: dma-controller@429e00000 { + compatible = "apple,t6041-sio", "apple,sio"; + reg = <0x4 0x29e00000 0x0 0x8000>; + dma-channels = <128>; + #dma-cells = <1>; + mboxes = <&sio_mbox>; + iommus = <&sio_dart 0>; + power-domains = <&ps_sio_cpu>; + status = "disabled"; + }; + + sio_mbox: mailbox@429e08000 { + compatible = "apple,t6041-asc-mailbox", "apple,asc-mailbox-v4"; + reg = <0x4 0x29e08000 0x0 0x4000>; + interrupt-parent = <&aic>; + interrupts = , + , + , + ; + interrupt-names = "send-empty", "send-not-empty", + "recv-empty", "recv-not-empty"; + #mbox-cells = <0>; + power-domains = <&ps_sio_cpu>; + status = "disabled"; + }; + }; #include "t6040-pmgr.dtsi" From 1821a91f9e88472630ec351d46b8066b02d08891 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 15:10:43 +1100 Subject: [PATCH 274/388] arm64: dts: apple: describe the T6040 external display processors Describe both external display coprocessors and mailboxes, their PIODMA streams, all four display crossbars and the fixed HDMI PHY with its mailbox. Keep them disabled until the loader can supply a qualified live display handoff. Admit the existing T6041 external DCP driver compatible in the binding and require its bandwidth handoff when enabled. Assisted-by: LLM Signed-off-by: Ryan Murray --- .../bindings/display/apple,dcp.yaml | 18 ++- arch/arm64/boot/dts/apple/t6040.dtsi | 137 ++++++++++++++++++ 2 files changed, 151 insertions(+), 4 deletions(-) diff --git a/Documentation/devicetree/bindings/display/apple,dcp.yaml b/Documentation/devicetree/bindings/display/apple,dcp.yaml index d79ad457f7703..e9adc197d0dea 100644 --- a/Documentation/devicetree/bindings/display/apple,dcp.yaml +++ b/Documentation/devicetree/bindings/display/apple,dcp.yaml @@ -34,6 +34,7 @@ properties: - apple,t6000-dcpext - apple,t6020-dcpext - const: apple,dcpext + - const: apple,t6041-dcpext - items: - const: apple,t8140-dcpext - const: apple,dcp @@ -372,10 +373,19 @@ allOf: pattern: '^(dp-xbar|typec[0-9]+)$' - if: not: - properties: - compatible: - contains: - const: apple,t8132-dcp + anyOf: + - properties: + compatible: + contains: + const: apple,t8132-dcp + - properties: + compatible: + contains: + const: apple,t6041-dcpext + status: + const: disabled + required: + - status then: required: - apple,bw-scratch diff --git a/arch/arm64/boot/dts/apple/t6040.dtsi b/arch/arm64/boot/dts/apple/t6040.dtsi index 1be0417d0a8b8..49e76b24ed648 100644 --- a/arch/arm64/boot/dts/apple/t6040.dtsi +++ b/arch/arm64/boot/dts/apple/t6040.dtsi @@ -2131,6 +2131,143 @@ status = "disabled"; }; + dcpext0: dcp@3a2e00000 { + compatible = "apple,t6041-dcpext"; + reg = <0x3 0xa2e00000 0x0 0x4000>, + <0x3 0xa0000000 0x0 0x4000000>, + <0x3 0xa2240000 0x0 0x10000>, + <0x3 0xa23a8000 0x0 0x4000>, + <0x3 0xa2800000 0x0 0x800000>; + reg-names = "coproc", "disp-0", "disp-1", "disp-2", "disp-3"; + mboxes = <&dcpext0_mbox>; + mbox-names = "mbox"; + iommus = <&dcpext0_dart 23>; + power-domains = <&ps_dispext0_cpu>; + /* The loader must supply the bandwidth and live display handoff. */ + status = "disabled"; + + dispext0_piodma: piodma { + iommus = <&dispext0_dart 16>; + }; + }; + + dcpext0_mbox: mailbox@3a2e08000 { + compatible = "apple,t6041-asc-mailbox", "apple,asc-mailbox-v4"; + reg = <0x3 0xa2e08000 0x0 0x4000>; + interrupt-parent = <&aic>; + interrupts = , + , + , + ; + interrupt-names = "send-empty", "send-not-empty", + "recv-empty", "recv-not-empty"; + #mbox-cells = <0>; + power-domains = <&ps_dispext0_cpu>; + status = "disabled"; + }; + + dcpext1: dcp@602e00000 { + compatible = "apple,t6041-dcpext"; + reg = <0x6 0x02e00000 0x0 0x4000>, + <0x6 0x00000000 0x0 0x4000000>, + <0x6 0x02240000 0x0 0x10000>, + <0x6 0x023a8000 0x0 0x4000>, + <0x6 0x02800000 0x0 0x800000>; + reg-names = "coproc", "disp-0", "disp-1", "disp-2", "disp-3"; + mboxes = <&dcpext1_mbox>; + mbox-names = "mbox"; + iommus = <&dcpext1_dart 23>; + power-domains = <&ps_dispext1_cpu>; + /* The loader must supply the bandwidth and live display handoff. */ + status = "disabled"; + + dispext1_piodma: piodma { + iommus = <&dispext1_dart 16>; + }; + }; + + dcpext1_mbox: mailbox@602e08000 { + compatible = "apple,t6041-asc-mailbox", "apple,asc-mailbox-v4"; + reg = <0x6 0x02e08000 0x0 0x4000>; + interrupt-parent = <&aic>; + interrupts = , + , + , + ; + interrupt-names = "send-empty", "send-not-empty", + "recv-empty", "recv-not-empty"; + #mbox-cells = <0>; + power-domains = <&ps_dispext1_cpu>; + status = "disabled"; + }; + + atcphy0_xbar: mux-controller@38324c000 { + compatible = "apple,t6041-display-crossbar"; + reg = <0x3 0x8324c000 0x0 0x4000>; + #mux-control-cells = <1>; + status = "disabled"; + }; + + atcphy1_xbar: mux-controller@38b24c000 { + compatible = "apple,t6041-display-crossbar"; + reg = <0x3 0x8b24c000 0x0 0x4000>; + #mux-control-cells = <1>; + status = "disabled"; + }; + + atcphy2_xbar: mux-controller@39324c000 { + compatible = "apple,t6041-display-crossbar"; + reg = <0x3 0x9324c000 0x0 0x4000>; + #mux-control-cells = <1>; + status = "disabled"; + }; + + atcphy3_xbar: mux-controller@39b24c000 { + compatible = "apple,t6041-display-crossbar"; + reg = <0x3 0x9b24c000 0x0 0x4000>; + #mux-control-cells = <1>; + status = "disabled"; + }; + + /* The fourth ATC is a fixed DisplayPort output for the HDMI bridge. */ + atcphy3: phy@39aa90000 { + compatible = "apple,t6040-atcphy"; + reg = <0x3 0x9aa90000 0x0 0x4000>, + <0x3 0x9a800000 0x0 0x4000>, + <0x3 0x9b000000 0x0 0x30000>, + <0x3 0x9b200000 0x0 0x4000>, + <0x3 0x9b140000 0x0 0x4000>, + <0x3 0x9b0c0000 0x0 0x10000>, + <0x3 0x98000000 0x0 0x8000>, + <0x3 0x98020000 0x0 0x4000>, + <0x3 0x9b244000 0x0 0x4000>, + <0x3 0x9b250000 0x0 0x4000>; + reg-names = "bank0", "bank1", "core", "cfg", "cpu", "cpu-data", + "axi2af", "axi2af-lioa", "fabric", "common-cfg"; + #phy-cells = <1>; + #reset-cells = <0>; + power-domains = <&ps_atc3_common_dp>; + mboxes = <&atcphy3_mbox>; + mbox-names = "phy-cpu"; + nonposted-mmio; + status = "disabled"; + }; + + atcphy3_mbox: mailbox@39b148000 { + compatible = "apple,t6041-asc-mailbox", "apple,asc-mailbox-v4"; + reg = <0x3 0x9b148000 0x0 0x4000>; + interrupt-parent = <&aic>; + interrupts = , + , + , + ; + interrupt-names = "send-empty", "send-not-empty", + "recv-empty", "recv-not-empty"; + #mbox-cells = <0>; + power-domains = <&ps_atc3_phymxwrap>; + status = "disabled"; + }; + }; #include "t6040-pmgr.dtsi" From ca1fcc36d96f73986e062912d83dfd7ad672888d Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 15:12:20 +1100 Subject: [PATCH 275/388] arm64: dts: apple: make the J616s loader placeholders warning-free Leave GPU reserved-memory reg properties absent until the loader supplies their addresses. The loader sets these properties rather than updating them in place. Give the DCP child address space an explicit identity mapping, as on the other Apple display descriptions. Assisted-by: LLM Signed-off-by: Ryan Murray --- Documentation/devicetree/bindings/display/apple,dcp.yaml | 2 ++ arch/arm64/boot/dts/apple/t6040-j616s.dts | 6 ------ arch/arm64/boot/dts/apple/t6040.dtsi | 1 + 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/Documentation/devicetree/bindings/display/apple,dcp.yaml b/Documentation/devicetree/bindings/display/apple,dcp.yaml index e9adc197d0dea..ee14690fb6319 100644 --- a/Documentation/devicetree/bindings/display/apple,dcp.yaml +++ b/Documentation/devicetree/bindings/display/apple,dcp.yaml @@ -277,6 +277,8 @@ properties: '#size-cells': const: 2 + ranges: true + piodma: type: object properties: diff --git a/arch/arm64/boot/dts/apple/t6040-j616s.dts b/arch/arm64/boot/dts/apple/t6040-j616s.dts index 8b5156308068d..ad27eacdc07dd 100644 --- a/arch/arm64/boot/dts/apple/t6040-j616s.dts +++ b/arch/arm64/boot/dts/apple/t6040-j616s.dts @@ -82,7 +82,6 @@ * segment-ranges[0]); the driver identifies the build from it. */ gpu_fw_text: gpu-fw-text { - reg = <0 0 0 0>; status = "disabled"; no-map; }; @@ -92,7 +91,6 @@ * places its crash log here. */ gpu_fw_data: gpu-fw-data { - reg = <0 0 0 0>; status = "disabled"; no-map; }; @@ -103,25 +101,21 @@ * the boot loader adds them to /memory. */ uat_ttbs: uat-ttbs { - reg = <0 0 0 0>; status = "disabled"; no-map; }; uat_pagetables: uat-pagetables { - reg = <0 0 0 0>; status = "disabled"; no-map; }; uat_l2_pagetables: uat-l2-pagetables { - reg = <0 0 0 0>; status = "disabled"; no-map; }; uat_handoff: uat-handoff { - reg = <0 0 0 0>; status = "disabled"; no-map; }; diff --git a/arch/arm64/boot/dts/apple/t6040.dtsi b/arch/arm64/boot/dts/apple/t6040.dtsi index 49e76b24ed648..d35c5f9f03e1e 100644 --- a/arch/arm64/boot/dts/apple/t6040.dtsi +++ b/arch/arm64/boot/dts/apple/t6040.dtsi @@ -680,6 +680,7 @@ /* the boot loader describes the piodma's regions with 2+2 cells */ #address-cells = <2>; #size-cells = <2>; + ranges; /* enabled once the boot loader has handed the display over */ status = "disabled"; From 4c0bf2ae5c7180b83d2c3bc66422765f88b5990f Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 15:14:05 +1100 Subject: [PATCH 276/388] arm64: dts: apple: describe the T6040 Neural Engine Describe the engine, its interrupt and three DART translation banks. Extend the ANE binding for the single-interrupt T6041 firmware interface and leave the engine disabled until that interface has a Linux backend. Assisted-by: LLM Signed-off-by: Ryan Murray --- .../devicetree/bindings/npu/apple,ane.yaml | 27 ++++++++++++++++--- arch/arm64/boot/dts/apple/t6040.dtsi | 12 +++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/Documentation/devicetree/bindings/npu/apple,ane.yaml b/Documentation/devicetree/bindings/npu/apple,ane.yaml index 79cf942654f7b..feae5b133ac2f 100644 --- a/Documentation/devicetree/bindings/npu/apple,ane.yaml +++ b/Documentation/devicetree/bindings/npu/apple,ane.yaml @@ -15,7 +15,9 @@ description: drives the engine through its register window. On T602x and T8112 a coprocessor runs the ANE firmware and the host talks to it through an ASC mailbox. T8140 has two interrupts and one system power domain; its - coprocessor uses a different firmware interface. + coprocessor uses a different firmware interface. T6041 likewise needs a + generation-specific firmware backend; it has one interrupt and its CPU + power state supplies the engine through the ANE system domain. properties: compatible: @@ -25,6 +27,7 @@ properties: - apple,t6020-ane - apple,t6021-ane - apple,t6022-ane + - apple,t6041-ane - apple,t8112-ane - apple,t8140-ane @@ -87,19 +90,35 @@ required: - power-domains allOf: + - if: + properties: + compatible: + const: apple,t8140-ane + then: + properties: + interrupts: + minItems: 2 + - if: + properties: + compatible: + const: apple,t6041-ane + then: + properties: + interrupts: + maxItems: 1 - if: properties: compatible: contains: - const: apple,t8140-ane + enum: + - apple,t8140-ane + - apple,t6041-ane then: properties: reg: maxItems: 1 reg-names: maxItems: 1 - interrupts: - minItems: 2 interrupt-names: false iommus: minItems: 3 diff --git a/arch/arm64/boot/dts/apple/t6040.dtsi b/arch/arm64/boot/dts/apple/t6040.dtsi index d35c5f9f03e1e..c83c8e8ce8f1f 100644 --- a/arch/arm64/boot/dts/apple/t6040.dtsi +++ b/arch/arm64/boot/dts/apple/t6040.dtsi @@ -2269,6 +2269,18 @@ status = "disabled"; }; + /* Firmware interface support is required before enabling the ANE. */ + ane: ane@484000000 { + compatible = "apple,t6041-ane"; + reg = <0x4 0x84000000 0x0 0x2000000>; + reg-names = "engine"; + interrupt-parent = <&aic>; + interrupts = ; + iommus = <&ane0_dart0 0>, <&ane0_dart1 0>, <&ane0_dart2 0>; + power-domains = <&ps_ane_cpu>; + status = "disabled"; + }; + }; #include "t6040-pmgr.dtsi" From e6f9117e65f4e9eb15c02c5a0b8f06c8c4704a1c Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 15:14:14 +1100 Subject: [PATCH 277/388] arm64: dts: apple: describe the T6040 video encoder Describe the codec, mailbox and coprocessor windows, all five interrupts, the five hardware power states and both DART banks using the AVE binding. Keep it disabled until firmware support is available. Assisted-by: LLM Signed-off-by: Ryan Murray --- .../bindings/media/apple,t8140-ave.yaml | 8 +++++--- arch/arm64/boot/dts/apple/t6040.dtsi | 20 +++++++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/Documentation/devicetree/bindings/media/apple,t8140-ave.yaml b/Documentation/devicetree/bindings/media/apple,t8140-ave.yaml index 110ebbc3d09e2..61e10feb5df71 100644 --- a/Documentation/devicetree/bindings/media/apple,t8140-ave.yaml +++ b/Documentation/devicetree/bindings/media/apple,t8140-ave.yaml @@ -4,19 +4,21 @@ $id: http://devicetree.org/schemas/media/apple,t8140-ave.yaml# $schema: http://devicetree.org/meta-schemas/core.yaml# -title: Apple T8140 video encoder +title: Apple video encoder maintainers: - Miguel Cruz <7362641+Acelogic@users.noreply.github.com> description: | - The T8140 AVE coprocessor encodes video using firmware preloaded by the + The T6041 and T8140 AVE coprocessors encode video using firmware preloaded by the bootloader. Its two DART instances translate the same stream. The bootloader supplies two reserved firmware regions and their IOVA mappings. properties: compatible: - const: apple,t8140-ave + enum: + - apple,t6041-ave + - apple,t8140-ave reg: items: diff --git a/arch/arm64/boot/dts/apple/t6040.dtsi b/arch/arm64/boot/dts/apple/t6040.dtsi index c83c8e8ce8f1f..84e96c3a9135e 100644 --- a/arch/arm64/boot/dts/apple/t6040.dtsi +++ b/arch/arm64/boot/dts/apple/t6040.dtsi @@ -2281,6 +2281,26 @@ status = "disabled"; }; + /* The video encoder stays disabled until its firmware backend is available. */ + ave: video-codec@283100000 { + compatible = "apple,t6041-ave"; + reg = <0x2 0x83100000 0x0 0x45c000>, + <0x2 0x83050000 0x0 0x4000>, + <0x2 0x83800000 0x0 0x800000>; + reg-names = "codec", "mailbox", "coproc"; + interrupt-parent = <&aic>; + interrupts = , + , + , + , + ; + power-domains = <&ps_venc0_dma>, <&ps_venc0_pipe4>, + <&ps_venc0_pipe5>, <&ps_venc0_me0>, <&ps_venc0_me1>; + power-domain-names = "dma", "pipe4", "pipe5", "me0", "me1"; + iommus = <&ave0_dart0 0>, <&ave0_dart1 0>; + status = "disabled"; + }; + }; #include "t6040-pmgr.dtsi" From d475557caa6287d0eefe05971ca9641bd830366b Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 15:14:43 +1100 Subject: [PATCH 278/388] arm64: dts: apple: describe the J616s SD reader PCIe port Describe port 1, its interrupt, reset and clock-request pins and its separate DART mapping. Leave the port disabled because its SMC power switch is not a hexadecimal gPxx GPIO and requires a separate service binding. Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/boot/dts/apple/t6040-j616s.dts | 33 +++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/arch/arm64/boot/dts/apple/t6040-j616s.dts b/arch/arm64/boot/dts/apple/t6040-j616s.dts index ad27eacdc07dd..132f7f3c38773 100644 --- a/arch/arm64/boot/dts/apple/t6040-j616s.dts +++ b/arch/arm64/boot/dts/apple/t6040-j616s.dts @@ -4385,3 +4385,36 @@ &ps_nub_spmi2 { status = "okay"; }; + +&pinctrl { + sd_clkreq_pins: sd-clkreq-pins { + pinmux = ; + }; +}; + +&pcie0 { + interrupts = , + ; + bus-range = <0 2>; + iommu-map = <0x100 &pcie_dart 1 2>, <0x200 &apcie1_dart 1 1>; + + port01: pci@1,0 { + device_type = "pci"; + reg = <0x800 0 0 0 0>; + reset-gpios = <&pinctrl 5 GPIO_ACTIVE_LOW>; + pinctrl-names = "default"; + pinctrl-0 = <&sd_clkreq_pins>; + #address-cells = <3>; + #size-cells = <2>; + ranges; + interrupt-controller; + #interrupt-cells = <1>; + interrupt-map-mask = <0 0 0 7>; + interrupt-map = <0 0 0 1 &port01 0 0 0 0>, + <0 0 0 2 &port01 0 0 0 1>, + <0 0 0 3 &port01 0 0 0 2>, + <0 0 0 4 &port01 0 0 0 3>; + /* The SD power switch needs an SMC service binding. */ + status = "disabled"; + }; +}; From edb0d3fb6e3cdf4d462c66ce0711d7ae68102757 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 15:15:26 +1100 Subject: [PATCH 279/388] arm64: dts: apple: describe the J616s console transports and tether variant Move the UART and DockChannel descriptions into the SoC tree, disabled by default, and provide a tether variant that enables its consoles while leaving the host-owned ATC0 port untouched. Give the normal board the PMP alias needed for its firmware handoff as well. Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/boot/dts/apple/Makefile | 1 + .../boot/dts/apple/t6040-j616s-tether.dts | 45 +++++++++++++++++++ arch/arm64/boot/dts/apple/t6040-j616s.dts | 1 + arch/arm64/boot/dts/apple/t6040.dtsi | 25 +++++++++++ 4 files changed, 72 insertions(+) create mode 100644 arch/arm64/boot/dts/apple/t6040-j616s-tether.dts diff --git a/arch/arm64/boot/dts/apple/Makefile b/arch/arm64/boot/dts/apple/Makefile index 007e204fdbc56..0f6e6714f7de7 100644 --- a/arch/arm64/boot/dts/apple/Makefile +++ b/arch/arm64/boot/dts/apple/Makefile @@ -95,6 +95,7 @@ dtb-$(CONFIG_ARCH_APPLE) += t6032-j575d.dtb dtb-$(CONFIG_ARCH_APPLE) += t6034-j514m.dtb dtb-$(CONFIG_ARCH_APPLE) += t6034-j516m.dtb dtb-$(CONFIG_ARCH_APPLE) += t6040-j616s.dtb +dtb-$(CONFIG_ARCH_APPLE) += t6040-j616s-tether.dtb dtb-$(CONFIG_ARCH_APPLE) += t8112-j413.dtb dtb-$(CONFIG_ARCH_APPLE) += t8112-j415.dtb dtb-$(CONFIG_ARCH_APPLE) += t8112-j473.dtb diff --git a/arch/arm64/boot/dts/apple/t6040-j616s-tether.dts b/arch/arm64/boot/dts/apple/t6040-j616s-tether.dts new file mode 100644 index 0000000000000..619e09a88d091 --- /dev/null +++ b/arch/arm64/boot/dts/apple/t6040-j616s-tether.dts @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-2.0+ OR MIT +/* J616s tether console; the host owns ATC0 during the debug session. */ +#include "t6040-j616s.dts" + +/ { + aliases { + serial0 = &serial0; + }; +}; + +&serial0 { + status = "okay"; +}; + +&ps_uart0 { + status = "okay"; +}; + +&dcuart { + status = "okay"; +}; + +&hpm0 { + status = "disabled"; +}; + +&atcphy0 { + status = "disabled"; +}; + +&atcphy0_mbox { + status = "disabled"; +}; + +&usb_drd0 { + status = "disabled"; +}; + +&usb_drd0_dart_0 { + status = "disabled"; +}; + +&usb_drd0_dart_1 { + status = "disabled"; +}; diff --git a/arch/arm64/boot/dts/apple/t6040-j616s.dts b/arch/arm64/boot/dts/apple/t6040-j616s.dts index 132f7f3c38773..deaedfa9b12ef 100644 --- a/arch/arm64/boot/dts/apple/t6040-j616s.dts +++ b/arch/arm64/boot/dts/apple/t6040-j616s.dts @@ -29,6 +29,7 @@ disp0-piodma = &disp0_piodma; /* the boot loader finds the GPU node through this alias */ gpu = &gpu; + pmp = &pmp; /* the boot loader reserves the SEP firmware through this alias */ sep = &sep; }; diff --git a/arch/arm64/boot/dts/apple/t6040.dtsi b/arch/arm64/boot/dts/apple/t6040.dtsi index 84e96c3a9135e..0b81ceb697d3a 100644 --- a/arch/arm64/boot/dts/apple/t6040.dtsi +++ b/arch/arm64/boot/dts/apple/t6040.dtsi @@ -2301,6 +2301,31 @@ status = "disabled"; }; + serial0: serial@429200000 { + compatible = "apple,s5l-uart"; + reg = <0x4 0x29200000 0x0 0x4000>; + reg-io-width = <4>; + interrupt-parent = <&aic>; + interrupts = ; + clocks = <&clkref>, <&clkref>; + clock-names = "uart", "clk_uart_baud0"; + power-domains = <&ps_uart0>; + status = "disabled"; + }; + + dcuart: serial@50883c000 { + compatible = "apple,dockchannel-uart"; + reg = <0x5 0x0883c000 0x0 0x4000>, + <0x5 0x08838000 0x0 0x4000>, + <0x5 0x0880c000 0x0 0x4000>; + reg-names = "data", "config", "irq"; + interrupt-parent = <&aic>; + interrupts = ; + apple,channel = <2>; + apple,sw-drain; + status = "disabled"; + }; + }; #include "t6040-pmgr.dtsi" From 658fbd4f89a35ea9668ec0ec4c8ca95c642db5d2 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 15:19:09 +1100 Subject: [PATCH 280/388] arm64: dts: apple: complete J616s console supply and fixed-PHY constraints Enable the UART supply parent with the tether UART and omit USB reset-provider cells on the fixed DisplayPort PHY, as required by its binding. Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/boot/dts/apple/t6040-j616s-tether.dts | 4 ++++ arch/arm64/boot/dts/apple/t6040.dtsi | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/arch/arm64/boot/dts/apple/t6040-j616s-tether.dts b/arch/arm64/boot/dts/apple/t6040-j616s-tether.dts index 619e09a88d091..8027360847204 100644 --- a/arch/arm64/boot/dts/apple/t6040-j616s-tether.dts +++ b/arch/arm64/boot/dts/apple/t6040-j616s-tether.dts @@ -12,6 +12,10 @@ status = "okay"; }; +&ps_uart_p { + status = "okay"; +}; + &ps_uart0 { status = "okay"; }; diff --git a/arch/arm64/boot/dts/apple/t6040.dtsi b/arch/arm64/boot/dts/apple/t6040.dtsi index 0b81ceb697d3a..2cf2f626c8d68 100644 --- a/arch/arm64/boot/dts/apple/t6040.dtsi +++ b/arch/arm64/boot/dts/apple/t6040.dtsi @@ -2246,7 +2246,6 @@ reg-names = "bank0", "bank1", "core", "cfg", "cpu", "cpu-data", "axi2af", "axi2af-lioa", "fabric", "common-cfg"; #phy-cells = <1>; - #reset-cells = <0>; power-domains = <&ps_atc3_common_dp>; mboxes = <&atcphy3_mbox>; mbox-names = "phy-cpu"; From 16b681646cf555d26833fd24464d47caf67dbb09 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 15:20:29 +1100 Subject: [PATCH 281/388] arm64: dts: apple: describe the T6040 ACIO mailboxes and DARTs Describe all three USB4 coprocessor mailboxes and their NHI translation banks with their interrupt and power resources. Keep them disabled until the host-router reset, SRAM and stream handoff is qualified. Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/boot/dts/apple/t6040.dtsi | 75 ++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/arch/arm64/boot/dts/apple/t6040.dtsi b/arch/arm64/boot/dts/apple/t6040.dtsi index 2cf2f626c8d68..e101bd2596d78 100644 --- a/arch/arm64/boot/dts/apple/t6040.dtsi +++ b/arch/arm64/boot/dts/apple/t6040.dtsi @@ -2325,6 +2325,81 @@ status = "disabled"; }; + /* ACIO 0 remains disabled until its host-router handoff is supported. */ + usb4_0_mbox: mailbox@381108000 { + compatible = "apple,t6041-asc-mailbox", "apple,asc-mailbox-v4"; + reg = <0x3 0x81108000 0x0 0x4000>; + interrupt-parent = <&aic>; + interrupts = , + , + , + ; + interrupt-names = "send-empty", "send-not-empty", + "recv-empty", "recv-not-empty"; + #mbox-cells = <0>; + power-domains = <&ps_atc0_cio>; + status = "disabled"; + }; + + usb4_0_dart: iommu@381a80000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x3 0x81a80000 0x0 0xc000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + status = "disabled"; + }; + + /* ACIO 1 remains disabled until its host-router handoff is supported. */ + usb4_1_mbox: mailbox@389108000 { + compatible = "apple,t6041-asc-mailbox", "apple,asc-mailbox-v4"; + reg = <0x3 0x89108000 0x0 0x4000>; + interrupt-parent = <&aic>; + interrupts = , + , + , + ; + interrupt-names = "send-empty", "send-not-empty", + "recv-empty", "recv-not-empty"; + #mbox-cells = <0>; + power-domains = <&ps_atc1_cio>; + status = "disabled"; + }; + + usb4_1_dart: iommu@389a80000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x3 0x89a80000 0x0 0xc000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + status = "disabled"; + }; + + /* ACIO 2 remains disabled until its host-router handoff is supported. */ + usb4_2_mbox: mailbox@391108000 { + compatible = "apple,t6041-asc-mailbox", "apple,asc-mailbox-v4"; + reg = <0x3 0x91108000 0x0 0x4000>; + interrupt-parent = <&aic>; + interrupts = , + , + , + ; + interrupt-names = "send-empty", "send-not-empty", + "recv-empty", "recv-not-empty"; + #mbox-cells = <0>; + power-domains = <&ps_atc2_cio>; + status = "disabled"; + }; + + usb4_2_dart: iommu@391a80000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x3 0x91a80000 0x0 0xc000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + status = "disabled"; + }; + }; #include "t6040-pmgr.dtsi" From 82f3d4154f1542d0dd19331950cc803ec489774b Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 15:26:18 +1100 Subject: [PATCH 282/388] arm64: dts: apple: complete active T6040 power-state parent links Connect the three ATC common states, AVD, PMP and PMS SRAM to their ADT hardware parents. Enable the two missing fabric providers, both marked critical by the ADT, so active consumers never depend on disabled suppliers. Retain the existing additional PHY wrapper ordering dependencies. Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/boot/dts/apple/t6040-pmgr.dtsi | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/arch/arm64/boot/dts/apple/t6040-pmgr.dtsi b/arch/arm64/boot/dts/apple/t6040-pmgr.dtsi index 18ec5df7d33bf..eab3825a95c26 100644 --- a/arch/arm64/boot/dts/apple/t6040-pmgr.dtsi +++ b/arch/arm64/boot/dts/apple/t6040-pmgr.dtsi @@ -297,6 +297,7 @@ #power-domain-cells = <0>; #reset-cells = <0>; label = "avd_sys"; + power-domains = <&ps_fab0_soc>; }; ps_afc: power-controller@178 { @@ -315,6 +316,7 @@ #reset-cells = <0>; label = "pmp"; apple,always-on; + power-domains = <&ps_pms>; }; ps_pms_sram: power-controller@418 { @@ -324,6 +326,7 @@ #reset-cells = <0>; label = "pms_sram"; apple,always-on; + power-domains = <&ps_pms>; }; ps_fab6_soc: power-controller@480 { @@ -358,6 +361,7 @@ #reset-cells = <0>; label = "atc0_common"; apple,always-on; + power-domains = <&ps_fab2_soc>; }; ps_atc1_common: power-controller@3b8 { @@ -367,6 +371,7 @@ #reset-cells = <0>; label = "atc1_common"; apple,always-on; + power-domains = <&ps_fab2_soc>; }; ps_atc2_common: power-controller@3e0 { @@ -376,6 +381,7 @@ #reset-cells = <0>; label = "atc2_common"; apple,always-on; + power-domains = <&ps_fab2_soc>; }; }; @@ -1380,7 +1386,6 @@ label = "fab0_soc"; power-domains = <&ps_afi>; apple,always-on; - status = "disabled"; }; ps_amcc0: power-controller@180 { @@ -1648,7 +1653,6 @@ label = "fab2_soc"; power-domains = <&ps_afi>; apple,always-on; - status = "disabled"; }; ps_dispext0_sys: power-controller@340 { From 40cb9c2f490063278df238b72213d3a2051b6a8f Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 15:56:32 +1100 Subject: [PATCH 283/388] arm64: dts: apple: keep T6041 SIO on its own firmware compatible The T6041 firmware protocol has not been qualified against the legacy SIO backend. Describe its resources with a generation-specific compatible, so a loader cannot accidentally bind that backend by enabling the node. Assisted-by: LLM Signed-off-by: Ryan Murray --- .../devicetree/bindings/dma/apple,sio.yaml | 16 +++++++++------- arch/arm64/boot/dts/apple/t6040.dtsi | 2 +- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Documentation/devicetree/bindings/dma/apple,sio.yaml b/Documentation/devicetree/bindings/dma/apple,sio.yaml index ab6bdf3686971..495adc52c7e6e 100644 --- a/Documentation/devicetree/bindings/dma/apple,sio.yaml +++ b/Documentation/devicetree/bindings/dma/apple,sio.yaml @@ -19,13 +19,15 @@ allOf: properties: compatible: - items: - - enum: - - apple,t6000-sio - - apple,t6030-sio - - apple,t6041-sio - - apple,t8103-sio - - const: apple,sio + oneOf: + - items: + - enum: + - apple,t6000-sio + - apple,t6030-sio + - apple,t8103-sio + - const: apple,sio + - description: T6041 firmware requires a generation-specific backend. + const: apple,t6041-sio reg: maxItems: 1 diff --git a/arch/arm64/boot/dts/apple/t6040.dtsi b/arch/arm64/boot/dts/apple/t6040.dtsi index e101bd2596d78..75c9b3f06ac21 100644 --- a/arch/arm64/boot/dts/apple/t6040.dtsi +++ b/arch/arm64/boot/dts/apple/t6040.dtsi @@ -2107,7 +2107,7 @@ /* The loader supplies the SIO firmware regions and protocol tables. */ sio: dma-controller@429e00000 { - compatible = "apple,t6041-sio", "apple,sio"; + compatible = "apple,t6041-sio"; reg = <0x4 0x29e00000 0x0 0x8000>; dma-channels = <128>; #dma-cells = <1>; From 9f1ce86eae38113b8bdb147b4daa8adda0d869cc Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 16:14:43 +1100 Subject: [PATCH 284/388] arm64: dts: apple: retain the active J616s PCIe scan range Keep the existing host bus range while the SD port is disabled. Generic ECAM access does not filter unavailable OF ports, so describing that port must not expand downstream configuration probing before its power handoff is qualified. Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/boot/dts/apple/t6040-j616s.dts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arch/arm64/boot/dts/apple/t6040-j616s.dts b/arch/arm64/boot/dts/apple/t6040-j616s.dts index deaedfa9b12ef..e24205e1d40da 100644 --- a/arch/arm64/boot/dts/apple/t6040-j616s.dts +++ b/arch/arm64/boot/dts/apple/t6040-j616s.dts @@ -4396,7 +4396,7 @@ &pcie0 { interrupts = , ; - bus-range = <0 2>; + /* Widen bus-range only with the SD power/port ownership handoff. */ iommu-map = <0x100 &pcie_dart 1 2>, <0x200 &apcie1_dart 1 1>; port01: pci@1,0 { From c744beabc5e71f58b58d1baafb9637307e8580bc Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 17:45:07 +1100 Subject: [PATCH 285/388] ASoC: apple: macaudio: expose speaker variant straps from the device tree Allow a machine card to describe speaker identity inputs with speaker-id-gpios, least significant bit first. Expose their logical levels in a read-only volatile Speaker Variant ID control. Cards without the property retain their existing controls. Describe the two J616s speaker variant inputs on AP GPIO 52 and 53. Assisted-by: LLM Signed-off-by: Ryan Murray --- .../bindings/sound/apple,macaudio.yaml | 9 +++ arch/arm64/boot/dts/apple/t6040-j616s.dts | 2 + sound/soc/apple/macaudio.c | 60 +++++++++++++++++++ 3 files changed, 71 insertions(+) diff --git a/Documentation/devicetree/bindings/sound/apple,macaudio.yaml b/Documentation/devicetree/bindings/sound/apple,macaudio.yaml index fdf3d1883a99d..755c5d195cf0f 100644 --- a/Documentation/devicetree/bindings/sound/apple,macaudio.yaml +++ b/Documentation/devicetree/bindings/sound/apple,macaudio.yaml @@ -36,6 +36,15 @@ properties: Model name for presentation to users $ref: /schemas/types.yaml#/definitions/string + speaker-id-gpios: + minItems: 1 + maxItems: 8 + description: + Inputs encoding the fitted speaker assembly variant, least significant + bit first. The machine driver exposes their logical levels through the + read-only Speaker Variant ID control. GPIO polarity flags define each + bit's logical level. + patternProperties: "^dai-link(@[0-9a-f]+)?$": description: | diff --git a/arch/arm64/boot/dts/apple/t6040-j616s.dts b/arch/arm64/boot/dts/apple/t6040-j616s.dts index e24205e1d40da..f4b0aa85e2758 100644 --- a/arch/arm64/boot/dts/apple/t6040-j616s.dts +++ b/arch/arm64/boot/dts/apple/t6040-j616s.dts @@ -1258,6 +1258,8 @@ sound: sound { compatible = "apple,j616-macaudio", "apple,macaudio"; model = "MacBook Pro J616"; + speaker-id-gpios = <&pinctrl 52 GPIO_ACTIVE_HIGH>, + <&pinctrl 53 GPIO_ACTIVE_HIGH>; #address-cells = <1>; #size-cells = <0>; diff --git a/sound/soc/apple/macaudio.c b/sound/soc/apple/macaudio.c index cc1123e10f398..b5511ed056887 100644 --- a/sound/soc/apple/macaudio.c +++ b/sound/soc/apple/macaudio.c @@ -24,6 +24,7 @@ #include #include +#include #include #include #include @@ -62,6 +63,7 @@ #define MACAUDIO_NUM_FE_LINKS 3 /* milliseconds */ #define SPEAKER_LOCK_TIMEOUT 250 +#define MACAUDIO_MAX_SPEAKER_ID_GPIOS 8 enum macaudio_amp_type { AMP_NONE, @@ -166,6 +168,7 @@ struct macaudio_snd_data { bool has_sense; bool has_safety; unsigned int max_channels; + struct gpio_descs *speaker_id_gpios; struct macaudio_link_props { /* frontend props */ @@ -1430,6 +1433,50 @@ static const struct snd_soc_jack_pin macaudio_jack_pins[] = { }, }; +static int macaudio_speaker_id_info(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_info *uinfo) +{ + struct snd_soc_card *card = snd_kcontrol_chip(kcontrol); + struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(card); + + uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER; + uinfo->count = 1; + uinfo->value.integer.min = 0; + uinfo->value.integer.max = BIT(ma->speaker_id_gpios->ndescs) - 1; + uinfo->value.integer.step = 1; + + return 0; +} + +static int macaudio_speaker_id_get(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *uvalue) +{ + struct snd_soc_card *card = snd_kcontrol_chip(kcontrol); + struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(card); + unsigned int value = 0; + unsigned int i; + int ret; + + for (i = 0; i < ma->speaker_id_gpios->ndescs; i++) { + ret = gpiod_get_value_cansleep(ma->speaker_id_gpios->desc[i]); + if (ret < 0) + return ret; + if (ret) + value |= BIT(i); + } + uvalue->value.integer.value[0] = value; + + return 0; +} + +static const struct snd_kcontrol_new macaudio_speaker_id_control = { + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .name = "Speaker Variant ID", + .access = SNDRV_CTL_ELEM_ACCESS_READ | SNDRV_CTL_ELEM_ACCESS_VOLATILE, + .info = macaudio_speaker_id_info, + .get = macaudio_speaker_id_get, +}; + static int macaudio_probe(struct snd_soc_card *card) { struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(card); @@ -1460,6 +1507,9 @@ static int macaudio_probe(struct snd_soc_card *card) snd_jack_set_key(ma->jack.jack, SND_JACK_BTN_2, KEY_VOLUMEDOWN); snd_jack_set_key(ma->jack.jack, SND_JACK_BTN_3, KEY_VOICECOMMAND); + if (ma->speaker_id_gpios) + return snd_soc_add_card_controls(card, &macaudio_speaker_id_control, 1); + return ret; } @@ -2183,6 +2233,16 @@ static int macaudio_snd_platform_probe(struct platform_device *pdev) if (ret) return ret; + data->speaker_id_gpios = devm_gpiod_get_array_optional(dev, "speaker-id", + GPIOD_IN); + if (IS_ERR(data->speaker_id_gpios)) + return dev_err_probe(dev, PTR_ERR(data->speaker_id_gpios), + "Failed to get speaker ID inputs\n"); + if (data->speaker_id_gpios && + (!data->speaker_id_gpios->ndescs || + data->speaker_id_gpios->ndescs > MACAUDIO_MAX_SPEAKER_ID_GPIOS)) + return dev_err_probe(dev, -EINVAL, "Invalid speaker ID input count\n"); + /* * Without sense the PCM2 link is gone, and a card whose front-ends * are not the MCA's (the J616 jack) has no PCM2 RX widget at all. From 4d5617e813a0db14124c9139df6ee7dabc75ec21 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 18:12:58 +1100 Subject: [PATCH 286/388] ASoC: apple: macaudio: append variant identity after existing controls Register the optional read-only identity control in late_probe, after the existing card and codec controls have been created. Their numeric identifiers remain stable when a board gains speaker-id-gpios. Assisted-by: LLM Signed-off-by: Ryan Murray --- sound/soc/apple/macaudio.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/sound/soc/apple/macaudio.c b/sound/soc/apple/macaudio.c index b5511ed056887..96cd57aae29a8 100644 --- a/sound/soc/apple/macaudio.c +++ b/sound/soc/apple/macaudio.c @@ -1507,9 +1507,6 @@ static int macaudio_probe(struct snd_soc_card *card) snd_jack_set_key(ma->jack.jack, SND_JACK_BTN_2, KEY_VOLUMEDOWN); snd_jack_set_key(ma->jack.jack, SND_JACK_BTN_3, KEY_VOICECOMMAND); - if (ma->speaker_id_gpios) - return snd_soc_add_card_controls(card, &macaudio_speaker_id_control, 1); - return ret; } @@ -1634,6 +1631,13 @@ static int macaudio_late_probe(struct snd_soc_card *card) mutex_unlock(&ma->volume_lock_mutex); } + /* Append identity metadata without renumbering the existing controls. */ + if (ma->speaker_id_gpios) { + ret = snd_soc_add_card_controls(card, &macaudio_speaker_id_control, 1); + if (ret) + return ret; + } + /* The notifier walks the card's controls: only while the card is bound */ return register_pm_notifier(&ma->pm_nb); } From 84f47efb93f38852c0686537e0e1ff298b7de8d4 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 18:37:12 +1100 Subject: [PATCH 287/388] drm/asahi: trace G17 input conversion and scheduler hop boundaries Add static-key input-resolution and scheduler lifecycle traces. Join these to existing publish/complete and dma_fence/sched events using the engine/context/sequence and fence identity to separate conversion, inline setup, dependency/capacity waits, scheduler dispatch and firmware execution in multi-queue signal hops. Counters are read only when each tracepoint is enabled. The trace path does not poll a fence, allocate, request GPU power or alter scheduling. These are measurement boundaries; no unmeasured latency saving is claimed. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/asahi/g17/job.rs | 16 ++++++-- drivers/gpu/drm/asahi/g17/queue/mod.rs | 6 +++ drivers/gpu/drm/asahi/g17/trace.c | 15 +++++++ drivers/gpu/drm/asahi/g17/trace.h | 56 ++++++++++++++++++++++++++ drivers/gpu/drm/asahi/g17/trace.rs | 30 ++++++++++++++ drivers/gpu/drm/asahi/g17/trace_api.h | 4 ++ 6 files changed, 124 insertions(+), 3 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/job.rs b/drivers/gpu/drm/asahi/g17/job.rs index 61033aa9e52b4..ad9adcecf0f81 100644 --- a/drivers/gpu/drm/asahi/g17/job.rs +++ b/drivers/gpu/drm/asahi/g17/job.rs @@ -363,10 +363,17 @@ impl sched::JobImpl for Job { } fn prepare(job: &mut Self) -> Option { - job.backend.prepare(&job.packet) + super::trace::schedule(&job.packet, 5, 0); + let wait = job.backend.prepare(&job.packet); + super::trace::schedule(&job.packet, 6, i32::from(wait.is_some())); + wait } fn run(job: &mut Self) -> Result> { - job.backend.publish(job.packet.clone()).map(Some) + super::trace::schedule(&job.packet, 3, 0); + let result = job.backend.publish(job.packet.clone()).map(Some); + super::trace::schedule(&job.packet, 4, + result.as_ref().err().map_or(0, |error| error.to_errno())); + result } fn timed_out(job: &mut Timeout, finished: bool) -> sched::Status { // The scheduler retains the detached job throughout this callback. Keep @@ -430,6 +437,7 @@ impl Scheduler { early: bool, publication: Option, ) -> Result { + super::trace::schedule(&packet, 0, dependencies.len() as i32); let completed = packet.completion.fence(); let host_timestamps = packet.completion.has_timestamps(); // Only commands which actually write timestamps become host-prefix @@ -462,7 +470,9 @@ impl Scheduler { }; // All fallible scheduler setup precedes early hardware publication. The // scheduler still owns watchdog, credits and cancellation after push. - if !(early && self.backend.try_early(&packet)) { + let inline = early && self.backend.try_early(&packet); + super::trace::schedule(&packet, if inline { 1 } else { 2 }, 0); + if !inline { if packet.completion.needs_source_ready() { packet.completion.set_source_ready(ready.clone()); } diff --git a/drivers/gpu/drm/asahi/g17/queue/mod.rs b/drivers/gpu/drm/asahi/g17/queue/mod.rs index 16891524e25a0..d22c0f617f374 100644 --- a/drivers/gpu/drm/asahi/g17/queue/mod.rs +++ b/drivers/gpu/drm/asahi/g17/queue/mod.rs @@ -212,6 +212,9 @@ impl Queue { } // Only convert inputs when no independent host-prefix wait could keep // the retained source bookkeeping queued behind an external signal. + if engine == Engine::Compute { + super::trace::inputs(self.context.id(), order.sequence, 0, inputs.len(), None); + } let sources = if engine == Engine::Compute && !inputs.is_empty() && !destinations.has_outputs() && self.frontiers[0].iter().all(|(sequence, fences)| @@ -225,6 +228,9 @@ impl Queue { failure.as_ref().is_none_or(|(sequence, _)| !Order::contains(order.wait_through[index], *sequence))) { self.backend.input_sources(inputs) } else { None }; + if engine == Engine::Compute { + super::trace::inputs(self.context.id(), order.sequence, 1, inputs.len(), sources.as_deref()); + } // Only earlier render ordinals covered by the explicit logical prefix // can delay host publication. These links never become GPU dependencies. diff --git a/drivers/gpu/drm/asahi/g17/trace.c b/drivers/gpu/drm/asahi/g17/trace.c index 59f0094d129e9..62307838bb36e 100644 --- a/drivers/gpu/drm/asahi/g17/trace.c +++ b/drivers/gpu/drm/asahi/g17/trace.c @@ -70,3 +70,18 @@ void asahi_g17_trace_report(u8 role, u8 ring, const u32 *words) { trace_asahi_g17_report(role, ring, words, __arch_counter_get_cntvct()); } + +void asahi_g17_trace_inputs(u32 ctx, u64 seq, u8 phase, u32 inputs, u32 sources, + u32 waits, bool converted) +{ + if (trace_asahi_g17_inputs_enabled()) + trace_asahi_g17_inputs(ctx, seq, phase, inputs, sources, waits, + converted, __arch_counter_get_cntvct()); +} + +void asahi_g17_trace_schedule(u8 engine, u32 ctx, u64 seq, u8 phase, s32 detail) +{ + if (trace_asahi_g17_schedule_enabled()) + trace_asahi_g17_schedule(engine, ctx, seq, phase, detail, + __arch_counter_get_cntvct()); +} diff --git a/drivers/gpu/drm/asahi/g17/trace.h b/drivers/gpu/drm/asahi/g17/trace.h index 36e1b445f1b77..c8cecbcc7f400 100644 --- a/drivers/gpu/drm/asahi/g17/trace.h +++ b/drivers/gpu/drm/asahi/g17/trace.h @@ -158,6 +158,62 @@ TRACE_EVENT(asahi_g17_event, TP_printk("phase=%u counter=%llu", __entry->phase, __entry->counter) ); +/* Input conversion: phase 0 entry, 1 result. converted=0 also covers no live input. */ +TRACE_EVENT(asahi_g17_inputs, + TP_PROTO(u32 ctx, u64 seq, u8 phase, u32 inputs, u32 sources, u32 waits, + bool converted, u64 counter), + TP_ARGS(ctx, seq, phase, inputs, sources, waits, converted, counter), + TP_STRUCT__entry( + __field(u32, ctx) + __field(u64, seq) + __field(u8, phase) + __field(u32, inputs) + __field(u32, sources) + __field(u32, waits) + __field(bool, converted) + __field(u64, counter) + ), + TP_fast_assign( + __entry->ctx = ctx; + __entry->seq = seq; + __entry->phase = phase; + __entry->inputs = inputs; + __entry->sources = sources; + __entry->waits = waits; + __entry->converted = converted; + __entry->counter = counter; + ), + TP_printk("ctx=%u seq=%llu phase=%u inputs=%u sources=%u waits=%u converted=%u counter=%llu", + __entry->ctx, __entry->seq, __entry->phase, __entry->inputs, + __entry->sources, __entry->waits, __entry->converted, __entry->counter) +); + +/* phase: 0 enqueue, 1 inline published, 2 scheduler fallback, 3 run begin, + * 4 run end, 5 prepare begin, 6 prepare end. detail: dependencies, error or wait. */ +TRACE_EVENT(asahi_g17_schedule, + TP_PROTO(u8 engine, u32 ctx, u64 seq, u8 phase, s32 detail, u64 counter), + TP_ARGS(engine, ctx, seq, phase, detail, counter), + TP_STRUCT__entry( + __field(u8, engine) + __field(u32, ctx) + __field(u64, seq) + __field(u8, phase) + __field(s32, detail) + __field(u64, counter) + ), + TP_fast_assign( + __entry->engine = engine; + __entry->ctx = ctx; + __entry->seq = seq; + __entry->phase = phase; + __entry->detail = detail; + __entry->counter = counter; + ), + TP_printk("engine=%u ctx=%u seq=%llu phase=%u detail=%d counter=%llu", + __entry->engine, __entry->ctx, __entry->seq, __entry->phase, + __entry->detail, __entry->counter) +); + /* Raw 18-word records: trace=0, statistics=1, event=2. No unit inference. */ TRACE_EVENT(asahi_g17_report, TP_PROTO(u8 role, u8 ring, const u32 *words, u64 counter), diff --git a/drivers/gpu/drm/asahi/g17/trace.rs b/drivers/gpu/drm/asahi/g17/trace.rs index cd38c95193585..72197f0eac0b7 100644 --- a/drivers/gpu/drm/asahi/g17/trace.rs +++ b/drivers/gpu/drm/asahi/g17/trace.rs @@ -105,3 +105,33 @@ pub(super) fn report(role: u8, ring: u8, words: &[u32; super::fw::channels::EVEN // SAFETY: C synchronously copies the complete initialized 18-word array. unsafe { asahi_g17_trace_report(role, ring, words.as_ptr()) }; } + + +unsafe extern "C" { + fn asahi_g17_trace_inputs(ctx: u32, seq: u64, phase: u8, inputs: u32, + sources: u32, waits: u32, converted: bool); + fn asahi_g17_trace_schedule(engine: u8, ctx: u32, seq: u64, phase: u8, detail: i32); +} + +/// Input conversion boundaries. No fence status is polled for tracing. +pub(super) fn inputs(ctx: u32, seq: u64, phase: u8, inputs: usize, + sources: Option<&super::sync_input::Sources>) +{ + let (count, waits) = sources.map_or((0, 0), + |s| (s.sources.len(), s.publication_waits().len())); + // SAFETY: Bounded UAPI counts and plain values only. C reads the timer only + // behind this event's static key; no GPU registers or pointers are exposed. + unsafe { asahi_g17_trace_inputs(ctx, seq, phase, inputs as u32, + count as u32, waits as u32, sources.is_some()) }; +} + +/// Host scheduling boundaries, joined to publish/complete by engine/context/sequence. +pub(super) fn schedule(packet: &super::job::Packet, phase: u8, detail: i32) { + let engine = match packet.command { + super::command::Validated::Render { .. } => RENDER, + super::command::Validated::Compute { .. } => COMPUTE, + }; + // SAFETY: Plain values only; counter sampling is static-key gated in C. + unsafe { asahi_g17_trace_schedule(engine, packet.context.id(), + packet.order.sequence, phase, detail) }; +} diff --git a/drivers/gpu/drm/asahi/g17/trace_api.h b/drivers/gpu/drm/asahi/g17/trace_api.h index be5eff4c8161b..cd4d360c55595 100644 --- a/drivers/gpu/drm/asahi/g17/trace_api.h +++ b/drivers/gpu/drm/asahi/g17/trace_api.h @@ -22,6 +22,10 @@ void asahi_g17_trace_message(u8 kind, u64 value); void asahi_g17_trace_event(u8 phase); void asahi_g17_trace_teardown(u8 kind, u64 a, u64 b, u64 c); +void asahi_g17_trace_inputs(u32 ctx, u64 seq, u8 phase, u32 inputs, u32 sources, + u32 waits, bool converted); +void asahi_g17_trace_schedule(u8 engine, u32 ctx, u64 seq, u8 phase, s32 detail); + bool asahi_g17_trace_report_enabled(void); void asahi_g17_trace_report(u8 role, u8 ring, const u32 *words); From d366d69bdefc079c745f87276f3496dc0a985821 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 18:38:34 +1100 Subject: [PATCH 288/388] docs: distinguish G17 recovery ordering from unresolved T6040 first faults Record the state-1 attribution fix, bounded tiler evidence and host-hop trace contract. Keep the achieved-clock decoder and both deterministic CTS first faults open, with the exact missing firmware facts and measurement boundaries. Preserve the measured g3 policy and all gates. Assisted-by: LLM Signed-off-by: Ryan Murray --- GAPS.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/GAPS.md b/GAPS.md index ccc3dc4016d59..9debaafbfcb9b 100644 --- a/GAPS.md +++ b/GAPS.md @@ -107,6 +107,45 @@ and page-fault fallback. Diagnostic counters explicitly identify lifetime stampless totals; they are not a count of commands retired by one restart. +## g5 recovery evidence and host-hop timing + +The shared recovery service previously captured slot keys and monitors at +halted state 1, then called `render_pass_started()` during classification +after acknowledging state 3 to 0. A resumed render could change that pass +state. G5 computes attribution from the state-1 pass snapshot and carries +only that result through the handshake. KUnit drives the real service and +rejects any pass read after the first acknowledgement. Unknown 25G83 keys +still retain firmware blame. This is an independently demonstrated ordering +fix; it does not establish the cause of either deterministic reason-2 fault. + +G5 adds the existing declared TVB state (inventory, page capacity, producer, +consumer, high-water and reset) to the halted snapshot without resetting any +counter. The blamed pair's first four commands include render geometry and +the computed GTP layout, alongside the existing pass/cursor/stamp records. +Pending/active growth and its reply remain visible. The next isolated run +can distinguish exhausted inventory or delayed growth from a pass that never +started; no buffer enlargement or firmware timeout change is justified yet. + +Static-key `asahi_g17_inputs` records compute input conversion entry/result, +source count and publication waits. `asahi_g17_schedule` records enqueue, +inline/fallback, preparation and run boundaries. Together with the existing +publish/complete, DMA-fence and scheduler tracepoints these split host cost +per hop into input resolution, scheduler setup/wait/dispatch and firmware +execution. They make no timing claim until a matched trace is captured. + +The available macOS reference uses 26A428 on T6040 and reports a software +request P10 while hardware residency is 1578 MHz at raw state 14. Linux runs +the admitted 25G83 image. Thus neither a literal P10 conversion nor copying +another firmware's statistics layout proves achieved MHz. The reference +contains aggregate frequency/residency and public DT/calibration facts, +not a Linux 25G83 tag/field decoder. The inspected licensed public G17 source +at `jgoyvaerts/linux` `cc32473a09dd` also has no decoded statistics adapter. +Required input remains a fresh Linux raw report series under labeled idle, +ALU and fill load, and independently established field/timebase/achieved-state +semantics for that exact firmware. No captured raw payload has been promoted +to public implementation data. Achieved clock/power and the CTS cure remain +open; g3's measured memory/latency policy is preserved. + ## g4 shared runtime ports and observability The operator has selected g3 as the hardware baseline. This follow-on retains From 51350a0d50224a07399bc69fc85660f781538814 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Tue, 27 Oct 2026 03:40:05 -0400 Subject: [PATCH 289/388] soc: apple: clpc: hand the T8140 GPU floor back 100 ms after the PMP stops limiting The GPU floor stayed released for a second after the PMP last limited the DCS. Light GPU work that followed heavier work therefore ran without the floor for up to that second: on Neo, compute.light d1 took 148-151 us instead of 102.7 us for the first 0.5-0.75 s of a run, before the floor came back. Under sustained heavy rendering the PMP limits the DCS again within much less than that, so a 100 ms hold keeps the floor released there while the light work that follows gets it back almost at once. --- drivers/soc/apple/clpc.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/drivers/soc/apple/clpc.c b/drivers/soc/apple/clpc.c index 4ef32f023756a..946bfdb52de92 100644 --- a/drivers/soc/apple/clpc.c +++ b/drivers/soc/apple/clpc.c @@ -48,8 +48,13 @@ enum apple_clpc_rail { #define APPLE_CLPC_BUSY_PCT 50 /* Time a rail keeps its last request after demand stops */ #define APPLE_CLPC_HOLD_MS 32 -/* Time the GPU floor stays released after the PMP last limited the DCS */ -#define APPLE_CLPC_GPU_BACKOFF_MS 1000 +/* + * Time the GPU floor stays released after the PMP last limited the DCS. Under + * sustained heavy rendering the PMP limits the DCS again well within this, so + * the floor stays released; light submissions that follow get the floor back + * this long after the PMP stops limiting. + */ +#define APPLE_CLPC_GPU_BACKOFF_MS 100 /** * struct apple_clpc_hw - per-SoC CLPC parameters From 3124fffeb69797ef8aba88d49836f97608bc757f Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 18:41:55 +1100 Subject: [PATCH 290/388] arm64: dts: apple: describe the J616s fixed HDMI route and HPD The J616s routes external display pipeline 0 through ATC3 to the HDMI bridge. Describe the fixed DP PHY, crossbar selection and reciprocal graph, and its active-high hotplug input on AOP GPIO 36, using the existing DCP properties. Permit the fixed DisplayPort PHY graph in the T6040 PHY binding, as well as the Type-C SuperSpeed graph. Leave both the external DCP and ATC3 disabled. The SMC bridge reset and power callbacks have a separate service contract, and the current firmware handoff still needs qualification before enabling the route. Assisted-by: LLM Signed-off-by: Ryan Murray --- .../bindings/phy/apple,t6040-atcphy.yaml | 5 +-- arch/arm64/boot/dts/apple/t6040-j616s.dts | 37 +++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/Documentation/devicetree/bindings/phy/apple,t6040-atcphy.yaml b/Documentation/devicetree/bindings/phy/apple,t6040-atcphy.yaml index 04c8907584946..7f24f79264211 100644 --- a/Documentation/devicetree/bindings/phy/apple,t6040-atcphy.yaml +++ b/Documentation/devicetree/bindings/phy/apple,t6040-atcphy.yaml @@ -106,8 +106,8 @@ properties: port: $ref: /schemas/graph.yaml#/properties/port description: > - Link to the SuperSpeed lanes of the Type-C connector. Absent on a fixed - DisplayPort PHY. + Link to the SuperSpeed lanes of the Type-C connector, or to the DCP + DisplayPort output on a fixed DisplayPort PHY. apple,tunable-axi2af: $ref: "#/$defs/apple,tunable" @@ -174,7 +174,6 @@ allOf: "#reset-cells": false orientation-switch: false mode-switch: false - port: false additionalProperties: false diff --git a/arch/arm64/boot/dts/apple/t6040-j616s.dts b/arch/arm64/boot/dts/apple/t6040-j616s.dts index f4b0aa85e2758..75e67c49002a4 100644 --- a/arch/arm64/boot/dts/apple/t6040-j616s.dts +++ b/arch/arm64/boot/dts/apple/t6040-j616s.dts @@ -4358,6 +4358,43 @@ #include "t6040-j616s-usb3.dtsi" +/* + * The HDMI bridge is fed by the fourth ATC's fixed DisplayPort output. + * Keep the external DCP disabled until its firmware handoff and the SMC + * bridge-power services are supported; those services are not GPIO outputs. + */ +&dcpext0 { + apple,connector-type = "HDMI-A"; + hdmi-hpd-gpios = <&pinctrl_aop 36 GPIO_ACTIVE_HIGH>; + phys = <&atcphy3 PHY_TYPE_DP>; + phy-names = "dp-phy"; + mux-controls = <&atcphy3_xbar 0>; + mux-control-names = "dp-xbar"; + mux-index = <0>; + apple,dptx-phy = <3>; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + port@1 { + reg = <1>; + + dcpext0_dpout: endpoint { + remote-endpoint = <&atcphy3_dp>; + }; + }; + }; +}; + +&atcphy3 { + port { + atcphy3_dp: endpoint { + remote-endpoint = <&dcpext0_dpout>; + }; + }; +}; + /* Additional Abbey PMICs on the two independent NUB SPMI buses. */ &nub_spmi1 { status = "okay"; From 467a513ce08b463af44d4335faafa49534b2d493 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 18:46:20 +1100 Subject: [PATCH 291/388] arm64: dts: apple: retain the enabled T6040 SEP power suppliers The complete power-state table and the SEP series both describe SPI_P, SPI2 and SEP. Replaying their individual patches during a rebase leaves duplicate nodes whose later disabled status overrides the enabled suppliers required by the SPI2 fingerprint transport and SEP. Keep one definition of each physical power state and retain the enabled SEP-series definitions, as in the previously integrated tree. This also restores the active-provider invariant checked against the compiled DT. Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/boot/dts/apple/t6040-pmgr.dtsi | 28 ----------------------- 1 file changed, 28 deletions(-) diff --git a/arch/arm64/boot/dts/apple/t6040-pmgr.dtsi b/arch/arm64/boot/dts/apple/t6040-pmgr.dtsi index eab3825a95c26..e0af3e1903cfb 100644 --- a/arch/arm64/boot/dts/apple/t6040-pmgr.dtsi +++ b/arch/arm64/boot/dts/apple/t6040-pmgr.dtsi @@ -600,15 +600,6 @@ status = "disabled"; }; - ps_spi_p: power-controller@198 { - compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; - reg = <0x198 4>; - #power-domain-cells = <0>; - #reset-cells = <0>; - label = "spi_p"; - power-domains = <&ps_sio>; - status = "disabled"; - }; ps_uart_p: power-controller@1a0 { compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; @@ -662,15 +653,6 @@ status = "disabled"; }; - ps_spi2: power-controller@1c8 { - compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; - reg = <0x1c8 4>; - #power-domain-cells = <0>; - #reset-cells = <0>; - label = "spi2"; - power-domains = <&ps_spi_p>; - status = "disabled"; - }; ps_spi3: power-controller@1d0 { compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; @@ -1078,16 +1060,6 @@ status = "disabled"; }; - ps_sep: power-controller@c00 { - compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; - reg = <0xc00 4>; - #power-domain-cells = <0>; - #reset-cells = <0>; - label = "sep"; - power-domains = <&ps_fab3_soc>; - apple,always-on; - status = "disabled"; - }; }; &pmgr_aux { From aecb91d831daac37c5260c2a2931eb4ce2cb31ad Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 18:52:18 +1100 Subject: [PATCH 292/388] drm/asahi: bound the G17 tail cache separately from 25G83 auxiliary storage 25G83 packs its existing GTP sub-buffers after the tail-pointer cache. One of them is 64 times the tile map. The inherited 32 MiB TPC limit was applied to the combined object, so the valid 10240-square target used by CTS point_size_10000 is rejected with ERANGE even though the cache and tile record individually fit their original limits. Apply TPC_MAX to the aligned cache, and retain TARGET_RECORD_MAX before allocating or growing the object. Both bounds cap the derived auxiliary extent. Keep the encoder's buffer sizing, address checks, allocation accounting, retained generations and memory-pressure handling unchanged. No firmware field or numeric resource limit increases. The same preflight serves fresh and grown targets; Neo has no GTP tail and retains its policy. KUnit covers the CTS geometry for both utile shapes, oversized core and layered tile-record rejection, and unchanged Neo cache capacity. Hardware must confirm the point case; this does not diagnose the separate 256-square 1 MiB vertex/barrier fault or decode firmware reason 2. Link: https://github.com/KhronosGroup/VK-GL-CTS/blob/vulkan-cts-1.4.6.2/external/vulkancts/modules/vulkan/rasterization/vktRasterizationTests.cpp#L9384 Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/asahi/g17/buffer/target.rs | 71 ++++++++++++++++++++-- 1 file changed, 67 insertions(+), 4 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/buffer/target.rs b/drivers/gpu/drm/asahi/g17/buffer/target.rs index 64c88da8eca14..42a2c04b40112 100644 --- a/drivers/gpu/drm/asahi/g17/buffer/target.rs +++ b/drivers/gpu/drm/asahi/g17/buffer/target.rs @@ -139,6 +139,16 @@ impl Layout { }) } pub(crate) fn tpc_capacity(&self) -> Result { + // TPC_MAX bounds the tail-pointer cache itself. On 25G83 this object + // also contains GTP sub-buffers, notably 64 times the tile map. Applying + // the cache limit to their combined extent rejects valid large targets. + // Keep the original cache and tile-record limits; they also bound the + // derived auxiliary extent before any backing allocation or replacement. + if aligned(self.tpc_size, mmu::UAT_PGSZ)?.max(cfg::TPC_MIN) > cfg::TPC_MAX + || self.record_stride()? > cfg::TARGET_RECORD_MAX + { + return Err(ERANGE); + } Ok(aligned(self.tpc_end, mmu::UAT_PGSZ)?.max(cfg::TPC_MIN)) } pub(crate) fn record_stride(&self) -> Result { @@ -188,7 +198,7 @@ impl Target { pub(crate) fn new(dev: &AsahiDrmDevice, vm: &mmu::Vm, layout: &Layout) -> Result> { let tpc = layout.tpc_capacity()?; let stride = layout.record_stride()?; - if tpc > cfg::TPC_MAX || stride > cfg::TARGET_RECORD_MAX { + if stride > cfg::TARGET_RECORD_MAX { return Err(ERANGE); } let records = Object::new(dev, vm, stride * DEPTH)?; @@ -272,9 +282,6 @@ impl Target { let stride = layout.record_stride()?; let mut grew = false; if tpc > self.tpc.backing.size() { - if tpc > cfg::TPC_MAX { - return Err(ERANGE); - } Self::replace(&mut self.retired, &mut self.tpc, dev, vm, tpc, owners)?; grew = true; } @@ -350,3 +357,59 @@ impl Target { } } } + + +#[kernel::macros::kunit_tests(asahi_g17_target_capacity)] +mod tests { + use super::*; + + fn pass(size: u32, utile: u8) -> RenderPass { + // SAFETY: RenderPass and its nested surfaces/programs contain only + // integers and bools, for which the all-zero representation is valid. + let mut pass: RenderPass = unsafe { core::mem::zeroed() }; + pass.width = size; + pass.height = size; + pass.layers = 1; + pass.utile_width = utile; + pass.utile_height = utile; + pass + } + + #[test] + fn t6040_auxiliary_tiler_storage_is_not_tail_pointer_cache() { + // CTS point_size_10000 renders to 10240 squared. Its GTP sub-buffer + // is required by the existing 25G83 program, not a larger cache. + for utile in [16, 32] { + let layout = Layout::new(&pass(10240, utile), 2, RegisterPrograms::V25G83).unwrap(); + assert!(layout.tpc_size < cfg::TPC_MAX); + assert!(layout.record_stride().unwrap() < cfg::TARGET_RECORD_MAX); + assert!(layout.tpc_end > cfg::TPC_MAX); + assert!(layout.tpc_capacity().unwrap() >= layout.tpc_end); + assert_eq!(layout.gtp[0], Some(0)); + assert!(layout.gtp[1].unwrap() >= layout.tpc_size); + } + } + + #[test] + fn core_and_tile_record_limits_still_reject_oversized_targets() { + let mut layout = Layout::new(&pass(10240, 32), 2, RegisterPrograms::V25G83).unwrap(); + layout.tpc_size = cfg::TPC_MAX + 1; + assert_eq!(layout.tpc_capacity(), Err(ERANGE)); + let mut layered = pass(16384, 16); + layered.layers = 2; + let layout = Layout::new(&layered, 2, RegisterPrograms::V25G83).unwrap(); + assert!(layout.record_stride().unwrap() > cfg::TARGET_RECORD_MAX); + assert_eq!(layout.tpc_capacity(), Err(ERANGE)); + } + + #[test] + fn neo_keeps_the_same_cache_extent_and_limit() { + let mut layout = Layout::new(&pass(10240, 32), 1, RegisterPrograms::G17P).unwrap(); + assert!(layout.gtp.iter().all(Option::is_none)); + assert_eq!(layout.tpc_capacity().unwrap(), + aligned(layout.tpc_size, mmu::UAT_PGSZ).unwrap().max(cfg::TPC_MIN)); + layout.tpc_size = cfg::TPC_MAX + 1; + layout.tpc_end = layout.tpc_size; + assert_eq!(layout.tpc_capacity(), Err(ERANGE)); + } +} From 1214a925dbee556c0c4305777308f9938f262eb8 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 18:52:18 +1100 Subject: [PATCH 293/388] docs: identify the T6040 large-target guard separately from the barrier fault Record the production layout reproducer, unchanged numeric bounds and mandatory hardware/memory acceptance for the point-target correction. Keep the 256-square large-vertex barrier fault unresolved and separate from framebuffer size and unqualified firmware reason codes. Assisted-by: LLM Signed-off-by: Ryan Murray --- GAPS.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/GAPS.md b/GAPS.md index 9debaafbfcb9b..c8d2c7c753261 100644 --- a/GAPS.md +++ b/GAPS.md @@ -107,6 +107,35 @@ and page-fault fallback. Diagnostic counters explicitly identify lifetime stampless totals; they are not a count of commands retired by one restart. +## g5 large-target capacity correction + +Public CTS `vulkan-cts-1.4.6.2` uses a 10240-square framebuffer for +`rasterization.primitive_size.points.point_size_10000`. The existing 25G83 +GTP layout appends a sub-buffer of 64 times the tile map after the TPC. +Applying the inherited 32 MiB cache bound to this combined allocation rejects +that valid target with ERANGE, despite its cache and tile record each fitting +their original limits. This is reproducible in the production layout code +without GPU execution. The normalized hardware summary contains ERANGE and +reason-2 recoveries but cannot establish their order or one-to-one association. + +G5 applies the original TPC_MAX to the aligned cache, preserving the original +tile-record bound before allocation or growth. The tile-map bound also caps +the derived auxiliary extent. It changes no firmware buffer-sizing formula, +numeric limit, VA window, generation/retention rule or allocation accounting. +Neo has no auxiliary tail and keeps the same policy. KUnit covers both utile +shapes at the CTS target size, oversized cache/layered-record rejection and +Neo capacity. Fresh and growing targets use the same preflight. Large targets +need their full required metadata through the existing memory/VA mechanisms; +allocation failure still propagates. Hardware execution/readback and post-close +memory/capacity baselines are mandatory acceptance gates. + +The separate 1 MiB vertex/barrier case uses a 256-square target, not a large +framebuffer. Its point count is actual vertex-buffer bytes divided by stride. +It cannot be explained by the large-target capacity guard. Preserve its first +operation/exception, generated sequence and the halted-state TVB/growth/pass +snapshots before choosing a coherence, VM-bind or firmware-timeout fix. The +point-target correction is not a claim that firmware reason 2 is decoded. + ## g5 recovery evidence and host-hop timing The shared recovery service previously captured slot keys and monitors at From d29673ec23c43190000c7f6f51164c563faa5ecf Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 19:06:41 +1100 Subject: [PATCH 294/388] arm64: dts: apple: describe the T6040 DisplayPort audio interfaces Describe the five SIO-fed DPA interfaces and their power domains. Connect DPA0, DPA1 and DPA2 to their assigned DCP, DCPEXT0 and DCPEXT1 audio ports; DPA3 and DPA4 have no assigned display processor. Extend the existing DPA binding for T6041 and require unconnected interfaces to stay disabled. Keep all five interfaces disabled: the T6041 SIO firmware backend and compatible DCP audio services are not implemented. In particular, adding an audio graph must not pull an unavailable component into the active display pipeline or claim that HDMI audio works. Assisted-by: LLM Signed-off-by: Ryan Murray --- .../bindings/sound/apple,t6030-dpaudio.yaml | 43 ++++-- arch/arm64/boot/dts/apple/t6040-dpaudio.dtsi | 132 ++++++++++++++++++ arch/arm64/boot/dts/apple/t6040.dtsi | 1 + 3 files changed, 168 insertions(+), 8 deletions(-) create mode 100644 arch/arm64/boot/dts/apple/t6040-dpaudio.dtsi diff --git a/Documentation/devicetree/bindings/sound/apple,t6030-dpaudio.yaml b/Documentation/devicetree/bindings/sound/apple,t6030-dpaudio.yaml index 9b4e9328d9020..bb92d6a30c428 100644 --- a/Documentation/devicetree/bindings/sound/apple,t6030-dpaudio.yaml +++ b/Documentation/devicetree/bindings/sound/apple,t6030-dpaudio.yaml @@ -4,29 +4,36 @@ $id: http://devicetree.org/schemas/sound/apple,t6030-dpaudio.yaml# $schema: http://devicetree.org/meta-schemas/core.yaml# -title: Apple T6030 DisplayPort audio +title: Apple T6030 and T6041 DisplayPort audio maintainers: - Ryan Murray description: | - The external display processor provides the HDMI port's audio service. - Its DisplayPort audio interface has no application-processor registers. - Samples arrive through SIO, and the graph connects this interface to - port 0 of the display processor that owns the audio service. + Each DisplayPort audio interface has no application-processor registers. + Samples arrive through SIO, and the graph connects the interface to port 0 + of the display processor that owns its audio service. An interface without + an assigned display processor has no graph connection and remains disabled. + + T6041 requires a generation-specific SIO firmware backend and a compatible + display audio service before these interfaces can be enabled. select: properties: compatible: contains: - const: apple,t6030-dpaudio + enum: + - apple,t6030-dpaudio + - apple,t6041-dpaudio required: - compatible properties: compatible: items: - - const: apple,t6030-dpaudio + - enum: + - apple,t6030-dpaudio + - apple,t6041-dpaudio - const: apple,dpaudio dmas: @@ -52,6 +59,26 @@ required: - dmas - dma-names - power-domains - - ports + +allOf: + - if: + properties: + compatible: + contains: + const: apple,t6030-dpaudio + then: + required: + - ports + + - if: + not: + required: + - ports + then: + properties: + status: + const: disabled + required: + - status additionalProperties: false diff --git a/arch/arm64/boot/dts/apple/t6040-dpaudio.dtsi b/arch/arm64/boot/dts/apple/t6040-dpaudio.dtsi new file mode 100644 index 0000000000000..68d346a4e1160 --- /dev/null +++ b/arch/arm64/boot/dts/apple/t6040-dpaudio.dtsi @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: GPL-2.0+ OR MIT +/* + * DPA interfaces are fed by SIO and have no AP-side MMIO registers. + * Keep them disabled until the T6041 SIO firmware and DCP audio services + * are supported. DPA3/4 have no assigned DCP in this SoC's firmware tree. + */ + +/ { + dpaudio0: audio-controller-0 { + compatible = "apple,t6041-dpaudio", "apple,dpaudio"; + dmas = <&sio 0x64>; + dma-names = "tx"; + power-domains = <&ps_dpa0>; + status = "disabled"; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + port@0 { + reg = <0>; + + dpaudio0_dcp: endpoint { + remote-endpoint = <&dcp_audio>; + }; + }; + }; + }; + + dpaudio1: audio-controller-1 { + compatible = "apple,t6041-dpaudio", "apple,dpaudio"; + dmas = <&sio 0x66>; + dma-names = "tx"; + power-domains = <&ps_dpa1>; + status = "disabled"; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + port@0 { + reg = <0>; + + dpaudio1_dcp: endpoint { + remote-endpoint = <&dcpext0_audio>; + }; + }; + }; + }; + + dpaudio2: audio-controller-2 { + compatible = "apple,t6041-dpaudio", "apple,dpaudio"; + dmas = <&sio 0x68>; + dma-names = "tx"; + power-domains = <&ps_dpa2>; + status = "disabled"; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + port@0 { + reg = <0>; + + dpaudio2_dcp: endpoint { + remote-endpoint = <&dcpext1_audio>; + }; + }; + }; + }; + + dpaudio3: audio-controller-3 { + compatible = "apple,t6041-dpaudio", "apple,dpaudio"; + dmas = <&sio 0x6a>; + dma-names = "tx"; + power-domains = <&ps_dpa3>; + status = "disabled"; + }; + + dpaudio4: audio-controller-4 { + compatible = "apple,t6041-dpaudio", "apple,dpaudio"; + dmas = <&sio 0x6c>; + dma-names = "tx"; + power-domains = <&ps_dpa4>; + status = "disabled"; + }; +}; + +&dcp { + ports { + #address-cells = <1>; + #size-cells = <0>; + + port@0 { + reg = <0>; + + dcp_audio: endpoint { + remote-endpoint = <&dpaudio0_dcp>; + }; + }; + }; +}; + +&dcpext0 { + ports { + #address-cells = <1>; + #size-cells = <0>; + + port@0 { + reg = <0>; + + dcpext0_audio: endpoint { + remote-endpoint = <&dpaudio1_dcp>; + }; + }; + }; +}; + +&dcpext1 { + ports { + #address-cells = <1>; + #size-cells = <0>; + + port@0 { + reg = <0>; + + dcpext1_audio: endpoint { + remote-endpoint = <&dpaudio2_dcp>; + }; + }; + }; +}; diff --git a/arch/arm64/boot/dts/apple/t6040.dtsi b/arch/arm64/boot/dts/apple/t6040.dtsi index 75c9b3f06ac21..d834d562c65d2 100644 --- a/arch/arm64/boot/dts/apple/t6040.dtsi +++ b/arch/arm64/boot/dts/apple/t6040.dtsi @@ -2403,3 +2403,4 @@ }; #include "t6040-pmgr.dtsi" +#include "t6040-dpaudio.dtsi" From 92f563f1b5409aafa16abd037d119ed57d57d70c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Bal=C3=A1zsy?= Date: Sun, 25 Oct 2026 16:28:36 +0100 Subject: [PATCH 295/388] drm/apple: route the T6041 Type-C displays through the display fabric MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dcpext of a T6041 listed the DP PHY and crossbar of every Type-C port and picked the port whose PHY had entered DP alt mode at connect time. The display fabric already routes the T602x, T6030 and J700 Type-C ports: a typec-routes node under the DCP, one mode switch per port, driven by the port controller. Use it for desktop 26.x as well and drop the private route (apple,dptx-phys, dp-phyN/dp-xbarN, apple_t6040_phy_dp_alt()). Three things differ from the other fabric users: - The T6040 DP PHY must be powered when the display connects, not when a route is claimed. The port controller calls the PHY's and the route's mode switch in connector order, and the PHY has to have seen DP alt mode and the plug orientation before its lanes are programmed. - The compact DPTX of desktop 26.x takes the crossbar link down and up around a link configuration through dcp->xbar, while the fabric's own direct-link helper is T6030 only. The route sets dcp->xbar for the time it is active; without it the link never trains. - There is a single Type-C pipeline. A port in DP alt mode without a sink (a dock, or a pin assignment the PHY cannot drive) must not hold it against the port that has a display: claim it on HPD only. Tested on a J616s (Mac16,7) with a USB-C display on one port: attach at boot, rebind of the port controller (replug) and a device suspend test (pm_test=devices). The other ports and a second display are not tested. Signed-off-by: FrantiÅ¡ek Balázsy --- drivers/gpu/drm/apple/dcp-fabric.c | 78 +++++++++++- drivers/gpu/drm/apple/dcp-fabric.h | 3 + drivers/gpu/drm/apple/dcp-internal.h | 11 -- drivers/gpu/drm/apple/dcp.c | 181 ++++----------------------- drivers/phy/apple/t6040-usb2.c | 9 -- include/linux/soc/apple/dp-tunnel.h | 15 --- 6 files changed, 106 insertions(+), 191 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp-fabric.c b/drivers/gpu/drm/apple/dcp-fabric.c index b5e2eafb8c090..629b33ddace2c 100644 --- a/drivers/gpu/drm/apple/dcp-fabric.c +++ b/drivers/gpu/drm/apple/dcp-fabric.c @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -306,6 +307,54 @@ static int dcp_dpxbar_tunnel_select_source(struct mux_control *mux, int state) return ret; } +/* + * Desktop 26.x firmware does not power the DP PHY of a Type-C route itself: + * the route does, once its crossbar points at the pipeline, and gives it + * back before the crossbar is released. + */ +static int dcp_typec_route_phy_on(struct apple_dcp_typec_route *route) +{ + int ret; + + if (route->dcp->fw_compat != DCP_FIRMWARE_V_26_6_DESKTOP || + route->phy_powered) + return 0; + ret = phy_init(route->phy); + if (!ret) { + ret = phy_power_on(route->phy); + if (ret) + phy_exit(route->phy); + } + if (ret) { + dev_warn(route->dcp->dev, "Type-C DP PHY init/power: %d\n", ret); + return ret; + } + route->phy_powered = true; + msleep(25); + return 0; +} + +/* + * The PHY is powered when the display connects, not when the route is + * claimed: the port controller's mode switches run in connector order, and + * the PHY must have seen DP alt mode and the plug orientation first. + */ +int dcp_typec_phy_on(struct apple_dcp *dcp) +{ + struct apple_dcp_typec_route *route = READ_ONCE(dcp->active_typec_route); + + return route ? dcp_typec_route_phy_on(route) : 0; +} + +static void dcp_typec_route_phy_off(struct apple_dcp_typec_route *route) +{ + if (!route->phy_powered) + return; + route->phy_powered = false; + phy_power_off(route->phy); + phy_exit(route->phy); +} + static int dcp_typec_route_activate(struct apple_dcp_typec_route *route, struct mux_control *xbar) { @@ -370,6 +419,12 @@ static int dcp_typec_route_activate(struct apple_dcp_typec_route *route, dcp_modes_begin_attachment(dcp); apple_connector_edid_set_live(dcp->connector, false); dcp->phy = route->phy; + /* + * Desktop 26.x compact DPTX takes the crossbar link down and up around + * a link configuration (dptxport_compact_xbar_link()) through dcp->xbar. + */ + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) + dcp->xbar = route->xbar; dcp->dptx_phy = route->dptx_phy; dcp->connector_type = DRM_MODE_CONNECTOR_USB; WRITE_ONCE(dcp->ext_backlight, false); @@ -444,6 +499,7 @@ static int dcp_typec_route_deactivate(struct apple_dcp_typec_route *route) if (route->deactivate_error) return route->deactivate_error; if (!route->deactivate_xbar_done) { + dcp_typec_route_phy_off(route); if (!route->tunnel || route->xbar_up) ret = mux_control_deselect(route->active_xbar ?: route->xbar); else @@ -546,8 +602,15 @@ static int dcp_typec_route_deactivate(struct apple_dcp_typec_route *route) dcp->dptx_phy = dcp->typec_routes[0].dptx_phy; } } else { - /* Keep DPTX endpoint discovery working before a cable is attached. */ - dcp->phy = dcp->typec_routes[0].phy; + /* + * Keep DPTX endpoint discovery working before a cable is + * attached. Desktop 26.x powers the PHY with the route, so an + * idle pipeline must not point at one. + */ + dcp->phy = dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP ? + NULL : dcp->typec_routes[0].phy; + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) + dcp->xbar = NULL; dcp->dptx_phy = dcp->typec_routes[0].dptx_phy; dcp->connector_type = DRM_MODE_CONNECTOR_USB; } @@ -1521,6 +1584,17 @@ static int dcp_typec_route_set(struct typec_mux_dev *mux, } } + /* + * Desktop 26.x has one Type-C pipeline: only a port with a sink (HPD) + * takes it. A dock or cable in DP alt mode without a display behind it + * would otherwise hold it against the port that has one. + */ + if (!port->owner && !hpd && + route->dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) { + port->applied_valid = true; + return 0; + } + if (!port->owner) { best = dcp_typec_free_route(port); /* Every pipeline is driving another port's display. */ diff --git a/drivers/gpu/drm/apple/dcp-fabric.h b/drivers/gpu/drm/apple/dcp-fabric.h index 869a2c3716cfe..57bffb02d715f 100644 --- a/drivers/gpu/drm/apple/dcp-fabric.h +++ b/drivers/gpu/drm/apple/dcp-fabric.h @@ -39,6 +39,8 @@ struct apple_dcp_typec_route { u32 dptx_phy; u32 mux_index; bool selected; + /* desktop 26.x: the DP PHY is initialised and powered for this route */ + bool phy_powered; /* crossbar output actually selected: xbar (dpphy) or a Thunderbolt dpin */ unsigned int tunnel_dpin; u64 tunnel_generation; @@ -51,6 +53,7 @@ struct apple_dcp_typec_route { int deactivate_error; }; +int dcp_typec_phy_on(struct apple_dcp *dcp); bool dcp_is_typec_output(struct apple_dcp *dcp); bool dcp_is_usb4_output(struct apple_dcp *dcp); bool dcp_uses_t6020_tunnel_flow(struct apple_dcp *dcp); diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index b720cd80c11d6..7920bd86af102 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -294,17 +294,6 @@ struct dcp_desktop_state { * display (unplug); the next swap needs a power-on and a mode set. */ bool ext_pipe_off; - /* - * Dynamic Type-C DP route (T6041 dcpext1): the DP PHYs and crossbars - * of every Type-C port, the one in use (or -1), the DPTX target PHY - * of each and the crossbar state selecting this processor. - */ - unsigned int ndyn; - int dyn_active; - struct phy *dyn_phy[3]; - struct mux_control *dyn_xbar[3]; - u32 dyn_dptx_phy[3]; - u32 dyn_mux_index; }; /* TODO: move IOMFB members to its own struct */ diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 628c1b7e05660..69a5a55ae63eb 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -2133,119 +2133,30 @@ static int dcp_dptx_connect_tile(struct apple_dcp *dcp) /* * Desktop 26.x external processors (T6041 dcpext0 HDMI, dcpext1 USB-C). - * They keep the classic external model of the driver this was ported from: - * no Type-C display fabric, the DPTX link set up on HPD (HDMI) or on the - * DP alt mode hotplug of the port (USB-C), and the Type-C port chosen - * dynamically from the PHY that entered DP alt mode. + * The DPTX link is set up on HPD (HDMI) or when the Type-C display fabric + * routes a port's DP alt mode to the processor (USB-C). */ -/* The caller holds hpd_mutex, which also orders dcp->phy against a connect. */ -static int dcp_desktop_dyn_route_select_locked(struct apple_dcp *dcp) -{ - struct dcp_desktop_state *d = dcp->desktop; - typeof(&apple_t6040_phy_dp_alt) dp_alt; - unsigned int i; - int ret; - - lockdep_assert_held(&dcp->hpd_mutex); - if (!d->ndyn || d->dyn_active >= 0) - return 0; - /* Looked up at runtime so appledrm does not require the T6040 PHY driver. */ -#if IS_ENABLED(CONFIG_PHY_APPLE_T6040_USB2) - dp_alt = symbol_get(apple_t6040_phy_dp_alt); -#else - dp_alt = NULL; -#endif - if (!dp_alt) { - dev_dbg(dcp->dev, "T6040 PHY driver not loaded\n"); - return -ENODEV; - } - for (i = 0; i < d->ndyn; i++) - if (dp_alt(d->dyn_phy[i])) - break; -#if IS_ENABLED(CONFIG_PHY_APPLE_T6040_USB2) - symbol_put(apple_t6040_phy_dp_alt); -#endif - if (i == d->ndyn) { - dev_dbg(dcp->dev, "no Type-C port in DP alt mode\n"); - return -ENODEV; - } - ret = mux_control_select(d->dyn_xbar[i], d->dyn_mux_index); - if (ret) { - dev_warn(dcp->dev, "Type-C port %u: crossbar select: %d\n", i, ret); - return ret; - } - ret = phy_init(d->dyn_phy[i]); - if (!ret) { - ret = phy_power_on(d->dyn_phy[i]); - if (ret) - phy_exit(d->dyn_phy[i]); - } - if (ret) { - dev_warn(dcp->dev, "Type-C port %u: DP PHY init/power: %d\n", i, ret); - mux_control_deselect(d->dyn_xbar[i]); - return ret; - } - msleep(25); - dcp->phy = d->dyn_phy[i]; - dcp->xbar = d->dyn_xbar[i]; - dcp->dptx_phy = d->dyn_dptx_phy[i]; - d->dyn_active = i; - dev_dbg(dcp->dev, "Type-C port %u selected (DPTX target PHY %u, crossbar state %u)\n", - i, dcp->dptx_phy, d->dyn_mux_index); - return 0; -} - -static void dcp_desktop_dyn_route_release_locked(struct apple_dcp *dcp) -{ - struct dcp_desktop_state *d = dcp->desktop; - int i = d->dyn_active; - - lockdep_assert_held(&dcp->hpd_mutex); - if (!d->ndyn || i < 0) - return; - d->dyn_active = -1; - dcp->phy = NULL; - dcp->xbar = NULL; - phy_power_off(d->dyn_phy[i]); - phy_exit(d->dyn_phy[i]); - mux_control_deselect(d->dyn_xbar[i]); - dev_dbg(dcp->dev, "Type-C port %u released\n", i); -} - -/* Unbind: a selected route holds its crossbar and its powered PHY. */ -static void dcp_desktop_dyn_route_release(struct apple_dcp *dcp) -{ - if (!dcp->desktop) - return; - guard(mutex)(&dcp->hpd_mutex); - dcp_desktop_dyn_route_release_locked(dcp); -} - static int dcp_desktop_dptx_connect(struct apple_dcp *dcp, u32 port) { struct dptx_port *dptx = &dcp->dptxport[port]; unsigned long left; - bool selected; int ret = 0; mutex_lock(&dcp->hpd_mutex); - /* A route selected here is released again if the connect fails. */ - selected = !dcp->phy && dcp->desktop->ndyn; - if (selected) { - ret = dcp_desktop_dyn_route_select_locked(dcp); - if (ret) - goto out_unlock; - } if (!dcp->phy) { dev_warn(dcp->dev, "dcp_dptx_connect: missing phy\n"); ret = -ENODEV; goto out_unlock; } + /* A fabric route powers its PHY here (a no-op for the other routes). */ + ret = dcp_typec_phy_on(dcp); + if (ret) + goto out_unlock; if (!dptx_service_available(dptx)) { dev_warn(dcp->dev, "dcp_dptx_connect: dptx service for port %u not enabled\n", port); ret = -ENODEV; - goto out_release; + goto out_unlock; } if (dptx->connected) goto out_unlock; @@ -2255,13 +2166,13 @@ static int dcp_desktop_dptx_connect(struct apple_dcp *dcp, u32 port) ret = dptxport_connect(dptx->service, 0, dcp->dptx_phy, dcp->dptx_die, true); if (ret) { dev_warn(dcp->dev, "DPTX port %u: connect failed: %d\n", port, ret); - goto out_release; + goto out_unlock; } ret = dptxport_request_display(dptx->service); if (ret) { dev_warn(dcp->dev, "DPTX port %u: display request failed: %d\n", port, ret); - goto out_release; + goto out_unlock; } dptx->connected = true; @@ -2290,9 +2201,6 @@ static int dcp_desktop_dptx_connect(struct apple_dcp *dcp, u32 port) av_service_connect(dcp); return 0; -out_release: - if (selected) - dcp_desktop_dyn_route_release_locked(dcp); out_unlock: mutex_unlock(&dcp->hpd_mutex); return ret; @@ -2310,8 +2218,6 @@ static int dcp_desktop_dptx_disconnect_oob(struct apple_dcp *dcp, u32 port) dptxport_set_hpd(dcp->dptxport[port].service, false); } ret = dcp_dptx_disconnect(dcp, port); - scoped_guard(mutex, &dcp->hpd_mutex) - dcp_desktop_dyn_route_release_locked(dcp); if (!dcp->main_display) dcp->desktop->ext_pipe_off = true; return ret; @@ -3382,7 +3288,7 @@ static int dcp_start_desktop_external(struct apple_dcp *dcp, u32 map) if (ret) return ret; - if (!dcp->phy && !d->ndyn) { + if (!dcp->phy) { dev_warn(dcp->dev, "no DP PHY and no Type-C route: no display link\n"); return 0; } @@ -5284,8 +5190,6 @@ VISIBLE_IF_KUNIT void dcp_comp_unbind(struct device *dev, struct device *main, v dcp->systemep = NULL; } - dcp_desktop_dyn_route_release(dcp); - /* * The auxiliary endpoints a desktop 26.x start brought up, still * routed while their shutdown is acknowledged. @@ -5396,62 +5300,31 @@ static void dcp_desktop_phy_off(void *data) } /* - * The outputs of a desktop 26.x external processor (T6041 dcpext): the - * fixed HDMI route's DP PHY is powered here, as the boot loader's working - * sequence does; a dynamic Type-C route lists the DP PHY and crossbar of - * every port (dp-phyN, dp-xbarN, apple,dptx-phys, apple,dptx-mux-index), - * and the one in DP alt mode is claimed at connect. + * The fixed HDMI output of a desktop 26.x external processor (T6041 dcpext0): + * its DP PHY is powered here, as the boot loader's working sequence does. A + * Type-C output (dcpext1) has no fixed PHY; the display fabric powers the + * PHY of the route a port claims (see dcp_typec_phy_on()). */ static int dcp_desktop_probe_routes(struct apple_dcp *dcp) { - struct dcp_desktop_state *d = dcp->desktop; struct device *dev = dcp->dev; - unsigned int i; - int nd, ret; + int ret; - d->dyn_active = -1; - if (!dcp_desktop_external(dcp)) + if (!dcp_desktop_external(dcp) || !dcp->phy) return 0; - if (dcp->phy) { - ret = phy_init(dcp->phy); - if (ret) - return dev_err_probe(dev, ret, "Failed to initialize dp-phy\n"); - ret = phy_power_on(dcp->phy); - if (ret) { - phy_exit(dcp->phy); - return dev_err_probe(dev, ret, "Failed to power on dp-phy\n"); - } - ret = devm_add_action_or_reset(dev, dcp_desktop_phy_off, dcp->phy); - if (ret) - return ret; - msleep(25); + ret = phy_init(dcp->phy); + if (ret) + return dev_err_probe(dev, ret, "Failed to initialize dp-phy\n"); + ret = phy_power_on(dcp->phy); + if (ret) { + phy_exit(dcp->phy); + return dev_err_probe(dev, ret, "Failed to power on dp-phy\n"); } - - nd = of_property_count_u32_elems(dev->of_node, "apple,dptx-phys"); - if (nd <= 0) - return 0; - if (nd > ARRAY_SIZE(d->dyn_phy)) - return dev_err_probe(dev, -EINVAL, "too many Type-C routes: %d\n", nd); - - for (i = 0; i < nd; i++) { - char name[16]; - - snprintf(name, sizeof(name), "dp-phy%u", i); - d->dyn_phy[i] = devm_phy_get(dev, name); - if (IS_ERR(d->dyn_phy[i])) - return dev_err_probe(dev, PTR_ERR(d->dyn_phy[i]), "%s\n", name); - snprintf(name, sizeof(name), "dp-xbar%u", i); - d->dyn_xbar[i] = devm_mux_control_get(dev, name); - if (IS_ERR(d->dyn_xbar[i])) - return dev_err_probe(dev, PTR_ERR(d->dyn_xbar[i]), "%s\n", name); - of_property_read_u32_index(dev->of_node, "apple,dptx-phys", i, - &d->dyn_dptx_phy[i]); - } - of_property_read_u32(dev->of_node, "apple,dptx-mux-index", &d->dyn_mux_index); - d->ndyn = nd; - dev_info(dev, "%u Type-C DP routes, crossbar state %u\n", d->ndyn, - d->dyn_mux_index); + ret = devm_add_action_or_reset(dev, dcp_desktop_phy_off, dcp->phy); + if (ret) + return ret; + msleep(25); return 0; } diff --git a/drivers/phy/apple/t6040-usb2.c b/drivers/phy/apple/t6040-usb2.c index 868d4304b2395..c15c004d94c83 100644 --- a/drivers/phy/apple/t6040-usb2.c +++ b/drivers/phy/apple/t6040-usb2.c @@ -1509,15 +1509,6 @@ static int t6040_dp_mux_set(struct typec_mux_dev *mux, struct typec_mux_state *s return 0; } -/* For the DCP's Type-C route: is this PHY's port in DP alt mode (C/E)? */ -bool apple_t6040_phy_dp_alt(struct phy *phy) -{ - struct t6040_usb2_phy *tphy = phy_get_drvdata(phy); - - return tphy && tphy->dp_alt; -} -EXPORT_SYMBOL_GPL(apple_t6040_phy_dp_alt); - static void t6040_dp_mux_unregister(void *mux) { typec_mux_unregister(mux); diff --git a/include/linux/soc/apple/dp-tunnel.h b/include/linux/soc/apple/dp-tunnel.h index 868de62db1156..540d48fe8e0dc 100644 --- a/include/linux/soc/apple/dp-tunnel.h +++ b/include/linux/soc/apple/dp-tunnel.h @@ -61,21 +61,6 @@ int apple_atc_dp_tunnel_rate_source(struct phy *phy, unsigned int source, */ int apple_atc_dp_tunnel_open(struct phy *phy); -/* - * T6040 Type-C PHY: true while the Type-C controller holds the PHY's port in - * DisplayPort alt mode with pin assignment C or E (DP on all four lanes), so - * appledrm can route a DCP that lists several Type-C DP PHYs to the one that - * has a DisplayPort sink plugged. - */ -#if IS_ENABLED(CONFIG_PHY_APPLE_T6040_USB2) -bool apple_t6040_phy_dp_alt(struct phy *phy); -#else -static inline bool apple_t6040_phy_dp_alt(struct phy *phy) -{ - return false; -} -#endif - /* * Display crossbar: take the connection of an already selected output down or * bring it back up (clock gates and enables) around a link reconfiguration. From b7e5c080e92a7f45f6b836f765d8a6e754beb32f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Bal=C3=A1zsy?= Date: Sun, 25 Oct 2026 14:04:18 +0100 Subject: [PATCH 296/388] drm/apple: check the desktop 26.6 callback records against their handlers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The H17P callbacks are checked against the record sizes the firmware sends; the desktop 26.6 ones were not. The u16 offset keeps a record inside the shared memory, but a handler could still read past a short request or write past a short reply into the next record. Give every desktop 26.6 handler the request and reply size it reads and writes, and refuse a callback whose record is shorter, or whose lengths do not fit the message, the way H17P refuses a malformed one. Longer records stay accepted. Signed-off-by: FrantiÅ¡ek Balázsy --- drivers/gpu/drm/apple/iomfb.c | 9 ++ drivers/gpu/drm/apple/iomfb.h | 2 + drivers/gpu/drm/apple/iomfb_v26_6_desktop.c | 91 +++++++++++++++++++++ 3 files changed, 102 insertions(+) diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index 27c7ba4b39b66..4b9ccafa0338c 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -1428,6 +1428,15 @@ static void dcpep_handle_cb(struct apple_dcp *dcp, enum dcp_context_id context, handled = tag >= 0 && tag < IOMFB_MAX_CB && dcp->cb_handlers && dcp->cb_handlers[tag]; + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && handled && + (length < sizeof(*hdr) || + (u64)sizeof(*hdr) + hdr->in_len + hdr->out_len > length || + !iomfb_check_callback_v26_6_desktop(tag, hdr->in_len, hdr->out_len))) { + dev_err(dev, "short IOMFB callback %d (%u, %u, length %u)\n", + tag, hdr->in_len, hdr->out_len, length); + dcp->crashed = true; + return; + } if (dcp->fw_compat == DCP_FIRMWARE_H17P && (tag < 0 || tag >= IOMFB_MAX_CB || (u64)sizeof(*hdr) + hdr->in_len + hdr->out_len != length || diff --git a/drivers/gpu/drm/apple/iomfb.h b/drivers/gpu/drm/apple/iomfb.h index 7e6e16f1dddf7..7c60a81207ad7 100644 --- a/drivers/gpu/drm/apple/iomfb.h +++ b/drivers/gpu/drm/apple/iomfb.h @@ -212,6 +212,8 @@ bool iomfb_validate_callback_h17p(int tag, u32 in_len, u32 out_len); struct apple_dcp; bool iomfb_check_callback_h17p(struct apple_dcp *dcp, int tag, u32 in_len, u32 out_len); +/* Desktop 26.6: does the record hold what the tag's handler reads and writes? */ +bool iomfb_check_callback_v26_6_desktop(int tag, u32 in_len, u32 out_len); typedef bool (*iomfb_cb_handler)(struct apple_dcp *, int, void *, void *); diff --git a/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c b/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c index de8783f3a3694..1e5f431a88137 100644 --- a/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c +++ b/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c @@ -266,6 +266,97 @@ static const iomfb_cb_handler cb_handlers[IOMFB_MAX_CB] = { [599] = trampoline_nop, }; +/* + * What each handler above reads from the request and writes to the reply. + * The firmware may send longer records; a shorter one is refused. + */ +struct dcp_cb_min_v26_6_desktop { + u32 in_len; + u32 out_len; + bool valid; +}; + +#define CB_MIN(in, out) { .in_len = (in), .out_len = (out), .valid = true } + +static const struct dcp_cb_min_v26_6_desktop cb_min[IOMFB_MAX_CB] = { + [0] = CB_MIN(0, sizeof(u8)), + [1] = CB_MIN(0, sizeof(u8)), + [2] = CB_MIN(0, 0), + [3] = CB_MIN(0, sizeof(struct dcp_rt_bandwidth)), + [6] = CB_MIN(sizeof(struct dcp_set_frame_sync_props_req_v26_6_desktop), + sizeof(struct dcp_set_frame_sync_props_resp_v26_6_desktop)), + [100] = CB_MIN(0, 0), + [101] = CB_MIN(0, sizeof(u32)), + [102] = CB_MIN(0, 0), + [104] = CB_MIN(0, 0), + [107] = CB_MIN(0, 0), + [108] = CB_MIN(0, sizeof(u8)), + [109] = CB_MIN(0, sizeof(u8)), + [110] = CB_MIN(0, sizeof(u8)), + [111] = CB_MIN(0, sizeof(u8)), + [112] = CB_MIN(0, sizeof(u8)), + [113] = CB_MIN(0, sizeof(u8)), + [114] = CB_MIN(sizeof(struct dcp_dict_passthrough_req_v26_6), + sizeof(struct dcp_dict_passthrough_resp_v26_6)), + [115] = CB_MIN(sizeof(struct dcpep_get_tiling_state_req), + sizeof(struct dcpep_get_tiling_state_resp)), + [121] = CB_MIN(0, 0), + [122] = CB_MIN(0, sizeof(u8)), + [123] = CB_MIN(0, sizeof(u8)), + [125] = CB_MIN(sizeof(struct dcp_read_edt_data_req), + sizeof(dcp_read_edt_data_resp_t)), + [127] = CB_MIN(sizeof(u32), sizeof(u8)), + [128] = CB_MIN(sizeof(struct dcp_set_dcpav_prop_chunk_req), sizeof(u8)), + [129] = CB_MIN(sizeof(struct dcp_set_dcpav_prop_end_req), sizeof(u8)), + [134] = CB_MIN(0, sizeof(u32)), + [201] = CB_MIN(sizeof(struct dcp_map_buf_req), + sizeof(struct DCP_FW_NAME(dcp_map_buf_resp))), + [206] = CB_MIN(0, sizeof(u8)), + [207] = CB_MIN(0, sizeof(u8)), + [208] = CB_MIN(0, 0), + [209] = CB_MIN(0, sizeof(u64)), + [300] = CB_MIN(sizeof(struct iomfb_property), 0), + [400] = CB_MIN(0, 0), + [401] = CB_MIN(sizeof(struct dcp_get_uint_prop_req), + sizeof(struct dcp_get_uint_prop_resp)), + [406] = CB_MIN(0, 0), + [408] = CB_MIN(0, sizeof(u64)), + [411] = CB_MIN(sizeof(struct DCP_FW_NAME(dcp_map_reg_req)), + sizeof(struct DCP_FW_NAME(dcp_map_reg_resp))), + [413] = CB_MIN(0, sizeof(u8)), + [414] = CB_MIN(sizeof(struct iomfb_sr_set_property_int_req), sizeof(u8)), + [415] = CB_MIN(0, sizeof(u8)), + [451] = CB_MIN(sizeof(struct dcp_allocate_buffer_req), + sizeof(struct dcp_allocate_buffer_resp)), + [552] = CB_MIN(0, sizeof(u8)), + [561] = CB_MIN(0, sizeof(u8)), + [563] = CB_MIN(0, sizeof(u8)), + [565] = CB_MIN(0, sizeof(u8)), + [567] = CB_MIN(0, sizeof(u8)), + [572] = CB_MIN(0, sizeof(u32)), + [575] = CB_MIN(sizeof(struct dcp_hotplug_req_v26_6), + sizeof(struct dcp_hotplug_resp_v26_6)), + [576] = CB_MIN(0, 0), + [581] = CB_MIN(0, 0), + [582] = CB_MIN(0, sizeof(u8)), + [590] = CB_MIN(sizeof(struct DCP_FW_NAME(dc_swap_complete_resp)), 0), + [592] = CB_MIN(sizeof(dcp_swap_intent_t), 0), + [593] = CB_MIN(sizeof(u32), 0), + [594] = CB_MIN(sizeof(u8), 0), + [595] = CB_MIN(0, 0), + [598] = CB_MIN(0, 0), + [599] = CB_MIN(0, 0), +}; + +#undef CB_MIN + +bool iomfb_check_callback_v26_6_desktop(int tag, u32 in_len, u32 out_len) +{ + if (tag < 0 || tag >= IOMFB_MAX_CB || !cb_min[tag].valid) + return false; + return in_len >= cb_min[tag].in_len && out_len >= cb_min[tag].out_len; +} + void DCP_FW_NAME(iomfb_start)(struct apple_dcp *dcp) { dcp->cb_handlers = cb_handlers; From d7f2aa6ead929fb17f5a43e161fc2af3e0a22890 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Bal=C3=A1zsy?= Date: Sun, 25 Oct 2026 14:04:43 +0100 Subject: [PATCH 297/388] phy: apple: t6040: give the Type-C lanes back to USB3 after DP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit t6040_dp_power_on() shuts the USB3 side down, but t6040_dp_power_off() only cleared dp_up, and t6040_usb3_power_on() could re-initialise the PHY under a live DP mode. Once a Type-C DP session ended with the plug still in, USB3 on that port stayed down until dwc3 power cycled the PHY. Track whether dwc3 holds the USB3 phy on and asked for the PIPE switch. USB3 bring-up now waits while DP mode is programmed, and when DP mode ends and the port has left DP alt mode, from the DP power-off or the mode switch, whichever comes last, USB3 is brought up again and the PIPE goes back to the PHY. On an unplug or a DP mode change tipd drops the USB role first, dwc3 powers the USB3 phy off, and nothing changes there. Signed-off-by: FrantiÅ¡ek Balázsy --- drivers/phy/apple/t6040-usb2.c | 62 ++++++++++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 7 deletions(-) diff --git a/drivers/phy/apple/t6040-usb2.c b/drivers/phy/apple/t6040-usb2.c index c15c004d94c83..20fb5e9da5cb0 100644 --- a/drivers/phy/apple/t6040-usb2.c +++ b/drivers/phy/apple/t6040-usb2.c @@ -237,6 +237,9 @@ struct t6040_usb2_phy { struct completion app_reply; enum typec_orientation orientation; bool usb3_up; /* tuned by the coprocessor this power cycle */ + bool usb3_on; /* dwc3 holds the USB3 phy powered on */ + bool usb3_pipe; /* dwc3 asked for the PIPE switch this power cycle */ + bool usb3_lent; /* DP mode took the lanes of this Type-C port */ bool pipe_switched; /* the controller's PIPE runs on this PHY */ /* DisplayPort (PHY mode 3); dp_ok is false when the DT lacks a DP route */ @@ -882,15 +885,19 @@ static void t6040_usb3_pipe_park(struct t6040_usb2_phy *tphy) * its PIPE on the dummy PHY. A failure here leaves the port at USB2, so it is * logged and not returned: the USB2 PHY and the core must still come up. */ -static int t6040_usb3_power_on(struct phy *phy) +static void t6040_usb3_up_locked(struct t6040_usb2_phy *tphy) { - struct t6040_usb2_phy *tphy = phy_get_drvdata(phy); bool swap; int ret; - guard(mutex)(&tphy->lock); + lockdep_assert_held(&tphy->lock); if (!tphy->usb3_ok || tphy->usb3_up) - return 0; + return; + /* DP mode owns all four lanes; USB3 comes back in t6040_usb3_reclaim_locked(). */ + if (tphy->dp_up) { + dev_dbg(tphy->dev, "USB3: lanes held by DP, USB2 only for now\n"); + return; + } switch (tphy->orientation) { case TYPEC_ORIENTATION_NORMAL: @@ -901,7 +908,7 @@ static int t6040_usb3_power_on(struct phy *phy) break; default: dev_info(tphy->dev, "USB3: no plug orientation, USB2 only\n"); - return 0; + return; } /* PIPE_HANDLER_DEFAULT: the dummy PIPE clock the controller keeps using at USB2 */ @@ -918,7 +925,7 @@ static int t6040_usb3_power_on(struct phy *phy) tphy->usb3_up = true; dev_dbg(tphy->dev, "USB3 PHY up, lane %d\n", swap); - return 0; + return; usb2_only: /* power_off skips a PHY that never came up: undo here, so the next attach starts clean */ @@ -926,6 +933,15 @@ static int t6040_usb3_power_on(struct phy *phy) sleep: t6040_usb3_sleep(tphy); dev_warn(tphy->dev, "USB3 PHY bring-up failed (%d), USB2 only\n", ret); +} + +static int t6040_usb3_power_on(struct phy *phy) +{ + struct t6040_usb2_phy *tphy = phy_get_drvdata(phy); + + guard(mutex)(&tphy->lock); + tphy->usb3_on = true; + t6040_usb3_up_locked(tphy); return 0; } @@ -955,6 +971,8 @@ static int t6040_usb3_power_off(struct phy *phy) struct t6040_usb2_phy *tphy = phy_get_drvdata(phy); guard(mutex)(&tphy->lock); + tphy->usb3_on = false; + tphy->usb3_pipe = false; t6040_usb3_down_locked(tphy); return 0; } @@ -972,6 +990,7 @@ static int t6040_usb3_set_mode(struct phy *phy, enum phy_mode mode, int submode) return -EOPNOTSUPP; guard(mutex)(&tphy->lock); + tphy->usb3_pipe = true; if (!tphy->usb3_up || tphy->pipe_switched) return 0; t6040_usb3_pipe_switch(tphy); @@ -1353,6 +1372,31 @@ static void t6040_dp_drive(struct t6040_usb2_phy *tphy, unsigned int lane, u32 p t6040_usb2_mask32(base + (tx ? 0x100 : 0xa8), 0x7ffff, eq); } +/* + * Give the lanes back to USB3 once the DP session is over and the port has + * left DP alt mode while dwc3 still holds the USB3 phy on: the USB3 bring-up + * programs the PHY afresh, as it does after DP on the next attach, and the + * PIPE returns to the PHY if the controller had asked for it. Called when + * the DP phy powers off and when the mux leaves DP, as either can come last. + * tipd drops the USB role before it leaves DP alt mode, so on an unplug or a + * DP mode change dwc3 powers the USB3 phy off first and this does nothing. + */ +static void t6040_usb3_reclaim_locked(struct t6040_usb2_phy *tphy) +{ + lockdep_assert_held(&tphy->lock); + if (!tphy->usb3_lent || tphy->dp_up || tphy->dp_alt) + return; + tphy->usb3_lent = false; + if (!tphy->usb3_on || tphy->usb3_up) + return; + t6040_usb3_up_locked(tphy); + if (tphy->usb3_up && tphy->usb3_pipe && !tphy->pipe_switched) { + t6040_usb3_pipe_switch(tphy); + tphy->pipe_switched = true; + } + dev_info(tphy->dev, "USB3 %s after DP\n", tphy->usb3_up ? "back" : "not restored"); +} + /* ---- DP phy_ops ---- */ static int t6040_dp_power_on(struct phy *phy) @@ -1368,8 +1412,10 @@ static int t6040_dp_power_on(struct phy *phy) * lanes to DP: aciophy_phy_shutdown of the USB3 side first, then the * same mode-3 init as the fixed DP PHY. */ - if (!t6040_dp_only(tphy)) + if (!t6040_dp_only(tphy)) { t6040_usb3_down_locked(tphy); + tphy->usb3_lent = true; + } ret = t6040_dp_phy_init(tphy, tphy->dp_flip); if (ret) { dev_err(tphy->dev, "DP PHY init failed: %d\n", ret); @@ -1389,6 +1435,7 @@ static int t6040_dp_power_off(struct phy *phy) guard(mutex)(&tphy->lock); tphy->dp_up = false; + t6040_usb3_reclaim_locked(tphy); return 0; } @@ -1506,6 +1553,7 @@ static int t6040_dp_mux_set(struct typec_mux_dev *mux, struct typec_mux_state *s dp ? "DP" : "non-DP", state->mode, alt, tphy->dp_flip ? "reversed" : "normal"); if (alt && tphy->dp_up) t6040_dp_xbar_config(tphy, tphy->dp_flip); + t6040_usb3_reclaim_locked(tphy); return 0; } From bd2cdf39d822e8fc98ab26a5d4a03befa21d8d8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Bal=C3=A1zsy?= Date: Sun, 25 Oct 2026 18:11:12 +0100 Subject: [PATCH 298/388] drm/apple: say how desktop 26.x uses the Type-C fabric MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The comment in dcp_is_typec_output() still described the private route the previous commit removed. Desktop 26.x now takes its Type-C route from the fabric and only drives the link through its own compact DPTX connect. Signed-off-by: FrantiÅ¡ek Balázsy --- drivers/gpu/drm/apple/dcp-fabric.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/apple/dcp-fabric.c b/drivers/gpu/drm/apple/dcp-fabric.c index 629b33ddace2c..f12bf78ec7fc7 100644 --- a/drivers/gpu/drm/apple/dcp-fabric.c +++ b/drivers/gpu/drm/apple/dcp-fabric.c @@ -103,7 +103,11 @@ static bool dcp_hdmi_borrowed(void *ctx); bool dcp_is_typec_output(struct apple_dcp *dcp) { - /* Desktop 26.x keeps its own Type-C route, outside this fabric. */ + /* + * Desktop 26.x takes its Type-C route from this fabric but drives the + * link through its own compact DPTX connect (dcp_desktop_dptx_connect()), + * not the Type-C output session below. + */ if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) return false; return dcp->active_typec_route || From 4299dc01bb2bab0b48a730f15bbad5d5f5b3ed08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Bal=C3=A1zsy?= Date: Sun, 25 Oct 2026 18:41:09 +0100 Subject: [PATCH 299/388] drm/apple: order desktop PHY power against route release, free the pipeline with the sink MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review of the Type-C fabric series found four holes on the T6041 path. dcp_typec_phy_on() looked at the active route under hpd_mutex only, while the deactivate powers the PHY off and releases the crossbar under tb_lock. A connect racing a release could power the PHY after the crossbar was deselected and leave phy_powered set. Take tb_lock around the check and the power-on, and refuse a route whose crossbar is already released. The start-up gate in dcp_start_desktop() is again satisfied by a Type-C route, so a restart after an unplug (dcp->phy is NULL then) still brings the DPTX services up, and the start-up link is only connected when a fixed PHY or an active route exists: an idle pipeline has no powered PHY and no crossbar selection to connect on. When HPD drops while the port stays in DP alt mode, the route, its crossbar and the PHY were kept. Release them with the sink, as a mode change does, so a display on another port is not refused as busy; the route is claimed again when HPD returns. Check that a desktop callback record lies inside the shared buffer before its header is read, and access dcp->xbar with READ_ONCE()/WRITE_ONCE() as it is read by the DPTX worker without a lock. Signed-off-by: FrantiÅ¡ek Balázsy --- drivers/gpu/drm/apple/dcp-fabric.c | 39 +++++++++++++++++++++++++++--- drivers/gpu/drm/apple/dcp.c | 10 ++++++-- drivers/gpu/drm/apple/dptxep.c | 5 ++-- drivers/gpu/drm/apple/iomfb.c | 11 +++++++-- 4 files changed, 55 insertions(+), 10 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp-fabric.c b/drivers/gpu/drm/apple/dcp-fabric.c index f12bf78ec7fc7..4b0dcedd9773c 100644 --- a/drivers/gpu/drm/apple/dcp-fabric.c +++ b/drivers/gpu/drm/apple/dcp-fabric.c @@ -345,9 +345,23 @@ static int dcp_typec_route_phy_on(struct apple_dcp_typec_route *route) */ int dcp_typec_phy_on(struct apple_dcp *dcp) { - struct apple_dcp_typec_route *route = READ_ONCE(dcp->active_typec_route); + struct apple_dcp_typec_route *route; - return route ? dcp_typec_route_phy_on(route) : 0; + /* + * Ordered against dcp_typec_route_deactivate(), which powers the PHY + * off and releases the crossbar under tb_lock: a connect racing the + * release either powers the PHY first and has it taken down again, or + * finds the route released and powers nothing. A desktop connect does + * not take the fabric lock, so tb_lock is the common one. + */ + guard(mutex)(&dcp->tb_lock); + route = READ_ONCE(dcp->active_typec_route); + if (!route) + return 0; + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && + route->deactivate_xbar_done) + return -ENODEV; + return dcp_typec_route_phy_on(route); } static void dcp_typec_route_phy_off(struct apple_dcp_typec_route *route) @@ -428,7 +442,7 @@ static int dcp_typec_route_activate(struct apple_dcp_typec_route *route, * a link configuration (dptxport_compact_xbar_link()) through dcp->xbar. */ if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) - dcp->xbar = route->xbar; + WRITE_ONCE(dcp->xbar, route->xbar); dcp->dptx_phy = route->dptx_phy; dcp->connector_type = DRM_MODE_CONNECTOR_USB; WRITE_ONCE(dcp->ext_backlight, false); @@ -614,7 +628,7 @@ static int dcp_typec_route_deactivate(struct apple_dcp_typec_route *route) dcp->phy = dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP ? NULL : dcp->typec_routes[0].phy; if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) - dcp->xbar = NULL; + WRITE_ONCE(dcp->xbar, NULL); dcp->dptx_phy = dcp->typec_routes[0].dptx_phy; dcp->connector_type = DRM_MODE_CONNECTOR_USB; } @@ -1509,6 +1523,23 @@ static int dcp_typec_route_set(struct typec_mux_dev *mux, if (ret && !dcp_is_external_h17p(dcp)) return ret; } + /* + * Desktop 26.x has one Type-C pipeline and powers the PHY with + * the route. A sink that went away while the port stays in DP + * alt mode (a dock whose monitor was unplugged) must give back + * the pipeline, its crossbar and the PHY, or a display on + * another port is refused as busy. The route is taken again + * when HPD returns. + */ + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) { + port->preferred_route = port->owner; + port->hpd = false; + ret = dcp_typec_route_deactivate(port->owner); + if (ret) + return ret; + port->owner = NULL; + dcp_typec_pipeline_freed(); + } } /* did the pairing pass count this port's direct stream so far? */ diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 69a5a55ae63eb..f02b8ca5f6fd6 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -3288,7 +3288,8 @@ static int dcp_start_desktop_external(struct apple_dcp *dcp, u32 map) if (ret) return ret; - if (!dcp->phy) { + /* An idle Type-C pipeline has no PHY until a route is claimed. */ + if (!dcp->phy && !dcp->nr_typec_routes) { dev_warn(dcp->dev, "no DP PHY and no Type-C route: no display link\n"); return 0; } @@ -3316,7 +3317,12 @@ static int dcp_start_desktop_external(struct apple_dcp *dcp, u32 map) if (ret) return ret; - if (!dptx_ret && dptx_service_available(&dcp->dptxport[0])) { + /* + * A Type-C route powers the PHY and selects the crossbar when it is + * claimed; until then there is no link to bring up. + */ + if (!dptx_ret && dptx_service_available(&dcp->dptxport[0]) && + (dcp->fixed_phy || dcp->active_typec_route)) { ret = dcp_dptx_connect(dcp, 0); if (ret) dev_info(dcp->dev, "no DPTX link at startup: %d\n", ret); diff --git a/drivers/gpu/drm/apple/dptxep.c b/drivers/gpu/drm/apple/dptxep.c index a7620457f1f46..a03efb93bdc91 100644 --- a/drivers/gpu/drm/apple/dptxep.c +++ b/drivers/gpu/drm/apple/dptxep.c @@ -490,11 +490,12 @@ static int dptxport_call_get_link_rate(struct apple_epic_service *service, static int dptxport_compact_xbar_link(struct apple_epic_service *service, bool up) { struct apple_dcp *dcp = service->ep->dcp; + struct mux_control *xbar = READ_ONCE(dcp->xbar); int ret; - if (!READ_ONCE(service->ep->compact) || !dcp->xbar) + if (!READ_ONCE(service->ep->compact) || !xbar) return 0; - ret = dcp_dpxbar_link(dcp->xbar, up); + ret = dcp_dpxbar_link(xbar, up); dev_dbg(dcp->dev, "DPTXPort: crossbar link %s: %d\n", up ? "up" : "down", ret); return ret; } diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index 4b9ccafa0338c..a354580f70dd6 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -1415,6 +1415,14 @@ static void dcpep_handle_cb(struct apple_dcp *dcp, enum dcp_context_id context, return; } + /* The header is read below: check the record lies in the shared buffer first. */ + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && + (length < sizeof(*hdr) || offset >= 0x8000 || length > 0x8000 - offset)) { + dev_err(dev, "invalid IOMFB callback envelope\n"); + dcp->crashed = true; + return; + } + tag = dcp_parse_tag(hdr->tag); if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && @@ -1429,8 +1437,7 @@ static void dcpep_handle_cb(struct apple_dcp *dcp, enum dcp_context_id context, handled = tag >= 0 && tag < IOMFB_MAX_CB && dcp->cb_handlers && dcp->cb_handlers[tag]; if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && handled && - (length < sizeof(*hdr) || - (u64)sizeof(*hdr) + hdr->in_len + hdr->out_len > length || + ((u64)sizeof(*hdr) + hdr->in_len + hdr->out_len > length || !iomfb_check_callback_v26_6_desktop(tag, hdr->in_len, hdr->out_len))) { dev_err(dev, "short IOMFB callback %d (%u, %u, length %u)\n", tag, hdr->in_len, hdr->out_len, length); From 3c753216c4c56745aab9fa9951f91399f098d1e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Bal=C3=A1zsy?= Date: Sun, 25 Oct 2026 18:41:09 +0100 Subject: [PATCH 300/388] selftests: apple-dcp: follow the desktop 26.6 fabric and callback checks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fabric extraction now reaches dcp_typec_route_phy_off() and the DCP_FIRMWARE_V_26_6_DESKTOP tests, and the dispatcher extraction the desktop callback check. Add the define and, as for the other desktop paths, a stub that asserts if a non-desktop scenario reaches the check; the PHY power-off is a no-op there as the real one returns for an unpowered PHY. Seven of the eight units that stopped compiling build and pass again; mux-release-stages needs -Wno-unused-but-set-global with clang 23. Signed-off-by: FrantiÅ¡ek Balázsy --- tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py | 3 +++ tools/testing/selftests/drivers/gpu/apple-dcp/quiesce.py | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py index 9c70ecf5aeda9..d306e52f80f0c 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py @@ -1879,6 +1879,9 @@ def dispatcher(root): static struct dcp_channel *dcp_get_channel(struct apple_dcp *d, enum dcp_context_id c) { (void)c; return &d->channel; } static int dcp_parse_tag(char *p) { (void)p; return case_tag; } +/* A desktop 26.6 callback is not under test here: no scenario selects that firmware. */ +static bool __attribute__((unused)) iomfb_check_callback_v26_6_desktop(int tag, u32 in_len, u32 out_len) +{ (void)tag; (void)in_len; (void)out_len; assert(!"desktop 26.6 callback in an H17P scenario"); return false; } static bool of_machine_is_compatible(const char *n) { (void)n; return machine; } static bool of_device_is_compatible(struct device_node *n, const char *s) { (void)s; return n->external; } diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/quiesce.py b/tools/testing/selftests/drivers/gpu/apple-dcp/quiesce.py index 48d7c3078092c..30eb0a183d6b7 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/quiesce.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/quiesce.py @@ -80,6 +80,10 @@ def make_unit(source, parent=False, reference=None): static struct drm_device *dcp_typec_drm(void) { return NULL; } static void dcp_typec_plan(struct drm_device *d,struct apple_dcp_typec_port *p,unsigned n) { (void)d;(void)p;(void)n; } static bool dcp_typec_tunnel_held(struct apple_dcp *d) { (void)d;return false; } +/* The desktop 26.6 (T6041) Type-C path is not under test: nothing here is on that firmware. */ +#define DCP_FIRMWARE_V_26_6_DESKTOP 100 +static void __attribute__((unused)) dcp_typec_route_phy_off(struct apple_dcp_typec_route *r) +{ (void)r; /* never powered: the real one returns at once */ } static int dcp_typec_route_deactivate(struct apple_dcp_typec_route *r) { deactivate_calls++;r->selected=false;return 0; } static void dcp_typec_port_attach(struct apple_dcp_typec_port *p) { (void)p; } static int gpiod_get_value_cansleep(void *p) { (void)p;return 0; } From c96ebf0c5bc7a7aa47a1b29d635cbfd16c2baf90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Bal=C3=A1zsy?= Date: Sun, 25 Oct 2026 19:39:47 +0100 Subject: [PATCH 301/388] drm/apple: connect desktop 26.x Type-C displays through the Type-C output session MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A desktop 26.x Type-C display took its route from the fabric but was connected by dcp_desktop_dptx_connect(), outside the session the other SoCs use: no attach and detach bookkeeping (cable flag, generation, firmware metadata, unplug count), no relink, no reconnect retry and none of the IRQ_HPD handling the fabric already dispatches to it. dcp_is_typec_output() is no longer false for the desktop firmware, and dcp_dptx_connect(), dcp_dptx_connect_oob() and dcp_dptx_disconnect_oob() take the common path for it. What compact DPTX does differently stays where it differs: - the connection validation call does not exist, so it is skipped; - the PHY is powered when the display connects, before the DPTX connect; - a link configuration that has not arrived when the wait ends is not an error, it comes once a sink arrives; - a disconnect marks the external pipe as shut down by the firmware. The fixed HDMI output (dcpext0) has no Type-C session and keeps dcp_desktop_dptx_connect(). Signed-off-by: FrantiÅ¡ek Balázsy --- drivers/gpu/drm/apple/dcp-fabric.c | 7 ------ drivers/gpu/drm/apple/dcp.c | 34 +++++++++++++++++++++++------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp-fabric.c b/drivers/gpu/drm/apple/dcp-fabric.c index 4b0dcedd9773c..09b5ec9652a0c 100644 --- a/drivers/gpu/drm/apple/dcp-fabric.c +++ b/drivers/gpu/drm/apple/dcp-fabric.c @@ -103,13 +103,6 @@ static bool dcp_hdmi_borrowed(void *ctx); bool dcp_is_typec_output(struct apple_dcp *dcp) { - /* - * Desktop 26.x takes its Type-C route from this fabric but drives the - * link through its own compact DPTX connect (dcp_desktop_dptx_connect()), - * not the Type-C output session below. - */ - if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) - return false; return dcp->active_typec_route || dcp->fixed_connector_type == DRM_MODE_CONNECTOR_USB; } diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index f02b8ca5f6fd6..ef92b5fcad71c 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -1582,6 +1582,7 @@ static int dcp_dptx_connect_raw(struct apple_dcp *dcp, u32 port, bool *retryable { /* T8122 dcpext: HPD only after the link wait, see dcp_ext_dptx_607() */ bool late_hpd = dcp_ext_dptx_607(dcp); + bool compact; unsigned long timeout; u64 generation; u8 dfp_port; @@ -1607,6 +1608,8 @@ static int dcp_dptx_connect_raw(struct apple_dcp *dcp, u32 port, bool *retryable dcp->connector_type, dcp->dptxport[port].connected); mutex_lock(&dcp->hpd_mutex); + compact = dptx_service_available(&dcp->dptxport[port]) && + READ_ONCE(dcp->dptxport[port].service->ep->compact); if (!dcp_connect_session_valid_locked(dcp, session, cable, recovery)) { ret = -ESTALE; goto out_unlock; @@ -1635,9 +1638,14 @@ static int dcp_dptx_connect_raw(struct apple_dcp *dcp, u32 port, bool *retryable reinit_completion(&dcp->dptxport[port].linkcfg_completion); WRITE_ONCE(dcp->dptxport[port].tile_hint, false); dcp->dptxport[port].atcphy = dcp->phy; + /* A desktop 26.x route powers its PHY when the display connects. */ + ret = dcp_typec_phy_on(dcp); + if (ret) + goto out_unlock; /* a tiled display's second half comes in on the port's dpin1 */ dfp_port = port && READ_ONCE(dcp->split.active) ? 2 : dcp->dptx_dfp_port; - if (!dcp_ext_dptx_607(dcp)) { + /* Compact DPTX (desktop 26.x) has no connection validation call. */ + if (!dcp_ext_dptx_607(dcp) && !compact) { ret = dptxport_validate_connection(dcp->dptxport[port].service, dfp_port, dcp->dptx_phy, dcp->dptx_die); @@ -1712,7 +1720,12 @@ static int dcp_dptx_connect_raw(struct apple_dcp *dcp, u32 port, bool *retryable /* A revoked wait must not disconnect or publish readiness for a new session. */ if (!dcp_connect_session_valid(dcp, session, cable, recovery)) return -ESTALE; - if (!ret && late_hpd) { + if (!ret && compact) { + /* Without a sink the link is configured once one arrives. */ + dev_dbg(dcp->dev, + "dcp_dptx_connect: no link configuration on port %u yet\n", port); + ret = 0; + } else if (!ret && late_hpd) { /* Without HPD this firmware does not train: assert it now. */ dev_dbg(dcp->dev, "dcp_dptx_connect: no link configuration on port %u yet, asserting HPD\n", @@ -2265,7 +2278,8 @@ int dcp_dptx_connect(struct apple_dcp *dcp, u32 port) struct dcp_fabric_session session = {}; int ret; - if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) + /* The fixed HDMI output of a desktop 26.x processor has no Type-C session. */ + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && !dcp_is_typec_output(dcp)) return dcp_desktop_dptx_connect(dcp, port); if (!dcp_is_external_h17p(dcp)) { @@ -2852,7 +2866,7 @@ int dcp_dptx_connect_oob(struct platform_device *pdev, u32 port) { struct apple_dcp *dcp = platform_get_drvdata(pdev); - if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && !dcp_is_typec_output(dcp)) return dcp_desktop_dptx_connect(dcp, port); if (dcp_is_external_h17p(dcp)) { @@ -2895,8 +2909,9 @@ int dcp_dptx_connect_oob(struct platform_device *pdev, u32 port) int dcp_dptx_disconnect_oob(struct platform_device *pdev, u32 port) { struct apple_dcp *dcp = platform_get_drvdata(pdev); + int ret; - if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && !dcp_is_typec_output(dcp)) return dcp_desktop_dptx_disconnect_oob(dcp, port); if (dcp_is_external_h17p(dcp)) @@ -2924,13 +2939,16 @@ int dcp_dptx_disconnect_oob(struct platform_device *pdev, u32 port) av_service_disconnect(dcp); if (dptx_service_available(&dcp->dptxport[port])) { - int ret = dptxport_set_hpd(dcp->dptxport[port].service, false); - + ret = dptxport_set_hpd(dcp->dptxport[port].service, false); if (ret) return ret; } - return dcp_dptx_disconnect(dcp, port); + ret = dcp_dptx_disconnect(dcp, port); + /* The firmware shuts a desktop 26.x external pipe down with its display. */ + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && !dcp->main_display) + dcp->desktop->ext_pipe_off = true; + return ret; } /* From d9ffff98f55cc1aad847ffc271f4683c72dee371 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Bal=C3=A1zsy?= Date: Sun, 25 Oct 2026 19:45:14 +0100 Subject: [PATCH 302/388] selftests: apple-dcp: the fast-replug scenario is not compact DPTX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dcp_dptx_connect_raw() now asks the DPTX endpoint whether it is compact and has the route power the PHY. This scenario's endpoint is the classic one and its PHY needs no route: report not compact and stub the PHY power-on. Signed-off-by: FrantiÅ¡ek Balázsy --- tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py index d306e52f80f0c..518cf23473d61 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py @@ -490,6 +490,8 @@ def bounce(root): m = re.search(r'^static int dcp_dptx_connect_raw\(', dcp, re.M) assert m, 'dcp_dptx_connect_raw' raw = block(dcp, m.start()) + # Compact DPTX is the desktop 26.x wire; this scenario's endpoint is not. + raw = raw.replace('READ_ONCE(dcp->dptxport[port].service->ep->compact)', 'false') def swap(old, new): nonlocal unit @@ -510,6 +512,8 @@ def swap(old, new): swap(old_raw, 'static int dcp_dptx_connect_raw(struct apple_dcp *, u32, bool *' + (', const struct dcp_fabric_session *, bool, bool' if sessions else '') + ');') swap('static int dptxport_set_hpd(void *p, bool on)\n{ (void)p; assert(!on); return hpd_error; }', + '/* The route powers a desktop 26.x PHY at connect: nothing to do for this scenario. */\n' + 'static int __attribute__((unused)) dcp_typec_phy_on(struct apple_dcp *d) { (void)d; return 0; }\n' 'static bool fw_hpd, fw_display, link_trains;\n' 'static int hpd_drops, connects, release_error_once, assert_error;\n' '/* A request that times out may still be carried out; a refused one is not. */\n' From fa0966306684bf116181edf58707f488b3fe3c8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Bal=C3=A1zsy?= Date: Sun, 25 Oct 2026 17:13:26 +0100 Subject: [PATCH 303/388] dt-bindings: power: apple: add apple,irq-safe to the PMP report entries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit apple,irq-safe on a PMP report entry marks an entry that parents the IRQ-safe PMGR power states of a coprocessor: genpd requires the parent of an IRQ-safe subdomain to be IRQ safe as well. Signed-off-by: FrantiÅ¡ek Balázsy --- .../devicetree/bindings/power/apple,t8140-pmp-report.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Documentation/devicetree/bindings/power/apple,t8140-pmp-report.yaml b/Documentation/devicetree/bindings/power/apple,t8140-pmp-report.yaml index e8eac9e5411e3..e79ea331865b5 100644 --- a/Documentation/devicetree/bindings/power/apple,t8140-pmp-report.yaml +++ b/Documentation/devicetree/bindings/power/apple,t8140-pmp-report.yaml @@ -93,6 +93,13 @@ patternProperties: holds this entry's device ID, instead of by reg. Only valid on a report whose PMP the host starts. + apple,irq-safe: + type: boolean + description: + The entry is the parent of IRQ-safe PMGR power states and is + switched with interrupts disabled. Setting the state takes a + spinlock and polls the acknowledgment atomically. + required: - compatible - reg From 540f52fa22b1b343f582fe128848d7c0a2ae6c08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Bal=C3=A1zsy?= Date: Sun, 25 Oct 2026 01:25:45 +0200 Subject: [PATCH 304/388] pmdomain: apple: pmp-report: IRQ-safe entries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A PMP device-state entry that parents PMGR power states (which are IRQ safe) must be IRQ safe too, or genpd refuses the subdomain. Setting the state takes a spinlock and polls the acknowledgment atomically, so the entry may run with interrupts disabled. Opt in with apple,irq-safe. Tested on a MacBook Pro M4 Pro (J616s): the DISPEXT0 and DISPEXT1 device-state entries parent the dispext0/1 PMGR chains that power the HDMI and USB-C DisplayPort controllers. Signed-off-by: FrantiÅ¡ek Balázsy --- drivers/pmdomain/apple/pmp-report.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/drivers/pmdomain/apple/pmp-report.c b/drivers/pmdomain/apple/pmp-report.c index 823669e27ad74..aaa38c06daaaf 100644 --- a/drivers/pmdomain/apple/pmp-report.c +++ b/drivers/pmdomain/apple/pmp-report.c @@ -1473,6 +1473,13 @@ static int apple_pmp_report_entry_probe(struct platform_device *pdev) ent->genpd.power_on = apple_pmp_report_entry_power_on; ent->genpd.power_off = apple_pmp_report_entry_power_off; + /* + * An entry that is the parent of PMGR power states (which are IRQ safe) + * must be IRQ safe too. Setting the state takes a spinlock and polls + * the acknowledgment atomically, so it may be called with IRQs off. + */ + if (of_property_read_bool(node, "apple,irq-safe")) + ent->genpd.flags |= GENPD_FLAG_IRQ_SAFE; ret = pm_genpd_init(&ent->genpd, NULL, true); if (ret) { From 0e553f35ebba58b41b088027e2d3911d93219b44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Bal=C3=A1zsy?= Date: Mon, 26 Oct 2026 15:55:27 +0100 Subject: [PATCH 305/388] dt-bindings: display: apple,dcp: add apple,t6041-dcpext MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The M4 Pro (T6041) external display processors use the generic dcpext fallback like the other dcpext variants; the driver already matches apple,t6041-dcpext. Signed-off-by: FrantiÅ¡ek Balázsy --- Documentation/devicetree/bindings/display/apple,dcp.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/Documentation/devicetree/bindings/display/apple,dcp.yaml b/Documentation/devicetree/bindings/display/apple,dcp.yaml index d79ad457f7703..3d691822c50b1 100644 --- a/Documentation/devicetree/bindings/display/apple,dcp.yaml +++ b/Documentation/devicetree/bindings/display/apple,dcp.yaml @@ -33,6 +33,7 @@ properties: - apple,t8112-dcpext - apple,t6000-dcpext - apple,t6020-dcpext + - apple,t6041-dcpext - const: apple,dcpext - items: - const: apple,t8140-dcpext From 204e304f1045936f0871517793a8c33eb156c132 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Bal=C3=A1zsy?= Date: Mon, 26 Oct 2026 15:52:02 +0100 Subject: [PATCH 306/388] arm64: dts: apple: t6040-j616s: external display chain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Describe the J616s external display chain, all disabled: dcpext0 behind the HDMI port (atc3 display crossbar, atcphy3, the DP-to-HDMI converter), dcpext1 behind the three Type-C ports (display fabric routes, atc0..2 crossbars), their DISPEXT power domains with the PMP acknowledgement entries, the display DARTs and mailboxes, and the dcpext firmware memory as disabled placeholders that the boot loader fills from the ADT. Nothing is enabled by the device tree: powering the DISPEXT chain without the PMP acknowledgement hangs the machine before root, and a boot loader that does not provide the firmware memory leaves the placeholders disabled. A gate enables the chain (next commit). Signed-off-by: FrantiÅ¡ek Balázsy --- .../dts/apple/t6040-j616s-extdisp-hdmi.dtsi | 308 ++++++++++++++++ .../apple/t6040-j616s-extdisp-mem-loader.dtsi | 72 ++++ .../dts/apple/t6040-j616s-extdisp-usbc.dtsi | 183 ++++++++++ .../boot/dts/apple/t6040-j616s-extdisp.dtsi | 345 ++++++++++++++++++ arch/arm64/boot/dts/apple/t6040-j616s.dts | 6 + 5 files changed, 914 insertions(+) create mode 100644 arch/arm64/boot/dts/apple/t6040-j616s-extdisp-hdmi.dtsi create mode 100644 arch/arm64/boot/dts/apple/t6040-j616s-extdisp-mem-loader.dtsi create mode 100644 arch/arm64/boot/dts/apple/t6040-j616s-extdisp-usbc.dtsi create mode 100644 arch/arm64/boot/dts/apple/t6040-j616s-extdisp.dtsi diff --git a/arch/arm64/boot/dts/apple/t6040-j616s-extdisp-hdmi.dtsi b/arch/arm64/boot/dts/apple/t6040-j616s-extdisp-hdmi.dtsi new file mode 100644 index 0000000000000..b414a0bddee7d --- /dev/null +++ b/arch/arm64/boot/dts/apple/t6040-j616s-extdisp-hdmi.dtsi @@ -0,0 +1,308 @@ +// SPDX-License-Identifier: GPL-2.0+ OR MIT +/* + * The fixed HDMI path of t6040-j616s-extdisp.dtsi (dcpext0 -> atc3 crossbar + * -> atcphy3 -> DP-to-HDMI converter) with every node left DISABLED. The + * nodes must not be enabled at boot in the device tree: powering the DISPEXT + * chain without the PMP acknowledgement hangs the machine before root. The + * enable gate (drivers/soc/apple/t6041-display-gate.c) switches them on in + * order once the PMP is bound. + * + * dispext0's DART is not in the display node's iommus: the gate appends it + * when it enables the chain, so the internal display's DART domain stays as + * it is without the external displays. + */ + +/* + * DISPEXT0 may only be powered once the PMP has acknowledged it in its + * device-state table (SOC-DEV-PS-REQ/ACK, PTD entries 480/488 on T6041): the + * soc-device record of DISPEXT0 has ID 18 (ADT iop-pmp-nub soc-device, ack + * flag set), so its request bit is 17. Without it, enabling the DISPEXT0 PMGR + * chain hung the machine before root. + * Same contract as the T6030 PMP device-state provider of + * aurora-silicon/linux#358, through the existing pmp-report entries here. + */ +&pmp_report { + pmp_dispext0: report@11 { + compatible = "apple,t6041-pmp-v2-report-entry", + "apple,t6000-pmp-v2-report-entry"; + reg = <0x11>; + label = "pmp-dispext0"; + #power-domain-cells = <0>; + /* parent of the IRQ-safe PMGR chain */ + apple,irq-safe; + status = "disabled"; + }; +}; + +&ps_dispext0_sys { + power-domains = <&pmp_dispext0>; + status = "disabled"; +}; + +&ps_dispext0_fe { + status = "disabled"; +}; + +&ps_dispext0_cpu { + /* + * NEVER enable: the ADT marks DISPEXT0_CPU no_ps (no PMGR power-state + * register, like the internal DISP_CPU). Its probe reads offset 0x350, + * and that read resets the SoC. DCPEXT0's own power gate (ADT power-gates 431) is the + * virtual DISPEXT0-CPU-V; the last real register of the chain is FE. + */ + /delete-property/ apple,always-on; + status = "disabled"; +}; + +&ps_atc3_common { + status = "disabled"; +}; + +&ps_atc3_common_dp { + status = "disabled"; +}; + +&ps_atc3_usb_aon { + status = "disabled"; +}; + +&ps_atc3_phymxwrap { + status = "disabled"; +}; + +&pmgr_mini { + /* ADT ATC3_USB: +0x148, parents ATC3_COMMON and ATC3_USB_AON; the crossbar's domain as on T6030 */ + ps_atc3_usb: power-controller@148 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x148 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc3_usb"; + power-domains = <&ps_atc3_common>, <&ps_atc3_usb_aon>; + status = "disabled"; + }; +}; + +&pinctrl_aop { + status = "disabled"; +}; + +&dcpext0_dart { + /* in the DISPEXT0 block: powered (after the PMP request) before probing */ + power-domains = <&ps_dispext0_fe>; + status = "disabled"; +}; + +&dispext0_dart { + /* in the DISPEXT0 block: powered (after the PMP request) before probing */ + power-domains = <&ps_dispext0_fe>; + status = "disabled"; +}; + +&dcpext0_mbox { + power-domains = <&ps_dispext0_fe>; + status = "disabled"; +}; + +&dcpext0 { + power-domains = <&ps_dispext0_fe>; + /* + * The IOMFB surface layout of the internal DCP; + * without apple,iomfb-surfaces apple-dcp's probe fails with -EINVAL. + */ + apple,iomfb-surfaces = <1 1 0 0>; + status = "disabled"; +}; + +&atcphy3_mbox { + status = "disabled"; +}; + +&atcphy3_xbar { + power-domains = <&ps_atc3_usb>; + status = "disabled"; +}; + +/* + * atcphy3 apply_tunables tables (aciophy_phy_init mode 3), from this machine's ADT /arm-io/atc-phy3 + * (privateartifacts/omarchy-m4-unit/adt.bin): AXI2AF converted entry by entry; every other table is + * byte-identical to atc-phy0 in the ADT, so the atcphy0 values of t6040-j616s-usb3.dtsi are reused + * (AUS40CMN_SHM differs: 0x50). Only the first six (axi2af..lane1) are applied in DP mode. + */ +&atcphy3 { + /* ATC3 ADT tunable_ATC0AXI2AF (port-specific), entries */ + apple,tunable-axi2af = + <0x0 0x800000ff 0x80000025>, + <0x4 0x80ff00ff 0x80250003>, + <0x8 0x80ff00ff 0x80250003>, + <0xc 0x80ff00ff 0x80250005>, + <0x10 0x80ff00ff 0x80250003>, + <0x14 0x80ff00ff 0x80250003>, + <0x18 0x80ff00ff 0x80250003>, + <0x1c 0x80ff00ff 0x80250003>, + <0x20 0x800000ff 0x8000001e>, + <0x24 0x800000ff 0x8000004a>, + <0x28 0x80ff00ff 0x804a0006>, + <0x2c 0x80ff00ff 0x804a0006>, + <0x30 0x80ff00ff 0x804a0006>, + <0x34 0x80ff00ff 0x804a0006>, + <0x38 0x80ff00ff 0x804a0006>, + <0x3c 0x80ff00ff 0x804a0006>, + <0x40 0x800000ff 0x8000001e>, + <0x44 0x800000ff 0x8000001e>, + <0x48 0x800000ff 0x8000001e>, + <0x100 0x000000ff 0x000000ff>, + <0x108 0xffff0006 0x00100000>, + <0x304 0x3f3f0000 0x04040000>, + <0x308 0x000fffff 0x00001681>, + <0x30c 0x000fffff 0x00001680>, + <0x310 0x70000fff 0x000001e0>, + <0x314 0xf0000fff 0x800001e0>, + <0x318 0x70000fff 0x000001e0>, + <0x31c 0xf0000fff 0x800001e0>, + <0x320 0x70000fff 0x000005a0>, + <0x324 0xf0000fff 0x800005a0>, + <0x328 0x70000fff 0x000005a0>, + <0x32c 0xf0000fff 0x800005a0>, + <0x330 0x70000fff 0x000005a0>, + <0x334 0xf0000fff 0x800005a0>, + <0x338 0x70000fff 0x000005a0>, + <0x33c 0x70000fff 0x000005a0>, + <0x340 0x70000fff 0x000005a0>, + <0x344 0x70000fff 0x000005a0>, + <0x348 0x70000fff 0x000005a0>, + <0x34c 0x70000fff 0x000005a0>, + <0x350 0x70000fff 0x000005a0>, + <0x358 0xffffffff 0x0000ffff>, + <0x35c 0xffffffff 0x0000ffff>, + <0x408 0xffff0020 0x00210020>, + <0x40c 0x000fffff 0x00001681>, + <0x410 0x000fffff 0x00001680>, + <0x414 0x70000fff 0x000001e0>, + <0x418 0x70000fff 0x000001e0>, + <0x41c 0x70000fff 0x000001e0>, + <0x420 0x70000fff 0x000001e0>, + <0x424 0x70000fff 0x000005a0>, + <0x428 0x70000fff 0x000005a0>, + <0x42c 0x70000fff 0x000005a0>, + <0x430 0x70000fff 0x000005a0>, + <0x434 0x70000fff 0x000005a0>, + <0x438 0x70000fff 0x000005a0>, + <0x43c 0x70000fff 0x000005a0>, + <0x440 0x70000fff 0x000005a0>, + <0x444 0x70000fff 0x000005a0>, + <0x448 0x70000fff 0x000005a0>, + <0x44c 0x70000fff 0x000005a0>, + <0x450 0x70000fff 0x000005a0>, + <0x454 0x70000fff 0x000005a0>, + <0x948 0x07ff07ff 0x00280000>, + <0xa48 0x07ff07ff 0x00400040>, + <0xb48 0x07ff07ff 0x00b8010e>, + <0xc48 0x07ff07ff 0x00400020>, + <0xd48 0x07ff07ff 0x008a008a>, + <0x1000 0xffffffff 0x7fffffff>, + <0x4008 0x00000001 0x00000001>, + <0x400c 0x00000001 0x00000001>, + <0x4014 0x00000001 0x00000001>, + <0x4018 0x00000001 0x00000000>, + <0x401c 0x00000001 0x00000001>; + /* the remaining tables equal atc-phy0 in the ADT, except AUS40CMN_SHM (0x50) */ + apple,tunable-fabric = + <0x4 0x003f003f 0x00200020>, + <0xc 0x000000f1 0x000000a1>, + <0x10 0x0000801f 0x00000004>, + <0x18 0x003f003f 0x00200020>, + <0x20 0x000000f1 0x000000a1>, + <0x24 0x0000801f 0x00000004>, + <0x2c 0x00ff00ff 0x00960096>, + <0x34 0x000000f1 0x00000071>, + <0x38 0x0000801f 0x00000004>, + <0x40 0x0000003f 0x00000020>, + <0x48 0x0000801f 0x00000004>, + <0x50 0x007f007f 0x00450045>, + <0x58 0x0000801f 0x00000004>, + <0x60 0x0000003f 0x00000020>, + <0x68 0x0000801f 0x00000004>, + <0x70 0x00000001 0x00000001>, + <0x74 0x0000801f 0x00000004>, + <0x7c 0x007f007f 0x00400040>, + <0x80 0x000000f1 0x00000011>, + <0x84 0x00000001 0x00000001>, + <0x88 0x00000001 0x00000001>, + <0x8c 0x0000801f 0x00000004>, + <0x94 0x001f001f 0x00100010>, + <0x98 0x000000f1 0x00000011>, + <0x9c 0x0000801f 0x00000004>, + <0xa4 0x000000ff 0x000000e0>, + <0xa8 0x0000801f 0x00000004>, + <0xb0 0x0000003f 0x00000020>, + <0xb8 0x0000801f 0x00000004>, + <0xc0 0x0000003f 0x00000020>, + <0xc8 0x0000801f 0x00000004>, + <0xd0 0x001f001f 0x00100010>, + <0xd8 0x00000001 0x00000001>, + <0xdc 0x00000001 0x00000001>, + <0xe0 0x0000801f 0x00000004>, + <0xe8 0x0000000f 0x00000008>, + <0xec 0x0000801f 0x00000004>, + <0xf4 0xffffffff 0x01010101>, + <0xf8 0x0000801f 0x00000004>, + <0x100 0x00000001 0x00000001>, + <0x104 0x00000001 0x00000001>, + <0x108 0x0000801f 0x00000004>, + <0x110 0x0000801f 0x00000004>, + <0x118 0x0000801f 0x00000004>, + <0x120 0x00000001 0x00000001>, + <0x124 0x00000001 0x00000001>, + <0x128 0x0000801f 0x00000004>, + <0x130 0xffffffff 0x01010101>, + <0x134 0x0000801f 0x00000004>, + <0x13c 0x00000001 0x00000001>, + <0x140 0x00000001 0x00000001>, + <0x144 0x0000801f 0x00000004>, + <0x14c 0x00000001 0x00000001>, + <0x150 0x00000001 0x00000001>, + <0x154 0x0000801f 0x00000004>, + <0x15c 0x0000801f 0x00000004>, + <0x164 0xffffffff 0x01010101>, + <0x168 0x0000ffff 0x00000101>, + <0x16c 0x0000801f 0x00000004>, + <0x174 0x00000001 0x00000001>, + <0x178 0x00000001 0x00000001>, + <0x17c 0x0000801f 0x00000004>, + <0x184 0x00000001 0x00000001>, + <0x188 0x00000001 0x00000001>, + <0x18c 0x0000801f 0x00000004>; + apple,tunable-common-cfg = + <0x8 0x00000001 0x00000001>; + apple,tunable-axi2af-lioa = + <0x28 0x0000fe2f 0x00000000>, + <0x2c 0x00000007 0x00000000>, + <0x30 0x0000007f 0x00000000>, + <0x34 0x000000ff 0x00000000>, + <0x58 0x0000010b 0x00000000>; + apple,tunable-lane0 = + <0xd094 0x00080000 0x00080000>, + <0xd098 0x00080000 0x00080000>, + <0xd0a4 0x00800000 0x00800000>, + <0xd0a8 0x00200000 0x00200000>; + apple,tunable-lane1 = + <0x1d094 0x00080000 0x00080000>, + <0x1d098 0x00080000 0x00080000>, + <0x1d0a4 0x00800000 0x00800000>, + <0x1d0a8 0x00200000 0x00200000>; + apple,tunable-aus40cmn-shm = + <0xa04 0x0000007c 0x00000050>; + apple,tunable-auspll = + <0x3224 0x00000c00 0x00000800>, + <0x3230 0x0000001f 0x0000000f>, + <0x3240 0x07d8007f 0x04000059>, + <0x32b0 0x0b800000 0x01800000>, + <0x32e8 0x00060001 0x00040001>, + <0x3338 0x0f800000 0x07800000>; + apple,tunable-cio4pll = + <0x3a30 0x0000001f 0x0000000f>, + <0x3a4c 0x0000fe7f 0x0000501e>, + <0x3ab0 0x00700000 0x00300000>, + <0x3ae8 0x00060000 0x00020000>; +}; diff --git a/arch/arm64/boot/dts/apple/t6040-j616s-extdisp-mem-loader.dtsi b/arch/arm64/boot/dts/apple/t6040-j616s-extdisp-mem-loader.dtsi new file mode 100644 index 0000000000000..08d4768494634 --- /dev/null +++ b/arch/arm64/boot/dts/apple/t6040-j616s-extdisp-mem-loader.dtsi @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-2.0+ OR MIT +/* + * The firmware memory of the external DCPs, for a boot loader that fills it + * (m4-loader m4_dcpext_fixup()). + * + * The loader reads /arm-io/dcpextN/iop-dcpextN-nub segment-ranges of the + * booting Mac (__TEXT, __DATA and the heap; __OS_LOG is left out as for the + * internal DCP), gives each placeholder below its reg, iommu-addresses and + * name, enables it, names the regions in the node's memory-region-names and + * sets apple,firmware-version/compat. A placeholder it does not fill is + * deleted together with the node's memory-region. A boot loader without this + * fix-up leaves them disabled, and the enable gate then keeps the external + * displays off. + * + * Nothing here enables dcpext0/1 or their DART/mailbox. + */ + +/ { + reserved-memory { + dcpext0_text: dcpext0-text { + compatible = "apple,asc-mem"; + reg = <0x0 0x0 0x0 0x4000>; + no-map; + status = "disabled"; + }; + + dcpext0_data: dcpext0-data { + compatible = "apple,asc-mem"; + reg = <0x0 0x0 0x0 0x4000>; + no-map; + status = "disabled"; + }; + + dcpext0_heap: dcpext0-heap { + compatible = "apple,asc-mem"; + reg = <0x0 0x0 0x0 0x4000>; + no-map; + status = "disabled"; + }; + + dcpext1_text: dcpext1-text { + compatible = "apple,asc-mem"; + reg = <0x0 0x0 0x0 0x4000>; + no-map; + status = "disabled"; + }; + + dcpext1_data: dcpext1-data { + compatible = "apple,asc-mem"; + reg = <0x0 0x0 0x0 0x4000>; + no-map; + status = "disabled"; + }; + + dcpext1_heap: dcpext1-heap { + compatible = "apple,asc-mem"; + reg = <0x0 0x0 0x0 0x4000>; + no-map; + status = "disabled"; + }; + }; +}; + +&dcpext0 { + memory-region = <&dcpext0_text>, <&dcpext0_data>, <&dcpext0_heap>; + memory-region-names = "text", "data", "heap"; +}; + +&dcpext1 { + memory-region = <&dcpext1_text>, <&dcpext1_data>, <&dcpext1_heap>; + memory-region-names = "text", "data", "heap"; +}; diff --git a/arch/arm64/boot/dts/apple/t6040-j616s-extdisp-usbc.dtsi b/arch/arm64/boot/dts/apple/t6040-j616s-extdisp-usbc.dtsi new file mode 100644 index 0000000000000..493dcbd3a28a0 --- /dev/null +++ b/arch/arm64/boot/dts/apple/t6040-j616s-extdisp-usbc.dtsi @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: GPL-2.0+ OR MIT +/* + * USB-C DisplayPort: dcpext1 over any of the three Type-C ports. As for + * HDMI, the DISPEXT1 chain and dcpext1 stay DISABLED in the DTB and are + * enabled by the enable gate (drivers/soc/apple/t6041-display-gate.c). The + * routes go through the display fabric (typec-routes, end of this file): + * tipd's mode switch reaches dcpext1, which claims the port. Boot-time + * changes: mode-switch on atcphy0..2 (the PHY registers a Type-C mode + * switch; tipd drives it) and the port@3 route endpoints of the connectors. + */ + +/* PMP device-state acknowledgement for DISPEXT1 (soc-device 19, request bit 18), as DISPEXT0. */ +&pmp_report { + pmp_dispext1: report@12 { + compatible = "apple,t6041-pmp-v2-report-entry", + "apple,t6000-pmp-v2-report-entry"; + reg = <0x12>; + label = "pmp-dispext1"; + #power-domain-cells = <0>; + apple,irq-safe; + status = "disabled"; + }; +}; + +/* + * DISPEXT1_SYS sits on FAB7_SOC (ADT pmgr device id2 134, pmgr_aux 0x488, parent + * FAB6_SOC, off at boot), which t6040-pmgr.dtsi does not describe. m4.62 enabled + * DISPEXT1_SYS without it and the first DCPEXT1 register access (its DART) reset + * the SoC. DISPEXT0_SYS's parent FAB2_SOC is on anyway through ATC0..2_COMMON. + */ +&pmgr_aux { + ps_fab7_soc: power-controller@488 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x488 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "fab7_soc"; + power-domains = <&ps_fab6_soc>; + status = "disabled"; + }; +}; + +&ps_dispext1_sys { + power-domains = <&ps_fab7_soc>, <&pmp_dispext1>; + status = "disabled"; +}; + +&ps_dispext1_fe { + status = "disabled"; +}; + +&ps_dispext1_cpu { + /* NEVER enable: ADT no_ps, as DISPEXT0_CPU (its register read resets the SoC). */ + /delete-property/ apple,always-on; + status = "disabled"; +}; + +/* The per-ATC crossbars of the Type-C ports: the ATC USB domain, as atcphy3's. */ +&atcphy0_xbar { + power-domains = <&ps_atc0_usb>; + status = "disabled"; +}; + +&atcphy1_xbar { + power-domains = <&ps_atc1_usb>; + status = "disabled"; +}; + +&atcphy2_xbar { + power-domains = <&ps_atc2_usb>; + status = "disabled"; +}; + +/* tipd switches the PHY into DP alt mode through the connector graph. */ +&atcphy0 { + mode-switch; +}; + +&atcphy1 { + mode-switch; +}; + +&atcphy2 { + mode-switch; +}; + +/* + * Type-C display routes through the display fabric, as T6030 and J700: + * dcpext1 offers one route per port, each a mode switch the port controller + * drives over the connector graph (port@3). The gate enables the routes + * before devices exist, so the port controllers wait for dcpext1's mode + * switches, and probes the controllers again once dcpext1 is bound. A route + * without a registered mode switch would make the port wait for one forever. + */ +&typec0 { + ports { + port@3 { + reg = <3>; + + typec0_dcpext1: endpoint { + remote-endpoint = <&dcpext1_typec0>; + }; + }; + }; +}; + +&typec1 { + ports { + port@3 { + reg = <3>; + + typec1_dcpext1: endpoint { + remote-endpoint = <&dcpext1_typec1>; + }; + }; + }; +}; + +&typec2 { + ports { + port@3 { + reg = <3>; + + typec2_dcpext1: endpoint { + remote-endpoint = <&dcpext1_typec2>; + }; + }; + }; +}; + +&dcpext1 { + apple,connector-type = "USB-C"; + /* as dcpext0: without it apple-dcp's probe fails with -EINVAL (m4.63) */ + apple,iomfb-surfaces = <1 1 0 0>; + phys = <&atcphy0 PHY_TYPE_DP>, <&atcphy1 PHY_TYPE_DP>, <&atcphy2 PHY_TYPE_DP>; + phy-names = "typec0", "typec1", "typec2"; + mux-controls = <&atcphy0_xbar 0>, <&atcphy1_xbar 0>, <&atcphy2_xbar 0>; + mux-control-names = "typec0", "typec1", "typec2"; + /* ADT: atcN-dpphy dp-switch-dfp-endpoint N; dcpext1 ufp 1 port 0 = crossbar state 2 */ + apple,typec-dptx-phys = <0 1 2>; + apple,typec-mux-indices = <2 2 2>; + + typec-routes { + #address-cells = <1>; + #size-cells = <0>; + + route@0 { + reg = <0>; + mode-switch; + status = "disabled"; + + port { + dcpext1_typec0: endpoint { + remote-endpoint = <&typec0_dcpext1>; + }; + }; + }; + + route@1 { + reg = <1>; + mode-switch; + status = "disabled"; + + port { + dcpext1_typec1: endpoint { + remote-endpoint = <&typec1_dcpext1>; + }; + }; + }; + + route@2 { + reg = <2>; + mode-switch; + status = "disabled"; + + port { + dcpext1_typec2: endpoint { + remote-endpoint = <&typec2_dcpext1>; + }; + }; + }; + }; +}; diff --git a/arch/arm64/boot/dts/apple/t6040-j616s-extdisp.dtsi b/arch/arm64/boot/dts/apple/t6040-j616s-extdisp.dtsi new file mode 100644 index 0000000000000..affbad9ba51ad --- /dev/null +++ b/arch/arm64/boot/dts/apple/t6040-j616s-extdisp.dtsi @@ -0,0 +1,345 @@ +// SPDX-License-Identifier: GPL-2.0+ OR MIT +/* + * External display resources of the J616s. Everything here is disabled in + * the device tree: the enable gate (drivers/soc/apple/t6041-display-gate.c) + * switches the chain on once the boot loader has provided the firmware + * memory of the external display processors, so without that the booted + * hardware state is the same as without this file. + * + * Values come from this unit's ADT (25G83); CPU address = ADT address + + * 0x2_0000_0000. Path of the picture: + * HDMI: dcpext0 -> atc3 display crossbar -> atcphy3 (fixed DP PHY, no + * USB controller) -> on-board DP-to-HDMI converter + * USB-C: dcpext1 -> atcN display crossbar -> atcphyN (DP alt mode) + * + * Needs the DP mode of the T6040 Type-C PHY driver (phy-apple-t6040-usb2) + * with its probe path for a PHY without a USB controller (atcphy3), the + * atc-dpxbar,t604x display crossbar driver and the dcpext firmware memory + * (dcp-map style memory-region with iommu-addresses, t6040-j616s-extdisp- + * mem-loader.dtsi). + */ + +/ { + pinctrl_aop: pinctrl@510824000 { + compatible = "apple,t6041-pinctrl", "apple,t8103-pinctrl"; + reg = <0x5 0x10824000 0x0 0x4000>; + + gpio-controller; + #gpio-cells = <2>; + gpio-ranges = <&pinctrl_aop 0 0 85>; + apple,npins = <85>; + + interrupt-controller; + #interrupt-cells = <2>; + interrupt-parent = <&aic>; + interrupts = , + , + , + , + , + , + ; + status = "disabled"; + }; + + dispext0_dart: iommu@3a2300000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x3 0xa2300000 0x0 0xc000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + status = "disabled"; + }; + + dcpext0_dart: iommu@3a2340000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x3 0xa2340000 0x0 0x20000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + status = "disabled"; + }; + + dcpext0: dcp@3a2e00000 { + compatible = "apple,t6041-dcpext", "apple,dcpext"; + reg = <0x3 0xa2e00000 0x0 0x4000>, + <0x3 0xa0000000 0x0 0x4000000>, + <0x3 0xa2240000 0x0 0x10000>, + <0x3 0xa23a8000 0x0 0x4000>, + <0x3 0xa2800000 0x0 0x800000>; + reg-names = "coproc", "disp-0", "disp-1", "disp-2", "disp-3"; + mboxes = <&dcpext0_mbox>; + mbox-names = "mbox"; + iommus = <&dcpext0_dart 23>; + /* guessed: the internal DCP uses 0x20000, J700 DISPEXT0 is +0x8000 */ + apple,bw-scratch = <&pmgr_dcp 0 4 0x28000>; + status = "disabled"; + + dispext0_piodma: piodma { + iommus = <&dispext0_dart 16>; + }; + }; + + dcpext0_mbox: mailbox@3a2e08000 { + compatible = "apple,t6041-asc-mailbox", "apple,asc-mailbox-v4"; + reg = <0x3 0xa2e08000 0x0 0x4000>; + interrupt-parent = <&aic>; + interrupts = , + , + , + ; + interrupt-names = "send-empty", "send-not-empty", + "recv-empty", "recv-not-empty"; + #mbox-cells = <0>; + status = "disabled"; + }; + + dispext1_dart: iommu@602300000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x6 0x02300000 0x0 0xc000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + status = "disabled"; + }; + + dcpext1_dart: iommu@602340000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x6 0x02340000 0x0 0xc000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + status = "disabled"; + }; + + dcpext1: dcp@602e00000 { + compatible = "apple,t6041-dcpext", "apple,dcpext"; + reg = <0x6 0x02e00000 0x0 0x4000>, + <0x6 0x00000000 0x0 0x4000000>, + <0x6 0x02240000 0x0 0x10000>, + <0x6 0x023a8000 0x0 0x4000>, + <0x6 0x02800000 0x0 0x800000>; + reg-names = "coproc", "disp-0", "disp-1", "disp-2", "disp-3"; + mboxes = <&dcpext1_mbox>; + mbox-names = "mbox"; + iommus = <&dcpext1_dart 23>; + /* guessed, as dcpext0 */ + apple,bw-scratch = <&pmgr_dcp 0 4 0x30000>; + status = "disabled"; + + dispext1_piodma: piodma { + iommus = <&dispext1_dart 16>; + }; + }; + + dcpext1_mbox: mailbox@602e08000 { + compatible = "apple,t6041-asc-mailbox", "apple,asc-mailbox-v4"; + reg = <0x6 0x02e08000 0x0 0x4000>; + interrupt-parent = <&aic>; + interrupts = , + , + , + ; + interrupt-names = "send-empty", "send-not-empty", + "recv-empty", "recv-not-empty"; + #mbox-cells = <0>; + status = "disabled"; + }; + + /* ATC3: the HDMI converter's fixed DP PHY; no USB controller, no pipehandler. */ + atcphy3: phy@39aa90000 { + compatible = "apple,t6040-atcphy"; + reg = <0x3 0x9aa90000 0x0 0x4000>, + <0x3 0x9a800000 0x0 0x4000>, + <0x3 0x9b000000 0x0 0x30000>, + <0x3 0x9b200000 0x0 0x4000>, + <0x3 0x9b140000 0x0 0x4000>, + <0x3 0x9b0c0000 0x0 0x10000>, + <0x3 0x98000000 0x0 0x8000>, + <0x3 0x98020000 0x0 0x4000>, + <0x3 0x9b244000 0x0 0x4000>, + <0x3 0x9b250000 0x0 0x4000>; + reg-names = "bank0", "bank1", "core", "cfg", "cpu", "cpu-data", + "axi2af", "axi2af-lioa", "fabric", "common-cfg"; + #phy-cells = <1>; + power-domains = <&ps_atc3_common_dp>; + mboxes = <&atcphy3_mbox>; + mbox-names = "phy-cpu"; + nonposted-mmio; + status = "disabled"; + }; + + atcphy3_mbox: mailbox@39b148000 { + compatible = "apple,t6041-asc-mailbox", "apple,asc-mailbox-v4"; + reg = <0x3 0x9b148000 0x0 0x4000>; + interrupt-parent = <&aic>; + interrupts = , + , + , + ; + interrupt-names = "send-empty", "send-not-empty", + "recv-empty", "recv-not-empty"; + #mbox-cells = <0>; + power-domains = <&ps_atc3_phymxwrap>; + status = "disabled"; + }; + + /* Display crossbars (ADT atcN-dpxbar, atc-dpxbar,t604x): no driver yet. */ + atcphy0_xbar: mux@38324c000 { + compatible = "apple,t6041-display-crossbar"; + reg = <0x3 0x8324c000 0x0 0x4000>; + #mux-control-cells = <1>; + status = "disabled"; + }; + + atcphy1_xbar: mux@38b24c000 { + compatible = "apple,t6041-display-crossbar"; + reg = <0x3 0x8b24c000 0x0 0x4000>; + #mux-control-cells = <1>; + status = "disabled"; + }; + + atcphy2_xbar: mux@39324c000 { + compatible = "apple,t6041-display-crossbar"; + reg = <0x3 0x9324c000 0x0 0x4000>; + #mux-control-cells = <1>; + status = "disabled"; + }; + + atcphy3_xbar: mux@39b24c000 { + compatible = "apple,t6041-display-crossbar"; + reg = <0x3 0x9b24c000 0x0 0x4000>; + #mux-control-cells = <1>; + status = "disabled"; + }; +}; + +/* + * PMGR domains from the ADT device table (+16 offset, +19 block; block 0 = + * pmgr_aux, block 3 = pmgr_mini; checked against the ATC0 nodes). Disabled + * so the pwrstate driver does not touch them yet: DISPEXT0/1 run the + * firmware iBoot loaded, and an always-on domain that happens to be off + * would be powered on by the driver. + */ +&pmgr_aux { + ps_dispext0_sys: power-controller@340 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x340 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dispext0_sys"; + status = "disabled"; + }; + + ps_dispext0_fe: power-controller@348 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x348 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dispext0_fe"; + power-domains = <&ps_dispext0_sys>; + status = "disabled"; + }; + + ps_dispext0_cpu: power-controller@350 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x350 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dispext0_cpu"; + power-domains = <&ps_dispext0_fe>; + apple,always-on; + status = "disabled"; + }; + + ps_atc3_common: power-controller@408 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x408 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc3_common"; + apple,always-on; + status = "disabled"; + }; + + ps_dispext1_sys: power-controller@490 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x490 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dispext1_sys"; + status = "disabled"; + }; + + ps_dispext1_fe: power-controller@498 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x498 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dispext1_fe"; + power-domains = <&ps_dispext1_sys>; + status = "disabled"; + }; + + ps_dispext1_cpu: power-controller@4a0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x4a0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dispext1_cpu"; + power-domains = <&ps_dispext1_fe>; + apple,always-on; + status = "disabled"; + }; +}; + +&pmgr_mini { + ps_atc3_common_dp: power-controller@b8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0xb8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc3_common_dp"; + power-domains = <&ps_atc3_phymxwrap>; + status = "disabled"; + }; + + ps_atc3_usb_aon: power-controller@e0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0xe0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc3_usb_aon"; + apple,always-on; + status = "disabled"; + }; + + ps_atc3_phymxwrap: power-controller@168 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x168 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc3_phymxwrap"; + power-domains = <&ps_atc3_common>, <&ps_atc3_usb_aon>; + status = "disabled"; + }; +}; + +/* + * HDMI port (disabled with dcpext0): converter power SMC gP0f/gP1a (as on + * the M3 Pro J514s), HPD on AOP GPIO 0x24. The converter's reset (gP10), + * force-dpfu (AOP GPIO 0x2a) and its I2C side channel (i2c8 0x08) are left + * undescribed, as on the J514s. + */ +&dcpext0 { + apple,connector-type = "HDMI-A"; + hdmi-hpd-gpios = <&pinctrl_aop 36 GPIO_ACTIVE_HIGH>; + hdmi-pwren-gpios = <&smc_gpio 15 GPIO_ACTIVE_HIGH>; + dp2hdmi-pwren-gpios = <&smc_gpio 26 GPIO_ACTIVE_HIGH>; + phys = <&atcphy3 PHY_TYPE_DP>; + phy-names = "dp-phy"; + mux-controls = <&atcphy3_xbar 0>; + mux-control-names = "dp-xbar"; + mux-index = <0>; + apple,dptx-phy = <3>; +}; diff --git a/arch/arm64/boot/dts/apple/t6040-j616s.dts b/arch/arm64/boot/dts/apple/t6040-j616s.dts index c47e71ed1e258..aa17241c57dd2 100644 --- a/arch/arm64/boot/dts/apple/t6040-j616s.dts +++ b/arch/arm64/boot/dts/apple/t6040-j616s.dts @@ -4331,3 +4331,9 @@ }; #include "t6040-j616s-usb3.dtsi" + +/* External display chain, all disabled: the gate in drivers/soc/apple/t6041-display-gate.c enables it. */ +#include "t6040-j616s-extdisp.dtsi" +#include "t6040-j616s-extdisp-mem-loader.dtsi" +#include "t6040-j616s-extdisp-hdmi.dtsi" +#include "t6040-j616s-extdisp-usbc.dtsi" From d962c07a66b18281e4f135a96230187d4ff4c967 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Bal=C3=A1zsy?= Date: Mon, 26 Oct 2026 15:52:41 +0100 Subject: [PATCH 307/388] soc: apple: t6041-display-gate: power up the J616s external displays MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend the T6041 display gate to the external displays. When both external display processors have their firmware memory (the memory-region nodes of the dcpext are available: the boot loader filled the placeholders), enable the chain after the PMP firmware is bound: the Type-C routes before any device exists (so the port controllers wait for dcpext1's mode switches) with the display subsystem held back, then the HDMI chain, the Type-C chain, and last both processors and the display subsystem together, so the display master is created once with every DART in its iommus. The port controllers are probed again once dcpext1 is bound, as the Type-C core treats a mode switch below a disabled node as absent at their first probe. If the PMP does not come within 20 s, or a step fails, the routes are withdrawn and only the internal panel stays. The processors are enabled only once the infrastructure devices have their drivers (the crossbar and PHY are modules), as a processor that finds one missing is given up for good. Without the firmware memory, with nomodeset, or on another machine, nothing changes. Tested on a J616s with m4-loader rev19-j616s-1 (which fills the memory from the ADT): HDMI and USB-C DisplayPort from boot, eDP unchanged. Signed-off-by: FrantiÅ¡ek Balázsy --- drivers/soc/apple/Kconfig | 6 +- drivers/soc/apple/t6041-display-gate.c | 454 +++++++++++++++++++++++++ 2 files changed, 458 insertions(+), 2 deletions(-) diff --git a/drivers/soc/apple/Kconfig b/drivers/soc/apple/Kconfig index 9033a75140946..cbd8a0f0d430a 100644 --- a/drivers/soc/apple/Kconfig +++ b/drivers/soc/apple/Kconfig @@ -269,8 +269,10 @@ config APPLE_T6041_DISPLAY_GATE boot loader has handed the running display coprocessor over. This option enables those nodes before devices are created, only when the boot loader has published a complete T6041 handoff for the - 26.6.2 display firmware. Without it, or with nomodeset, it does - nothing. + 26.6.2 display firmware. It also powers up the J616s external + display chain (HDMI, Type-C DisplayPort) once the boot loader has + given the external display processors their firmware memory. + Without these, or with nomodeset, it does nothing. Say 'y' if you want the native display on M4 Pro MacBook Pros. diff --git a/drivers/soc/apple/t6041-display-gate.c b/drivers/soc/apple/t6041-display-gate.c index aa088ddb957de..eaa711247399c 100644 --- a/drivers/soc/apple/t6041-display-gate.c +++ b/drivers/soc/apple/t6041-display-gate.c @@ -156,3 +156,457 @@ static int __init apple_t6041_display_gate(void) /* Runs before of_platform_default_populate_init() at arch_initcall_sync. */ arch_initcall(apple_t6041_display_gate); + +/* + * External displays (HDMI behind dcpext0, Type-C behind dcpext1). The + * device tree describes the whole chain disabled; it is enabled here once the + * boot loader has given each external display processor its firmware memory + * (the memory-region nodes of the dcpext are available, or they stay disabled + * placeholders). Before any device exists only the Type-C routes are enabled + * (the port controllers then wait for dcpext1's mode switches) and the + * display subsystem is held back. The power-up waits for the PMP firmware, + * whose acknowledgement the DISPEXT domains need: HDMI chain, Type-C chain, + * then both processors and the display subsystem together, so the display + * master is created once with every DART in its iommus. + */ +#include +#include +#include +#include +#include +#include +#include + +/* + * Two stages, each with its own display DART (a DART still disabled in the + * display subsystem's iommus would make of_iommu drop its whole fwspec): the + * HDMI chain, then the Type-C chain. Run at the same time the two processors + * crashed (dcpext0: axi_rd_err), as they never did one after the other. + * Parents first; the genpd links in the tree do the real ordering. + */ +struct gate_stage { + const char *dart; /* display DART of this stage */ + const char *const *nodes; + unsigned int nr; +}; + +static const char *const gate_hdmi_nodes[] = { + /* ATC3 power, PMP DISPEXT0 request, DISPEXT0, AOP pins, PHY, crossbar, DARTs, mailbox, dcpext0 */ + "/power-management@502800000/power-controller@408", + "/power-management@508300000/power-controller@e0", + "/power-management@508300000/power-controller@168", + "/power-management@508300000/power-controller@b8", + "/power-management@508300000/power-controller@148", + "/pmp-report@5003c0000/report@11", + "/power-management@502800000/power-controller@340", + "/power-management@502800000/power-controller@348", + "/pinctrl@510824000", + "/mailbox@39b148000", + "/phy@39aa90000", + "/mux@39b24c000", + "/iommu@3a2340000", + "/iommu@3a2300000", + "/mailbox@3a2e08000", + "/dcp@3a2e00000", +}; + +static const char *const gate_usbc_nodes[] = { + /* PMP DISPEXT1 request, FAB7_SOC, DISPEXT1, crossbars, DARTs, mailbox, routes, dcpext1 */ + "/pmp-report@5003c0000/report@12", + "/power-management@502800000/power-controller@488", + "/power-management@502800000/power-controller@490", + "/power-management@502800000/power-controller@498", + "/mux@38324c000", + "/mux@38b24c000", + "/mux@39324c000", + "/iommu@602340000", + "/iommu@602300000", + "/mailbox@602e08000", + "/dcp@602e00000/typec-routes/route@0", + "/dcp@602e00000/typec-routes/route@1", + "/dcp@602e00000/typec-routes/route@2", + "/dcp@602e00000", +}; + +static const struct gate_stage gate_stages[] = { + { "/iommu@3a2300000", gate_hdmi_nodes, ARRAY_SIZE(gate_hdmi_nodes) }, + { "/iommu@602300000", gate_usbc_nodes, ARRAY_SIZE(gate_usbc_nodes) }, +}; + +/* A display processor (a direct child of the root) and a Type-C display route. */ +static bool gate_is_dcp(const char *path) +{ + return !strncmp(path, "/dcp@", 5) && !strchr(path + 5, '/'); +} + +static bool gate_is_route(const char *path) +{ + return strstr(path, "/typec-routes/"); +} + +static int gate_display_add_dart(struct of_changeset *cs, struct device_node *display, + struct device_node *dart) +{ + struct property *old, *prop; + __be32 *value; + + old = of_find_property(display, "iommus", NULL); + if (!old || !dart->phandle || old->length % (2 * sizeof(*value))) + return -EINVAL; + prop = kzalloc(sizeof(*prop), GFP_KERNEL); + value = kmalloc(old->length + 2 * sizeof(*value), GFP_KERNEL); + if (!prop || !value) { + kfree(prop); + kfree(value); + return -ENOMEM; + } + memcpy(value, old->value, old->length); + value[old->length / sizeof(*value)] = cpu_to_be32(dart->phandle); + value[old->length / sizeof(*value) + 1] = cpu_to_be32(0); + prop->name = kstrdup("iommus", GFP_KERNEL); + prop->length = old->length + 2 * sizeof(*value); + prop->value = value; + if (!prop->name) { + kfree(value); + kfree(prop); + return -ENOMEM; + } + return of_changeset_update_property(cs, display, prop); +} + +/* + * The processor's firmware memory must be described: every memory-region of + * the dcpext node (the stage's last node) must be an available reserved-memory + * node. A loader that fills the regions from the ADT (m4-loader + * m4_dcpext_fixup) leaves disabled placeholders when it refuses, and an older + * loader leaves them all disabled; enabling the processor then would only + * make its driver fail. /chosen m4,dcpext is that loader's record (0 filled, + * 2 static memory), printed when the check fails. + */ +static bool gate_firmware_memory(const char *dcp) +{ + struct device_node *np, *region; + int i, n; + u32 record[2]; + + np = of_find_node_by_path(dcp); + if (!np) + return false; + n = of_count_phandle_with_args(np, "memory-region", NULL); + for (i = 0; i < n; i++) { + region = of_parse_phandle(np, "memory-region", i); + if (region && of_device_is_available(region) && + of_device_is_compatible(region, "apple,asc-mem")) { + of_node_put(region); + continue; + } + of_node_put(region); + n = -ENODEV; + break; + } + of_node_put(np); + if (n > 0) + return true; + np = of_find_node_by_path("/chosen"); + if (np && !of_property_read_u32_array(np, "m4,dcpext", record, 2)) + pr_err("%s: no firmware memory (loader record <%u %u>), nothing enabled\n", dcp, + record[0], record[1]); + else + pr_err("%s: no firmware memory, nothing enabled\n", dcp); + of_node_put(np); + return false; +} + +/* Every node of @st is there and still disabled, and its processor has firmware memory. */ +static bool gate_stage_ready(const struct gate_stage *st) +{ + struct device_node *np; + unsigned int i; + + if (!gate_firmware_memory(st->nodes[st->nr - 1])) + return false; + for (i = 0; i < st->nr; i++) { + np = of_find_node_by_path(st->nodes[i]); + if (!np) { + pr_err("%s: no such node, nothing enabled\n", st->nodes[i]); + return false; + } + if (of_device_is_available(np)) { + pr_err("%s: already enabled, nothing changed\n", st->nodes[i]); + of_node_put(np); + return false; + } + of_node_put(np); + } + return true; +} + +/* Enable @st's nodes and put its display DART into the display subsystem's iommus. */ +static int gate_stage_apply(const struct gate_stage *st, struct device_node *display) +{ + struct device_node *np; + struct of_changeset *cs; + unsigned int i; + int ret; + + cs = kzalloc(sizeof(*cs), GFP_KERNEL); + if (!cs) + return -ENOMEM; + of_changeset_init(cs); + + np = of_find_node_by_path(st->dart); + ret = np ? gate_display_add_dart(cs, display, np) : -ENODEV; + of_node_put(np); + for (i = 0; !ret && i < st->nr; i++) { + if (gate_is_dcp(st->nodes[i]) || gate_is_route(st->nodes[i])) + continue; + np = of_find_node_by_path(st->nodes[i]); + ret = np ? of_changeset_update_prop_string(cs, np, "status", "okay") : -ENODEV; + of_node_put(np); + } + if (!ret) + ret = of_changeset_apply(cs); + if (ret) { + of_changeset_destroy(cs); + kfree(cs); + } + /* Applied, it stays: the live tree refers to it. */ + return ret; +} + +#define GATE_PMP_WAIT_MS 20000 + +/* Set the status of the processors (externals) or the routes, and the display subsystem, in one changeset. */ +static int gate_set(struct device_node *display, bool dcps, const char *dcp_status, bool routes, + const char *route_status, const char *display_status) +{ + struct device_node *np; + struct of_changeset *cs; + unsigned int i, j; + int ret = 0; + + cs = kzalloc(sizeof(*cs), GFP_KERNEL); + if (!cs) + return -ENOMEM; + of_changeset_init(cs); + for (i = 0; !ret && i < ARRAY_SIZE(gate_stages); i++) + for (j = 0; !ret && j < gate_stages[i].nr; j++) { + const char *path = gate_stages[i].nodes[j]; + + if (!(dcps && gate_is_dcp(path)) && !(routes && gate_is_route(path))) + continue; + np = of_find_node_by_path(path); + ret = np ? of_changeset_update_prop_string(cs, np, "status", + gate_is_dcp(path) ? dcp_status : + route_status) : -ENODEV; + of_node_put(np); + } + if (!ret && display_status) + ret = of_changeset_update_prop_string(cs, display, "status", display_status); + if (!ret) + ret = of_changeset_apply(cs); + if (ret) { + of_changeset_destroy(cs); + kfree(cs); + } + return ret; +} + +static struct device_node *gate_display; +static struct device_node *gate_pmp; +static atomic_t gate_started = ATOMIC_INIT(0); + +/* Internal panel only: the routes go again, so the port controllers probe without mode switches. */ +static void gate_fallback(const char *why) +{ + int ret = gate_set(gate_display, false, NULL, true, "disabled", "okay"); + + pr_err("%s: external displays stay off, internal panel only (%d)\n", why, ret); +} + +/* + * The Type-C core treats a mode switch below a disabled node as absent, so the + * port controllers, which probe before dcpext1 is enabled, only took the USB + * muxes. Once dcpext1 is bound (its route mode switches are registered), let + * each controller wired to a route probe again, so that it takes them too. + */ +static void gate_reprobe_port_controllers(void) +{ + struct device *done[ARRAY_SIZE(gate_usbc_nodes)] = {}; + struct device_node *dcp, *np, *ep, *connector, *ctrl; + struct device *dev = NULL; + unsigned int i, n = 0, tries; + + dcp = of_find_node_by_path("/dcp@602e00000"); + for (tries = 0; dcp && tries < 150; tries++) { + dev = dcp->fwnode.dev; + if (dev && READ_ONCE(dev->driver)) + break; + msleep(200); + } + of_node_put(dcp); + if (!dev || !READ_ONCE(dev->driver)) { + pr_err("dcpext1 not bound, port controllers left as they are\n"); + return; + } + for (i = 0; i < ARRAY_SIZE(gate_usbc_nodes); i++) { + unsigned int k; + + if (!gate_is_route(gate_usbc_nodes[i])) + continue; + np = of_find_node_by_path(gate_usbc_nodes[i]); + ep = np ? of_graph_get_next_endpoint(np, NULL) : NULL; + connector = ep ? of_graph_get_remote_port_parent(ep) : NULL; + ctrl = connector ? of_get_parent(connector) : NULL; + dev = ctrl ? get_device(ctrl->fwnode.dev) : NULL; + of_node_put(ctrl); + of_node_put(connector); + of_node_put(ep); + of_node_put(np); + if (!dev) + continue; + for (k = 0; k < n && done[k] != dev; k++) + ; + if (k < n || !READ_ONCE(dev->driver)) { + put_device(dev); + continue; + } + done[n++] = dev; + if (device_reprobe(dev)) + dev_err(dev, "probe again failed\n"); + else + dev_info(dev, "probed again for the display routes\n"); + } + while (n) + put_device(done[--n]); +} + +/* + * A processor that finds its crossbar or DP PHY without a driver is given up + * for good (the deferred probe timeout is 0 here), and those drivers are + * modules that load only once the stage has created their devices. Wait until + * every such device of the enabled infrastructure has its driver. Returns the + * first path still without one, or NULL. + */ +static const char *gate_unbound_infra(void) +{ + unsigned int i, j; + + for (i = 0; i < ARRAY_SIZE(gate_stages); i++) + for (j = 0; j < gate_stages[i].nr; j++) { + const char *path = gate_stages[i].nodes[j]; + struct device_node *np; + struct device *dev; + + /* Power domains are the genpd core's to wait for; the modules are the point here. */ + if (gate_is_dcp(path) || gate_is_route(path) || + !strncmp(path, "/power-management@", 18)) + continue; + np = of_find_node_by_path(path); + dev = np ? READ_ONCE(np->fwnode.dev) : NULL; + of_node_put(np); + if (dev && !READ_ONCE(dev->driver)) + return path; + } + return NULL; +} + +#define GATE_DRIVER_WAIT_MS 10000 + +static void gate_wait_for_drivers(void) +{ + const char *path = NULL; + unsigned int tries; + + for (tries = 0; tries < GATE_DRIVER_WAIT_MS / 100; tries++) { + path = gate_unbound_infra(); + if (!path) + return; + msleep(100); + } + pr_warn("%s has no driver after " __stringify(GATE_DRIVER_WAIT_MS) " ms, enabling the processors anyway\n", + path); +} + +static void gate_power_up(struct work_struct *work) +{ + unsigned int i; + int ret = 0; + + for (i = 0; !ret && i < ARRAY_SIZE(gate_stages); i++) { + ret = gate_stage_apply(&gate_stages[i], gate_display); + if (!ret) + pr_info("stage %u enabled\n", i); + } + if (!ret) + gate_wait_for_drivers(); + /* Both processors and the display subsystem together: the master binds once, with all DCPs. */ + if (!ret) + ret = gate_set(gate_display, true, "okay", false, NULL, "okay"); + if (ret) { + gate_fallback("could not enable the external displays"); + return; + } + pr_info("processors and display subsystem enabled\n"); + gate_reprobe_port_controllers(); +} +static DECLARE_WORK(gate_power_up_work, gate_power_up); + +static void gate_timeout(struct work_struct *work) +{ + if (!atomic_xchg(&gate_started, 1)) + gate_fallback("no PMP within " __stringify(GATE_PMP_WAIT_MS) " ms"); +} +static DECLARE_DELAYED_WORK(gate_timeout_work, gate_timeout); + +/* The PMP's driver is bound: DISPEXT can be acknowledged now. */ +static int gate_pmp_bound(struct notifier_block *nb, unsigned long action, void *data) +{ + struct device *dev = data; + + if (action != BUS_NOTIFY_BOUND_DRIVER || dev->of_node != gate_pmp || atomic_xchg(&gate_started, 1)) + return NOTIFY_DONE; + cancel_delayed_work(&gate_timeout_work); + queue_work(system_long_wq, &gate_power_up_work); + return NOTIFY_OK; +} +static struct notifier_block gate_pmp_nb = { .notifier_call = gate_pmp_bound }; + +static int __init apple_t6041_extdisp_gate(void) +{ + unsigned int i; + int ret; + + if (!of_machine_is_compatible("apple,j616s")) + return 0; + if (IS_ENABLED(CONFIG_VIDEO) && video_firmware_drivers_only()) + return 0; + + /* Everything must be there and disabled, with firmware memory, before anything is changed. */ + for (i = 0; i < ARRAY_SIZE(gate_stages); i++) + if (!gate_stage_ready(&gate_stages[i])) + return 0; + gate_display = of_find_node_by_path("/display-subsystem"); + gate_pmp = of_find_node_by_path("/pmp@500700000"); + if (!gate_display || !gate_pmp || !of_device_is_available(gate_display) || + !of_device_is_available(gate_pmp)) { + pr_err("no display subsystem or PMP, nothing enabled\n"); + return 0; + } + + /* No device exists yet: routes on, display subsystem held back until the power-up. */ + ret = gate_set(gate_display, false, NULL, true, "okay", "disabled"); + if (ret) { + pr_err("could not prepare the tree: %d, nothing enabled\n", ret); + return 0; + } + ret = bus_register_notifier(&platform_bus_type, &gate_pmp_nb); + if (ret) { + gate_fallback("no bus notifier"); + return 0; + } + schedule_delayed_work(&gate_timeout_work, msecs_to_jiffies(GATE_PMP_WAIT_MS)); + pr_info("routes enabled, waiting for the PMP\n"); + return 0; +} +arch_initcall(apple_t6041_extdisp_gate); From d42eadabb9c4e4547768c427aae2e46a1a5a1443 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Bal=C3=A1zsy?= Date: Mon, 26 Oct 2026 17:22:40 +0100 Subject: [PATCH 308/388] drm/asahi: admit a third loaded T6040 25G83 image MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A J616s with 26.6.2 boot firmware (25G83, iBoot mBoot-18000.161.10) can load a third normalized T6040 TEXT identity: TEXT 0x60000, DATA 0x138000, the same entropy offsets, sha256 d725264d4831b95a5b8a375abf2fb5cdfbd6d7cbc7f1baa2cc8b37ee5d6e5e98. Without it the GPU probe stops with "GPU firmware image not admitted" and the device has no render node. Add the digest to the 26.6.2 T6040 image list next to the two existing identities and to the KUnit test that exercises every listed image through the lookup used by identify(). Signed-off-by: FrantiÅ¡ek Balázsy --- drivers/gpu/drm/asahi/fwid.rs | 7 ++++++- drivers/gpu/drm/asahi/hw/t6040.rs | 11 +++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/asahi/fwid.rs b/drivers/gpu/drm/asahi/fwid.rs index bb1fd3c36f623..be81eac5dfb74 100644 --- a/drivers/gpu/drm/asahi/fwid.rs +++ b/drivers/gpu/drm/asahi/fwid.rs @@ -213,7 +213,7 @@ mod tests { } #[test] - fn t6040_accepts_both_loaded_images() { + fn t6040_accepts_all_loaded_images() { let crate::g17::fw::variant::Firmware::Identified(builds) = crate::hw::t6040::CONFIG.firmware else { @@ -231,6 +231,11 @@ mod tests { 0x31, 0xc3, 0xb2, 0x8d, 0x05, 0x09, 0xa1, 0x7e, 0xb7, 0x61, 0x08, 0xb7, 0xc9, 0xa5, 0x2a, 0xcc, 0xd9, 0xac, ], + [ + 0xd7, 0x25, 0x26, 0x4d, 0x48, 0x31, 0xb9, 0x5a, 0x5b, 0x8a, 0x37, 0x5a, 0xbf, 0x2f, + 0xb5, 0xcd, 0xfb, 0xd6, 0xd7, 0xcb, 0xc7, 0xf1, 0xba, 0xa2, 0xcc, 0x8b, 0x37, 0xee, + 0x5d, 0x6e, 0x5e, 0x98, + ], ]; for (i, digest) in digests.into_iter().enumerate() { let found = measured(build.images, 0x138000, [0x59839, 0x5991c], digest).unwrap(); diff --git a/drivers/gpu/drm/asahi/hw/t6040.rs b/drivers/gpu/drm/asahi/hw/t6040.rs index fa3e51efacadc..11ed37a7c1ef8 100644 --- a/drivers/gpu/drm/asahi/hw/t6040.rs +++ b/drivers/gpu/drm/asahi/hw/t6040.rs @@ -85,6 +85,17 @@ pub(crate) const CONFIG: Config = Config { 0xc9, 0xa5, 0x2a, 0xcc, 0xd9, 0xac, ], }, + crate::fwid::FirmwareBuild { + name: "Apple GFX 25G83 for T6040 (normalized loaded image, second alternate J616s fill)", + text_size: 0x60000, + data_size: 0x138000, + entropy: [0x59839, 0x5991c], + text_sha256: [ + 0xd7, 0x25, 0x26, 0x4d, 0x48, 0x31, 0xb9, 0x5a, 0x5b, 0x8a, 0x37, 0x5a, 0xbf, + 0x2f, 0xb5, 0xcd, 0xfb, 0xd6, 0xd7, 0xcb, 0xc7, 0xf1, 0xba, 0xa2, 0xcc, 0x8b, + 0x37, 0xee, 0x5d, 0x6e, 0x5e, 0x98, + ], + }, ], variant: &fw::variant::V25G83, }]), From 579de5efa9a20ddeefe9a5cd5ad09f32b6d7f731 Mon Sep 17 00:00:00 2001 From: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com> Date: Tue, 27 Oct 2026 19:53:42 +1100 Subject: [PATCH 309/388] drm/apple: carry external VRR through the J616s g5 desktop policy The g5 line already has the internal-panel adaptive-sync policy and failure fallback that overlap the last two commits of #451. Keep that panel path, admit VRR advertised by external sink modes, and program the desktop sync parameter when an external pipe enters or leaves VRR. Ported from the external-display intent of fbalazsy's #451 while preserving its preceding commits. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/iomfb.c | 12 +++++++++--- drivers/gpu/drm/apple/iomfb_template.c | 10 +++++++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index a354580f70dd6..1d583cac75100 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -204,9 +204,15 @@ static inline u64 dcpep_ack(enum dcp_context_id id) bool dcp_desktop_vrr_supported(struct apple_dcp *dcp) { - return dcp->panel.has_vrr && dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && - dcp_has_panel(dcp) && dcp->main_display && - !READ_ONCE(dcp->desktop->vrr_failed); + if (dcp->fw_compat != DCP_FIRMWARE_V_26_6_DESKTOP || + READ_ONCE(dcp->desktop->vrr_failed)) + return false; + + /* External processors advertise VRR on their sink modes, not a panel. */ + if (!dcp->main_display) + return !dcp_has_panel(dcp); + + return dcp_has_panel(dcp) && dcp->panel.has_vrr; } int dcp_desktop_vrr_check(struct apple_dcp *dcp, struct drm_crtc_state *state) diff --git a/drivers/gpu/drm/apple/iomfb_template.c b/drivers/gpu/drm/apple/iomfb_template.c index b1145f733c9bb..ee86743626bc5 100644 --- a/drivers/gpu/drm/apple/iomfb_template.c +++ b/drivers/gpu/drm/apple/iomfb_template.c @@ -3086,9 +3086,13 @@ int DCP_FW_NAME(iomfb_modeset)(struct apple_dcp *dcp, return -ENOMEM; cookie = &modeset->wait; modeset->actual = dcp->mode; - modeset->program_sync = dcp_has_panel(dcp) && dcp->main_display && - (dcp_desktop_vrr_supported(dcp) || dcp->vrr_enabled || - READ_ONCE(dcp->desktop->vrr_failed)); + modeset->program_sync = + (dcp_has_panel(dcp) && dcp->main_display && + (dcp_desktop_vrr_supported(dcp) || dcp->vrr_enabled || + READ_ONCE(dcp->desktop->vrr_failed))) || + (!dcp->main_display && + (mode->vrr || dcp->vrr_enabled || + READ_ONCE(dcp->desktop->vrr_failed))); modeset->vrr = modeset->program_sync && mode->vrr && crtc_state->vrr_enabled && dcp_desktop_vrr_supported(dcp); modeset->min_vrr = mode->min_vrr; From 1665ca50c028fc101a7393826f9039cfe5669f80 Mon Sep 17 00:00:00 2001 From: fbalazsy Date: Tue, 27 Oct 2026 10:03:52 +0100 Subject: [PATCH 310/388] arm64: dts: apple: t6040-j616s: firmware display endpoints, Type-C routes on dcpext0 The desktop 26.6 firmware names the display pipes by their display index (endpoint 0 the internal panel, 1 dcpext0, 2 dcpext1) when two of them drive one display: apple,dcp-endpoint records it, separate from apple,dcp-index, which the driver also uses for the doorbell and the PHY mode. The two external pipes merge upwards only, so a display wider than one pipe (the Samsung Odyssey G95NC, 7680x2160) needs dcpext0 as the primary. dcpext0 therefore also gets a route to every Type-C port next to its fixed HDMI path (a hybrid pipeline, as the T6030 J514s): crossbar state 0 on atcphy0..2 (the ADT's dp-switch-dfp-endpoint 0 = dispext0, as its HDMI route on atcphy3), the connectors' port@3 with an endpoint per pipe. The routes are disabled with the rest of the chain; the display gate enables them in the HDMI stage. --- .../bindings/display/apple,dcp.yaml | 8 ++ .../dts/apple/t6040-j616s-extdisp-usbc.dtsi | 91 ++++++++++++++++++- .../boot/dts/apple/t6040-j616s-extdisp.dtsi | 3 + drivers/soc/apple/t6041-display-gate.c | 3 + 4 files changed, 102 insertions(+), 3 deletions(-) diff --git a/Documentation/devicetree/bindings/display/apple,dcp.yaml b/Documentation/devicetree/bindings/display/apple,dcp.yaml index 3d691822c50b1..4297fb6167300 100644 --- a/Documentation/devicetree/bindings/display/apple,dcp.yaml +++ b/Documentation/devicetree/bindings/display/apple,dcp.yaml @@ -214,6 +214,14 @@ properties: $ref: /schemas/types.yaml#/definitions/uint32 description: Display coprocessor index on multi-DCP systems. + apple,dcp-endpoint: + $ref: /schemas/types.yaml#/definitions/uint32 + description: + The display endpoint this coprocessor is to its firmware (its display + index), by which two external pipes name each other when a display + wider than one pipe is driven by two of them. 0 is the internal + display. + apple,dptx-phy: $ref: /schemas/types.yaml#/definitions/uint32 description: DisplayPort transmitter PHY selector. diff --git a/arch/arm64/boot/dts/apple/t6040-j616s-extdisp-usbc.dtsi b/arch/arm64/boot/dts/apple/t6040-j616s-extdisp-usbc.dtsi index 493dcbd3a28a0..3d375fd93a98e 100644 --- a/arch/arm64/boot/dts/apple/t6040-j616s-extdisp-usbc.dtsi +++ b/arch/arm64/boot/dts/apple/t6040-j616s-extdisp-usbc.dtsi @@ -95,11 +95,19 @@ &typec0 { ports { port@3 { + #address-cells = <1>; + #size-cells = <0>; reg = <3>; - typec0_dcpext1: endpoint { + typec0_dcpext1: endpoint@0 { + reg = <0>; remote-endpoint = <&dcpext1_typec0>; }; + + typec0_dcpext0: endpoint@1 { + reg = <1>; + remote-endpoint = <&dcpext0_typec0>; + }; }; }; }; @@ -107,11 +115,19 @@ &typec1 { ports { port@3 { + #address-cells = <1>; + #size-cells = <0>; reg = <3>; - typec1_dcpext1: endpoint { + typec1_dcpext1: endpoint@0 { + reg = <0>; remote-endpoint = <&dcpext1_typec1>; }; + + typec1_dcpext0: endpoint@1 { + reg = <1>; + remote-endpoint = <&dcpext0_typec1>; + }; }; }; }; @@ -119,11 +135,19 @@ &typec2 { ports { port@3 { + #address-cells = <1>; + #size-cells = <0>; reg = <3>; - typec2_dcpext1: endpoint { + typec2_dcpext1: endpoint@0 { + reg = <0>; remote-endpoint = <&dcpext1_typec2>; }; + + typec2_dcpext0: endpoint@1 { + reg = <1>; + remote-endpoint = <&dcpext0_typec2>; + }; }; }; }; @@ -181,3 +205,64 @@ }; }; }; + +/* + * dcpext0 also offers a route to every Type-C port, next to its fixed HDMI + * path (a hybrid pipeline, as the T6030 J514s). It is the only pipe that + * can be the primary of a two-pipe display (firmware endpoint 1; the pipes + * merge upwards only, m4.138), so a 7680 px sink on USB-C needs it: the + * fabric takes it for a Type-C port with appledrm.typec_hybrid_first. + * ADT: atcN-dpphy dp-switch-dfp-endpoint 0 = dispext0 = crossbar state 0, + * as its HDMI route on atcphy3 (mux-index 0). + */ +&dcpext0 { + phys = <&atcphy3 PHY_TYPE_DP>, + <&atcphy0 PHY_TYPE_DP>, <&atcphy1 PHY_TYPE_DP>, <&atcphy2 PHY_TYPE_DP>; + phy-names = "dp-phy", "typec0", "typec1", "typec2"; + mux-controls = <&atcphy3_xbar 0>, + <&atcphy0_xbar 0>, <&atcphy1_xbar 0>, <&atcphy2_xbar 0>; + mux-control-names = "dp-xbar", "typec0", "typec1", "typec2"; + apple,typec-dptx-phys = <0 1 2>; + apple,typec-mux-indices = <0 0 0>; + + typec-routes { + #address-cells = <1>; + #size-cells = <0>; + + route@0 { + reg = <0>; + mode-switch; + status = "disabled"; + + port { + dcpext0_typec0: endpoint { + remote-endpoint = <&typec0_dcpext0>; + }; + }; + }; + + route@1 { + reg = <1>; + mode-switch; + status = "disabled"; + + port { + dcpext0_typec1: endpoint { + remote-endpoint = <&typec1_dcpext0>; + }; + }; + }; + + route@2 { + reg = <2>; + mode-switch; + status = "disabled"; + + port { + dcpext0_typec2: endpoint { + remote-endpoint = <&typec2_dcpext0>; + }; + }; + }; + }; +}; diff --git a/arch/arm64/boot/dts/apple/t6040-j616s-extdisp.dtsi b/arch/arm64/boot/dts/apple/t6040-j616s-extdisp.dtsi index affbad9ba51ad..c38c36853b02d 100644 --- a/arch/arm64/boot/dts/apple/t6040-j616s-extdisp.dtsi +++ b/arch/arm64/boot/dts/apple/t6040-j616s-extdisp.dtsi @@ -73,6 +73,8 @@ iommus = <&dcpext0_dart 23>; /* guessed: the internal DCP uses 0x20000, J700 DISPEXT0 is +0x8000 */ apple,bw-scratch = <&pmgr_dcp 0 4 0x28000>; + /* firmware Disp_Index, the dual-pipe endpoint ID */ + apple,dcp-endpoint = <1>; status = "disabled"; dispext0_piodma: piodma { @@ -125,6 +127,7 @@ iommus = <&dcpext1_dart 23>; /* guessed, as dcpext0 */ apple,bw-scratch = <&pmgr_dcp 0 4 0x30000>; + apple,dcp-endpoint = <2>; status = "disabled"; dispext1_piodma: piodma { diff --git a/drivers/soc/apple/t6041-display-gate.c b/drivers/soc/apple/t6041-display-gate.c index eaa711247399c..cccbec79e095f 100644 --- a/drivers/soc/apple/t6041-display-gate.c +++ b/drivers/soc/apple/t6041-display-gate.c @@ -207,6 +207,9 @@ static const char *const gate_hdmi_nodes[] = { "/iommu@3a2340000", "/iommu@3a2300000", "/mailbox@3a2e08000", + "/dcp@3a2e00000/typec-routes/route@0", + "/dcp@3a2e00000/typec-routes/route@1", + "/dcp@3a2e00000/typec-routes/route@2", "/dcp@3a2e00000", }; From 90cd05465e28617293d193714e4f38fcfb31a25a Mon Sep 17 00:00:00 2001 From: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:07:08 +1100 Subject: [PATCH 311/388] selftests: apple-dcp: keep the g5 desktop callback fixture once MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The g5 lifecycle fixture already extracts the real desktop 26.6 callback checker. The #451 fixture follow-up adds an assert stub for its older base, which makes three g5 host scenarios fail to compile with a duplicate symbol. Keep the extracted checker and remove only that redundant stub. The original #451 commits remain attributed to FrantiÅ¡ek Balázsy. This is a g5 integration fixup. Assisted-by: LLM Signed-off-by: Ryan Murray --- tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py index 518cf23473d61..f17525db99a6b 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py @@ -1883,9 +1883,6 @@ def dispatcher(root): static struct dcp_channel *dcp_get_channel(struct apple_dcp *d, enum dcp_context_id c) { (void)c; return &d->channel; } static int dcp_parse_tag(char *p) { (void)p; return case_tag; } -/* A desktop 26.6 callback is not under test here: no scenario selects that firmware. */ -static bool __attribute__((unused)) iomfb_check_callback_v26_6_desktop(int tag, u32 in_len, u32 out_len) -{ (void)tag; (void)in_len; (void)out_len; assert(!"desktop 26.6 callback in an H17P scenario"); return false; } static bool of_machine_is_compatible(const char *n) { (void)n; return machine; } static bool of_device_is_compatible(struct device_node *n, const char *s) { (void)s; return n->external; } From 48fe30da9842007954558d215edae084c90ee1ff Mon Sep 17 00:00:00 2001 From: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:31:38 +1100 Subject: [PATCH 312/388] drm/apple: combine g5 exact desktop callbacks with the #451 bounds table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The g5 line already validates exact record lengths for its desktop 26.6 callbacks. #451 adds a minimum-length table for the full handler set, so a straight replay defines the checker twice and fails the WERROR build. Keep the g5 exact checks as a helper, fill the four g5 handlers absent from the new table, then require both checks. The table now covers every registered desktop handler. Point the synthetic desktop fixtures at the exact helper; production dispatch uses the combined checker. The original #451 commits remain attributed to FrantiÅ¡ek Balázsy. This is a g5 integration fixup. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/iomfb_v26_6_desktop.c | 10 ++++++++-- .../testing/selftests/drivers/gpu/apple-dcp/desktop.py | 3 ++- .../selftests/drivers/gpu/apple-dcp/lifecycle.py | 5 +++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c b/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c index 1e5f431a88137..d6f649391ced3 100644 --- a/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c +++ b/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c @@ -132,7 +132,7 @@ static bool trampoline_panel_dictionary_v26_6(struct apple_dcp *dcp, int tag, return true; } -bool iomfb_check_callback_v26_6_desktop(int tag, u32 in_len, u32 out_len) +static bool iomfb_check_callback_exact_v26_6_desktop(int tag, u32 in_len, u32 out_len) { switch (tag) { case 201: @@ -311,6 +311,7 @@ static const struct dcp_cb_min_v26_6_desktop cb_min[IOMFB_MAX_CB] = { [134] = CB_MIN(0, sizeof(u32)), [201] = CB_MIN(sizeof(struct dcp_map_buf_req), sizeof(struct DCP_FW_NAME(dcp_map_buf_resp))), + [202] = CB_MIN(sizeof(struct dcp_unmap_buf_req_v26_6), 0), [206] = CB_MIN(0, sizeof(u8)), [207] = CB_MIN(0, sizeof(u8)), [208] = CB_MIN(0, 0), @@ -328,6 +329,9 @@ static const struct dcp_cb_min_v26_6_desktop cb_min[IOMFB_MAX_CB] = { [415] = CB_MIN(0, sizeof(u8)), [451] = CB_MIN(sizeof(struct dcp_allocate_buffer_req), sizeof(struct dcp_allocate_buffer_resp)), + [452] = CB_MIN(sizeof(struct dcp_map_physical_req), + sizeof(struct dcp_map_physical_resp)), + [454] = CB_MIN(sizeof(u32), sizeof(u32)), [552] = CB_MIN(0, sizeof(u8)), [561] = CB_MIN(0, sizeof(u8)), [563] = CB_MIN(0, sizeof(u8)), @@ -339,6 +343,7 @@ static const struct dcp_cb_min_v26_6_desktop cb_min[IOMFB_MAX_CB] = { [576] = CB_MIN(0, 0), [581] = CB_MIN(0, 0), [582] = CB_MIN(0, sizeof(u8)), + [588] = CB_MIN(0, 0), [590] = CB_MIN(sizeof(struct DCP_FW_NAME(dc_swap_complete_resp)), 0), [592] = CB_MIN(sizeof(dcp_swap_intent_t), 0), [593] = CB_MIN(sizeof(u32), 0), @@ -354,7 +359,8 @@ bool iomfb_check_callback_v26_6_desktop(int tag, u32 in_len, u32 out_len) { if (tag < 0 || tag >= IOMFB_MAX_CB || !cb_min[tag].valid) return false; - return in_len >= cb_min[tag].in_len && out_len >= cb_min[tag].out_len; + return in_len >= cb_min[tag].in_len && out_len >= cb_min[tag].out_len && + iomfb_check_callback_exact_v26_6_desktop(tag, in_len, out_len); } void DCP_FW_NAME(iomfb_start)(struct apple_dcp *dcp) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py index 60b903f85d22f..0b00a6a81d33f 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py @@ -389,7 +389,8 @@ def panel_identity(root, output=None): dictionary += block((root/'drivers/gpu/drm/apple/parser.c').read_text(), 'int parse_panel_edid_data(') dictionary += block(backend, 'struct dcp_dict_passthrough_req_v26_6 {')+';\n' dictionary += block(backend, 'static bool trampoline_panel_dictionary_v26_6(') - dictionary += block(backend, 'bool iomfb_check_callback_v26_6_desktop(') + dictionary += block(backend, 'static bool iomfb_check_callback_exact_v26_6_desktop(') + dictionary += '\n#define iomfb_check_callback_v26_6_desktop iomfb_check_callback_exact_v26_6_desktop\n' bindings = '\n'.join(line for line in block(backend, 'static const iomfb_cb_handler cb_handlers[').splitlines() if '[413]' in line or '[552]' in line or '[561]' in line) unit = code + r''' diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py index f17525db99a6b..440b5202f670a 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py @@ -1936,5 +1936,6 @@ def dispatcher(root): """ desktop = (root / "drivers/gpu/drm/apple/iomfb_v26_6_desktop.c").read_text() - checker = block(desktop, 'bool iomfb_check_callback_v26_6_desktop(') - return prefix + checker + source[start:end] + suffix + checker = block(desktop, 'static bool iomfb_check_callback_exact_v26_6_desktop(') + alias = '\n#define iomfb_check_callback_v26_6_desktop iomfb_check_callback_exact_v26_6_desktop\n' + return prefix + checker + alias + source[start:end] + suffix From e3e7131681e248aeb54a82d460739fa5ec50db7f Mon Sep 17 00:00:00 2001 From: fbalazsy Date: Tue, 27 Oct 2026 10:03:52 +0100 Subject: [PATCH 313/388] drm/apple: drive a display wider than one pipe with two desktop 26.6 pipes One DISPEXT pipe of the desktop 26.6 firmware drives modes up to 6720 px wide ("IOMFB removing mode: 7680 x 2160, max width: 6720"); a 7680x2160 sink lost its native mode. The firmware supports a two-pipe display: the primary pipe and a peer merge their frame timing generators and each encodes half of the line, as the T6050 firmware-27 peer (#485) does. The primary answers the display allocation in SET_TILED_DISPLAY_HINTS with its own and the peer's endpoint IDs when the sink is wider than one pipe and an idle pipe above it exists (apple,dcp-endpoint; the pipes merge upwards only: dcpext0 is the primary, dcpext1 its peer; its sinkless HDMI link does not make dcpext0 busy, that link is released before the pair comes up). With the pair reserved the hotplug record is echoed as on firmware 27, so both pipes know each other's display ID. A DRM mode set wider than one pipe then runs the merge on both pipes: the primary's video data (A364) is split into a half-width derived mode inserted on both (A365, A366), both pipes are powered as a pair (A473), their master swap IDs aligned (A360/A361), and the paired mode set (A413) run with the tiling rendezvous the firmware drives through D115 (read) and D116 (write): membership, phase, display IDs, offsets; the DSC capabilities, bits per pixel and slice hints are copied from the primary to the peer in between (A367..A373). The same mode set again keeps the pair (DRM repeats it right after the connector came up, and the merged pipe no longer resolves the original timing). Each frame is the primary's swap: its left tile stays in the swap, the right tile goes to the peer as its own swap_start/swap_submit chain with the primary's swap ID (the firmware does not use the IOMFB transaction queue on this firmware); the peer's completion is not a DRM event. A merged pair is stopped as a pair before the primary's power-off or sleep: the single-pipe power-off of a merged primary timed out and the peer's firmware looped. Adaptive sync follows the CRTC on the pair: IOMFBParameter 14 goes to both pipes with the same minimum rate before the paired mode set. A pipe keeps the parameter across a replug, so a peer that last ran a VRR mode alone merged with VRR on while the primary ran without it, and its firmware crashed. A changed rate (VRR switched on or off) stops the pair and installs it again, as the mode set applies it on one pipe. --- drivers/gpu/drm/apple/dcp-internal.h | 17 + drivers/gpu/drm/apple/dcp-peer-v26_6.h | 107 ++ drivers/gpu/drm/apple/dcp-peer-v26_6.inc | 1054 +++++++++++++++++++ drivers/gpu/drm/apple/dcp.c | 6 + drivers/gpu/drm/apple/dcp.h | 17 + drivers/gpu/drm/apple/dptxep.c | 24 + drivers/gpu/drm/apple/iomfb_template.c | 101 +- drivers/gpu/drm/apple/iomfb_v26_6_desktop.c | 2 + 8 files changed, 1327 insertions(+), 1 deletion(-) create mode 100644 drivers/gpu/drm/apple/dcp-peer-v26_6.h create mode 100644 drivers/gpu/drm/apple/dcp-peer-v26_6.inc diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index 591ddf250fc84..545d688b2ea53 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -18,6 +18,7 @@ #include #include "dcp-fabric.h" +#include "dcp-peer-v26_6.h" #include "dcp-fabric-session.h" #include "dcp-relink.h" #include "dptxep.h" @@ -287,6 +288,22 @@ struct apple_dcp { bool vblank_works_disabled; struct dcp_fabric_pipeline fabric; struct device *dev; + /* Peer DCP for dual-pipe (desktop 26.6): the primary reserves an idle + * peer pipe; the peer records its owner. Protected by dcp_peer_lock. + */ + struct apple_dcp *peer_reserved; + struct apple_dcp *peer_owner; + struct dcp_peer_tiling peer_tiling; + bool peer_unbinding; + /* Firmware display endpoint (its Disp_Index), apple,dcp-endpoint; + * separate from index, which also selects the doorbell and PHY mode. */ + u32 peer_endpoint; + /* Paired DRM scanout on the primary; under dcp_peer_lock. */ + bool peer_active, peer_failed; + /* The DRM mode the pair was set to (timing, color mode IDs, minRR). */ + u32 peer_mode_timing, peer_mode_color, peer_min_vrr; + /* On the peer: its first completion since the pair was set was logged. */ + bool peer_complete_logged; struct platform_device *piodma; bool piodma_created; struct iommu_domain *iommu_dom; diff --git a/drivers/gpu/drm/apple/dcp-peer-v26_6.h b/drivers/gpu/drm/apple/dcp-peer-v26_6.h new file mode 100644 index 0000000000000..df83045cfa73e --- /dev/null +++ b/drivers/gpu/drm/apple/dcp-peer-v26_6.h @@ -0,0 +1,107 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ +#ifndef APPLE_DCP_PEER_V26_6_H +#define APPLE_DCP_PEER_V26_6_H + +#include +#include +#include + +struct dcp_peer_tiling { + u32 phase, modeset_state, x, y, width, height, display_id; + bool member, vftg, primary, ready, mode_flag, display_id_valid; + bool modeset_complete; +}; + +static inline bool dcp_peer_tiling_get( + const struct dcp_peer_tiling *own, + const struct dcp_peer_tiling *related, u32 event, u32 param, u32 *value) +{ + const struct dcp_peer_tiling *target = own; + + if (event == 2 && own->member) { + if (!related) + return false; + target = related; + } + switch (param) { + case 1: *value = target->member; return true; + case 2: + if (event == 1 || target->member) + *value = target->phase; + return true; + case 3: return true; + case 4: + if (event != 1 && !target->member) + return true; + if (!target->display_id_valid) + return false; + *value = target->display_id; + return true; + case 5: case 12: *value = target->x; return true; + case 6: + if (event == 1 || target->member) + *value = target->ready; + return true; + case 8: *value = own->vftg; return true; + case 9: *value = own->primary; return true; + case 10: + if (event == 1 || target->member) + *value = target->modeset_state; + return true; + case 11: *value = 0; return true; + case 13: *value = target->y; return true; + case 14: *value = target->mode_flag; return true; + default: return false; + } +} + +static inline bool dcp_peer_tiling_set(struct dcp_peer_tiling *own, + struct dcp_peer_tiling *related, u32 event, u32 param, u32 value) +{ + struct dcp_peer_tiling *target = event == 2 ? related : own; + + if (!target) + return false; + switch (param) { + case 2: target->phase = value; return true; + case 6: target->ready = !!value; return true; + case 10: target->modeset_state = value; return true; + default: return false; + } +} + +/* + * Display allocation hints — 160 bytes, layout shared with fw27. + * The AP fills primary/peer endpoint IDs and extra-pipe count for a sink + * wider than one pipe's maximum. + */ +struct dcp_display_hints { + u8 hints[0x6c]; + u8 padding[4]; + __le32 primary_id, peer_id; + u8 extra_pipes, flags; + u8 allocation_reserved[22]; + __le32 status; + u8 tail[12]; +} __packed; + +static_assert(sizeof(struct dcp_display_hints) == 160); +static_assert(offsetof(struct dcp_display_hints, primary_id) == 0x70); +static_assert(offsetof(struct dcp_display_hints, status) == 0x90); + +/* The widest mode one desktop 26.6 DISPEXT pipe drives (firmware: "max width: 6720"). */ +#define DCP_DESKTOP_PIPE_MAX_WIDTH 6720 + +/* + * Peer validity by firmware endpoint (apple,dcp-endpoint, the firmware's + * Disp_Index). Endpoint 0 is the internal display and never a peer; the + * external pipes merge upwards only (the T8132 rule inherited by T6050): + * dcpext0 (1) is the primary, dcpext1 (2) its peer. The other direction + * pairs, but the peer's half of the picture is noise. + */ +static inline bool dcp_peer_index_allowed_26_6(u32 primary, u32 peer) +{ + return primary && peer > primary; +} + +#endif diff --git a/drivers/gpu/drm/apple/dcp-peer-v26_6.inc b/drivers/gpu/drm/apple/dcp-peer-v26_6.inc new file mode 100644 index 0000000000000..8be2e69a03e5d --- /dev/null +++ b/drivers/gpu/drm/apple/dcp-peer-v26_6.inc @@ -0,0 +1,1054 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ +/* + * Desktop 26.6 dual-pipe peer DCP — allocation, tiling rendezvous and + * paired modeset for a sink wider than one pipe's maximum (G95NC 7680x2160). + * + * The 26.6 firmware uses the same A360-A373 display-pipeline methods as + * firmware 27; its set_power_state is A473. The sequence follows the + * T6050 firmware-27 peer (aurora-silicon/linux#485), with the method and + * callback numbers of the desktop 26.6 firmware: D115 reads the tiling + * state, D116 writes it, and the peer's swaps go as a direct + * swap_start/swap_submit chain (nothing advances the IOMFB transaction + * queue on this firmware). The two DISPEXT pipes merge upwards only: + * the primary is firmware endpoint 1 (dcpext0), the peer endpoint 2. + */ + +static DEFINE_MUTEX(dcp_peer_lock); + +/* ---- tiling state (D114/D115 callbacks) ---- */ + +bool dcp_peer_tiling_state_26_6(struct apple_dcp *dcp, bool write, + u32 event, u32 param, u32 *value) +{ + struct apple_dcp *peer; + + guard(mutex)(&dcp_peer_lock); + if (dcp->peer_unbinding || dcp->crashed) + return false; + peer = dcp->peer_reserved ?: dcp->peer_owner; + if (!peer || peer->peer_unbinding || peer->crashed) + return false; + if (!dcp->peer_tiling.vftg || !peer->peer_tiling.vftg) + return false; + if (write) + return dcp_peer_tiling_set(&dcp->peer_tiling, + &peer->peer_tiling, event, param, *value); + return dcp_peer_tiling_get(&dcp->peer_tiling, &peer->peer_tiling, + event, param, value); +} + +/* ---- synchronous RPC helper ---- */ + +struct dcp_peer_wait { + struct kref refs; + struct completion done; + size_t size; + u8 reply[128]; +}; + +static void dcp_peer_wait_free(struct kref *ref) +{ + kfree(container_of(ref, struct dcp_peer_wait, refs)); +} + +static void dcp_peer_wait_done(struct apple_dcp *dcp, void *out, void *cookie) +{ + struct dcp_peer_wait *wait = cookie; + + if (wait->size && out) + memcpy(wait->reply, out, min(wait->size, sizeof(wait->reply))); + complete(&wait->done); + kref_put(&wait->refs, dcp_peer_wait_free); +} + +static int dcp_peer_call(struct apple_dcp *dcp, const char *tag, + void *in, size_t in_size, void *out, size_t out_size) +{ + struct dcp_method_entry method = { .name = "peer_call" }; + struct dcp_peer_wait *wait; + int ret = 0; + + if (out_size > sizeof(wait->reply)) + return -EINVAL; + wait = kzalloc(sizeof(*wait), GFP_KERNEL); + if (!wait) + return -ENOMEM; + memcpy(method.tag, tag, 4); + wait->size = out_size; + init_completion(&wait->done); + kref_init(&wait->refs); + kref_get(&wait->refs); + dcp_push(dcp, false, &method, in_size, out_size, in, + dcp_peer_wait_done, wait); + if (!wait_for_completion_timeout(&wait->done, + msecs_to_jiffies(!memcmp(tag, "A413", 4) ? 8500 : 2000))) + ret = -ETIMEDOUT; + else if (out_size && out) + memcpy(out, wait->reply, out_size); + kref_put(&wait->refs, dcp_peer_wait_free); + return ret; +} + +static int dcp_peer_call_oob(struct apple_dcp *dcp, const char *tag, + void *in, size_t in_size, void *out, size_t out_size) +{ + struct dcp_method_entry method = { .name = "peer_call_oob" }; + struct dcp_peer_wait *wait; + int ret = 0; + + if (out_size > sizeof(wait->reply)) + return -EINVAL; + wait = kzalloc(sizeof(*wait), GFP_KERNEL); + if (!wait) + return -ENOMEM; + memcpy(method.tag, tag, 4); + wait->size = out_size; + init_completion(&wait->done); + kref_init(&wait->refs); + kref_get(&wait->refs); + dcp_push(dcp, true, &method, in_size, out_size, in, + dcp_peer_wait_done, wait); + if (!wait_for_completion_timeout(&wait->done, msecs_to_jiffies(2000))) + ret = -ETIMEDOUT; + else if (out_size && out) + memcpy(out, wait->reply, out_size); + kref_put(&wait->refs, dcp_peer_wait_free); + return ret; +} + +/* ---- peer release / forget ---- */ + +static void dcp_peer_forget(struct apple_dcp *dcp) +{ + guard(mutex)(&dcp_peer_lock); + dcp->peer_unbinding = true; + if (dcp->peer_reserved) { + dcp->peer_reserved->peer_owner = NULL; + memset(&dcp->peer_reserved->peer_tiling, 0, + sizeof(dcp->peer_reserved->peer_tiling)); + dcp->peer_reserved = NULL; + memset(&dcp->peer_tiling, 0, sizeof(dcp->peer_tiling)); + } + if (dcp->peer_owner) { + memset(&dcp->peer_owner->peer_tiling, 0, + sizeof(dcp->peer_owner->peer_tiling)); + memset(&dcp->peer_tiling, 0, sizeof(dcp->peer_tiling)); + dcp->peer_owner->peer_reserved = NULL; + dcp->peer_owner = NULL; + } +} + +/* ---- display IDs from D575 hotplug ---- */ + +void dcp_peer_display_ids(struct apple_dcp *dcp, u32 own, u32 related) +{ + struct apple_dcp *peer; + + guard(mutex)(&dcp_peer_lock); + peer = dcp->peer_reserved ?: dcp->peer_owner; + if (!peer || own == related || dcp->peer_unbinding || peer->peer_unbinding) + return; + dcp->peer_tiling.display_id = own; + dcp->peer_tiling.display_id_valid = true; + peer->peer_tiling.display_id = related; + peer->peer_tiling.display_id_valid = true; +} + +/* ---- peer allocation (DPTX call 21 handler) ---- */ + +/* + * Unlike T6050, where the peer is a pipe without outputs, the only T6041 + * candidate (dcpext0) has Type-C routes and HDMI. It qualifies while idle: + * no active route and nothing connected. Its fixed HDMI path brings the + * DPTX link up at start without a sink (dcp_desktop_dp2hdmi_hpd()); that + * link alone does not make it busy. Returns a reason mask, 0 = eligible. + */ +static bool dcp_peer_hdmi_sinkless(struct apple_dcp *p) +{ + return p->fixed_phy && p->hdmi_hpd && + !(p->connector && READ_ONCE(p->connector->connected)); +} + +static u32 dcp_peer_reject_26_6(struct apple_dcp *dcp, struct apple_dcp *p) +{ + u32 why = 0; + + if (!p->active || p->crashed || p->peer_unbinding) + why |= BIT(0); + if (p->peer_owner || p->peer_reserved) + why |= BIT(1); + if (p->active_typec_route) + why |= BIT(2); + if ((p->connector && p->connector->connected) || p->dptxport[1].connected) + why |= BIT(3); + if (p->dptxport[0].connected && !dcp_peer_hdmi_sinkless(p)) + why |= BIT(8); + if (!dcp_peer_index_allowed_26_6(dcp->peer_endpoint, p->peer_endpoint)) + why |= BIT(4); + if (p->dptx_die != dcp->dptx_die) + why |= BIT(5); + if (!p->crtc || p->crtc->base.dev != dcp->crtc->base.dev) + why |= BIT(6); + if (p->fw_compat != DCP_FIRMWARE_V_26_6_DESKTOP) + why |= BIT(7); + return why; +} + +int dcp_peer_hints_26_6(struct apple_dcp *dcp, struct dptx_port *port, + const void *data, size_t size, void *reply, size_t reply_size) +{ + struct dcp_display_hints *r = reply; + struct apple_dcp *peer = NULL; + struct drm_crtc *crtc; + u32 width; + + if (size != sizeof(*r) || reply_size != sizeof(*r)) + return -EINVAL; + memcpy(reply, data, sizeof(*r)); + r->status = 0; + if (!of_device_is_compatible(dcp->dev->of_node, "apple,t6041-dcpext") || + port->unit || !dcp->crtc) + return 0; + + width = get_unaligned_le32(r->hints + 0x38); + if (!(r->hints[0] & 1) || !width || width <= DCP_DESKTOP_PIPE_MAX_WIDTH) { + memset(&r->primary_id, 0, 32); + return 0; + } + + guard(mutex)(&dcp_peer_lock); + if (dcp->peer_unbinding || dcp->crashed) + return -ENODEV; + if (dcp->peer_owner) { + memset(&r->primary_id, 0, 32); + return 0; + } + peer = dcp->peer_reserved; + if (!peer) { + drm_for_each_crtc(crtc, dcp->crtc->base.dev) { + struct apple_dcp *p = platform_get_drvdata(to_apple_crtc(crtc)->dcp); + u32 why; + + if (!p || p == dcp || + !of_device_is_compatible(p->dev->of_node, "apple,t6041-dcpext")) + continue; + why = dcp_peer_reject_26_6(dcp, p); + dev_dbg(dcp->dev, "peer candidate %s endpoint %u: reject %#x\n", + dev_name(p->dev), p->peer_endpoint, why); + if (why) + continue; + if (!peer || p->peer_endpoint < peer->peer_endpoint) + peer = p; + } + if (!peer) { + dev_info(dcp->dev, "no idle peer pipe for a %u px display\n", width); + memset(&r->primary_id, 0, 32); + return 0; + } + dcp->peer_reserved = peer; + peer->peer_owner = dcp; + dev_info(dcp->dev, "reserved peer %s endpoint %u for %ux%u\n", + dev_name(peer->dev), peer->peer_endpoint, width, + get_unaligned_le32(r->hints + 0x3c)); + } + + r->primary_id = cpu_to_le32(dcp->peer_endpoint); + r->peer_id = cpu_to_le32(peer->peer_endpoint); + /* One extra pipe, full dual pipe: the limited-dual-pipe flag stays clear. */ + r->extra_pipes = 1; + r->flags &= ~3; + return 0; +} + +/* ---- paired power (A473 set_power_state) ---- */ + +/* As dcp_power_state_req(): the state, no transition flags, a result. */ +static void dcp_peer_power_req(u8 *req, bool on) +{ + memset(req, 0, 16); + put_unaligned_le64(on, req); + memset(req + 13, 0xaa, 3); +} + +static int dcp_peer_power(struct apple_dcp *dcp, struct apple_dcp *peer, bool on) +{ + struct dcp_peer_wait *wait; + struct dcp_method_entry method = { .name = "peer_power", + .tag = {'A', '4', '7', '3'} }; + u8 power[16], response[8]; + int ret; + + wait = kzalloc(sizeof(*wait), GFP_KERNEL); + if (!wait) + return -ENOMEM; + kref_init(&wait->refs); + init_completion(&wait->done); + wait->size = sizeof(response); + dcp_peer_power_req(power, on); + kref_get(&wait->refs); + dcp_push(dcp, false, &method, sizeof(power), sizeof(response), power, + dcp_peer_wait_done, wait); + ret = dcp_peer_call(peer, "A473", power, sizeof(power), + response, sizeof(response)); + if (!ret && get_unaligned_le32(response + 4)) + ret = -EIO; + if (!wait_for_completion_timeout(&wait->done, msecs_to_jiffies(2000))) + ret = ret ?: -ETIMEDOUT; + else if (!ret && get_unaligned_le32(wait->reply + 4)) + ret = -EIO; + kref_put(&wait->refs, dcp_peer_wait_free); + dev_dbg(dcp->dev, "peer power %u result=%d\n", on, ret); + return ret; +} + +/* ---- quiesce: tear down the pair ---- */ + +static int dcp_peer_quiesce(struct apple_dcp *dcp, struct apple_dcp *peer) +{ + __le32 manage = 0; + int ret; + + { + guard(mutex)(&dcp_peer_lock); + dcp->peer_tiling.modeset_complete = false; + peer->peer_tiling.modeset_complete = false; + } + ret = dcp_peer_power(dcp, peer, false); + if (ret) + return ret; + msleep(100); + ret = dcp_peer_call(dcp, "A362", &manage, sizeof(manage), NULL, 0); + if (ret) + return ret; + ret = dcp_peer_call(peer, "A362", &manage, sizeof(manage), NULL, 0); + if (ret) + return ret; + { + guard(mutex)(&dcp_peer_lock); + dcp->peer_tiling.member = peer->peer_tiling.member = false; + dcp->peer_tiling.vftg = peer->peer_tiling.vftg = false; + dcp->peer_tiling.modeset_complete = false; + peer->peer_tiling.modeset_complete = false; + dcp->peer_tiling.phase = peer->peer_tiling.phase = 0; + dcp->peer_tiling.modeset_state = peer->peer_tiling.modeset_state = 0; + dcp->peer_tiling.ready = peer->peer_tiling.ready = false; + } + return 0; +} + +/* + * Back to one pipe: quiesce the pair, then power the primary again. A DRM + * commit that only changes the mode does not power the CRTC on again. + */ +static int dcp_peer_unpair(struct apple_dcp *dcp, struct apple_dcp *peer) +{ + u8 power[16], response[8]; + int ret; + + dcp_peer_power_req(power, true); + ret = dcp_peer_quiesce(dcp, peer); + if (!ret) + ret = dcp_peer_call(dcp, "A473", power, sizeof(power), + response, sizeof(response)); + if (!ret && get_unaligned_le32(response + 4)) + ret = -EIO; + dev_info(dcp->dev, "peer unpair result=%d\n", ret); + return ret; +} + +/* + * The primary is being powered off or put to sleep (DPMS, lock screen): + * stop the pair first. On m4.135 the lock screen's DPMS-off ran the + * single-pipe power-off against a merged pair: set_power_state(0) timed + * out, the DPTX HPD deassert failed and the firmware looped until reboot. + * Returns 1 after a paired stop, 0 for an unpaired pipe, or an error. + */ +int dcp_peer_stop_26_6(struct apple_dcp *dcp) +{ + struct apple_dcp *peer; + int ret; + + scoped_guard(mutex, &dcp_peer_lock) { + peer = dcp->peer_reserved; + if (!peer || peer->peer_owner != dcp || + (!dcp->peer_tiling.vftg && !dcp->peer_tiling.member && + !dcp->peer_active)) + return 0; + dcp->peer_active = false; + } + ret = dcp_peer_quiesce(dcp, peer); + dev_info(dcp->dev, "peer stop before power-off: %d\n", ret); + return ret ? ret : 1; +} + +/* ---- DSC copy from primary to peer ---- */ + +static int dcp_peer_copy_dsc(struct apple_dcp *primary, struct apple_dcp *peer) +{ + u8 caps_req[36] = {}, caps[32], bpp_req[4] = {}, bpp[8]; + u8 hints_req[12] = {}, hints[12]; + u32 required; + int ret; + + ret = dcp_peer_call_oob(primary, "A369", NULL, 0, &required, 4); + dev_dbg(primary->dev, "peer DSC required: ret=%d value=%#x\n", ret, required); + if (ret || !(required & 1)) + return ret; + ret = dcp_peer_call_oob(primary, "A367", caps_req, 36, caps, 32); + if (ret) + return ret; + memcpy(caps_req, caps, 32); + ret = dcp_peer_call(peer, "A368", caps_req, 36, caps, 32); + if (ret) + return ret; + ret = dcp_peer_call_oob(primary, "A370", bpp_req, 4, bpp, 8); + if (ret || get_unaligned_le32(bpp + 2)) + return ret ?: -EIO; + memcpy(bpp_req, bpp, 2); + ret = dcp_peer_call(peer, "A371", bpp_req, 4, bpp, 8); + if (ret || get_unaligned_le32(bpp + 2)) + return ret ?: -EIO; + ret = dcp_peer_call_oob(primary, "A372", hints_req, 12, hints, 12); + if (ret || get_unaligned_le32(hints + 8)) + return ret ?: -EIO; + memcpy(hints_req, hints, 8); + ret = dcp_peer_call(peer, "A373", hints_req, 12, hints, 12); + if (ret || get_unaligned_le32(hints + 8)) + return ret ?: -EIO; + dev_dbg(primary->dev, "peer DSC copied: bpp=%u hints=%*ph\n", + get_unaligned_le16(bpp), 8, hints); + return 0; +} + +/* ---- split timing for the half-width derived mode ---- */ + +static void dcp_peer_split_timing(u8 *timing) +{ + static const u8 half_fields[] = { 4, 8, 12, 16, 20, 28, 68 }; + + for (unsigned int i = 0; i < sizeof(half_fields); i++) { + u8 *field = timing + half_fields[i]; + + put_unaligned_le32(get_unaligned_le32(field) >> 1, field); + } + put_unaligned_le32((s32)get_unaligned_le32(timing + 24) / 2, + timing + 24); +} + +/* ---- A364/A365: get video data and insert merge modes ---- */ + +struct dcp_peer_video_data { + u8 timing[0x50]; + u8 color[0x20]; +} __packed; + +struct dcp_peer_get_video_req { + struct dcp_peer_video_data video; + __le32 timing_id, color_id; + u8 requires_merge, timing_null, color_null, requires_merge_null; +} __packed; + +struct dcp_peer_get_video_resp { + struct dcp_peer_video_data video; + u8 requires_merge, success, padding[2]; +} __packed; + +struct dcp_peer_insert_video_req { + struct dcp_peer_video_data video; + __le32 timing_id, color_id; + u8 timing_null, color_null, padding[2]; +} __packed; + +struct dcp_peer_insert_video_resp { + struct dcp_peer_video_data video; + u8 success, padding[3]; +} __packed; + +static_assert(sizeof(struct dcp_peer_get_video_req) == 0x7c); +static_assert(sizeof(struct dcp_peer_get_video_resp) == 0x74); +static_assert(sizeof(struct dcp_peer_insert_video_req) == 0x7c); +static_assert(sizeof(struct dcp_peer_insert_video_resp) == 0x74); + +/* ---- paired modeset ---- */ + +static int dcp_peer_prepare_vftg(struct apple_dcp *dcp, struct apple_dcp *peer, + const struct dcp_peer_insert_video_req *video) +{ + guard(mutex)(&dcp_peer_lock); + if (dcp->peer_reserved != peer || peer->peer_owner != dcp) + return -ENODEV; + dcp->peer_tiling.vftg = peer->peer_tiling.vftg = true; + dcp->peer_tiling.modeset_complete = false; + peer->peer_tiling.modeset_complete = false; + dcp->peer_tiling.primary = true; + peer->peer_tiling.primary = false; + dcp->peer_tiling.width = peer->peer_tiling.width = + get_unaligned_le32(video->video.timing + 8); + dcp->peer_tiling.height = peer->peer_tiling.height = + get_unaligned_le32(video->video.timing + 0x28); + dcp->peer_tiling.x = dcp->peer_tiling.y = peer->peer_tiling.y = 0; + peer->peer_tiling.x = dcp->peer_tiling.width; + return 0; +} + +/* + * A360/A361: both engines must hand out the same swap ID for a paired + * frame. The primary consumed one more ID than the peer during the paired + * mode set on m4.134 (peer 74, primary 75), so this runs again afterwards. + */ +static int dcp_peer_sync_swap_ids(struct apple_dcp *dcp, struct apple_dcp *peer, + const char *when) +{ + u32 primary_id, peer_id, status; + int ret; + + ret = dcp_peer_call(dcp, "A360", NULL, 0, &primary_id, 4); + if (ret) + return ret; + ret = dcp_peer_call(peer, "A360", NULL, 0, &peer_id, 4); + if (ret) + return ret; + dev_dbg(dcp->dev, "peer master swap IDs %s: primary %u peer %u\n", + when, primary_id, peer_id); + if (primary_id != peer_id) { + status = max(primary_id, peer_id); + ret = dcp_peer_call(primary_id < peer_id ? dcp : peer, + "A361", &status, 4, NULL, 0); + } + return ret; +} + +/* + * Adaptive sync (IOMFBParameter 14, minimum rate in 16.16 Hz, 0 = off) goes + * to both pipes before the paired mode set, as the single-pipe mode set + * sends it before the mode is selected. A pipe keeps the parameter across + * a replug: a peer that last ran a VRR mode alone would otherwise merge + * with VRR on while the primary runs without it, which crashed its firmware. + */ +static int dcp_peer_adaptive_sync(struct apple_dcp *pipe, u32 min_vrr) +{ + struct dcp_set_parameter_dcp param = { + .param = IOMFBPARAM_ADAPTIVE_SYNC, + .value = { min_vrr }, + .count = 3, + }; + u32 status = 0; + int ret; + + ret = dcp_peer_call(pipe, "A442", ¶m, sizeof(param), &status, 4); + dev_dbg(pipe->dev, "peer adaptive sync minRR %#x: ret=%d status=%#x\n", + min_vrr, ret, status); + return ret; +} + +static int dcp_peer_modeset(struct apple_dcp *dcp, struct apple_dcp *peer, + const struct dcp_peer_insert_video_req *video, + bool firmware_requires_merge, u32 min_vrr) +{ + struct dcp_peer_wait *wait; + struct dcp_method_entry method = { .name = "peer_modeset", + .tag = {'A', '4', '1', '3'} }; + /* A413 takes the color mode first, as dcp_set_digital_out_mode_req. */ + __le32 mode[2] = {video->color_id, video->timing_id}; + u32 phase, status; + unsigned long deadline; + int ret; + + { + guard(mutex)(&dcp_peer_lock); + if (dcp->peer_reserved != peer || peer->peer_owner != dcp || + !dcp->peer_tiling.display_id_valid || + !peer->peer_tiling.display_id_valid) + return -ENODEV; + dcp->peer_tiling.member = peer->peer_tiling.member = true; + dcp->peer_tiling.mode_flag = peer->peer_tiling.mode_flag = + !firmware_requires_merge; + } + ret = dcp_peer_power(dcp, peer, true); + if (ret) + return ret; + msleep(100); + + ret = dcp_peer_adaptive_sync(dcp, min_vrr); + if (!ret) + ret = dcp_peer_adaptive_sync(peer, min_vrr); + if (ret) + return ret; + + ret = dcp_peer_sync_swap_ids(dcp, peer, "before A413"); + if (ret) + return ret; + + /* Set phase to 1 on both engines */ + phase = 1; + dcp_peer_tiling_state_26_6(dcp, true, 1, 10, &phase); + dcp_peer_tiling_state_26_6(peer, true, 1, 10, &phase); + + /* A413 on primary (async), then wait for phase >= 2 */ + wait = kzalloc(sizeof(*wait), GFP_KERNEL); + if (!wait) + return -ENOMEM; + init_completion(&wait->done); + kref_init(&wait->refs); + kref_get(&wait->refs); + wait->size = 4; + dcp_push(dcp, false, &method, sizeof(mode), 4, mode, + dcp_peer_wait_done, wait); + + deadline = jiffies + msecs_to_jiffies(8500); + for (;;) { + phase = 0; + if (!dcp_peer_tiling_state_26_6(dcp, false, 1, 10, &phase)) { + ret = -ENODEV; + goto put; + } + if (phase >= 2) + break; + /* + * The 26.6 desktop primary merges the timing and completes its + * A413 without waiting for the peer (no phase2 rendezvous seen + * on m4.132): go on to the peer once it succeeded. + */ + if (completion_done(&wait->done)) { + status = get_unaligned_le32(wait->reply); + dev_dbg(dcp->dev, + "peer primary A413 done before phase2: phase=%u status=%#x\n", + phase, status); + if (status) { + ret = -EIO; + goto put; + } + break; + } + if (time_after_eq(jiffies, deadline)) { + ret = -ETIMEDOUT; + dev_err(dcp->dev, + "peer primary timed out before phase2: phase=%u\n", phase); + goto put; + } + msleep(2); + } + dev_dbg(dcp->dev, "peer primary reached modeset phase%u\n", phase); + + /* Copy DSC parameters from primary to peer */ + ret = dcp_peer_copy_dsc(dcp, peer); + if (ret) + goto put; + + /* A413 on peer */ + ret = dcp_peer_call(peer, "A413", mode, sizeof(mode), &status, 4); + if (ret || status) { + dev_err(dcp->dev, "peer A413 failed: ret=%d status=%#x\n", ret, status); + ret = ret ?: -EIO; + goto put; + } + + /* Wait for primary's A413 to complete */ + if (!wait_for_completion_timeout(&wait->done, msecs_to_jiffies(8500))) + ret = -ETIMEDOUT; + else if (get_unaligned_le32(wait->reply)) + ret = -EIO; + if (!ret) + ret = dcp_peer_sync_swap_ids(dcp, peer, "after the paired modeset"); +put: + kref_put(&wait->refs, dcp_peer_wait_free); + if (!ret) { + guard(mutex)(&dcp_peer_lock); + dcp->peer_tiling.modeset_complete = true; + peer->peer_tiling.modeset_complete = true; + } + dev_dbg(dcp->dev, "peer paired modeset result=%d\n", ret); + return ret; +} + +/* ---- install video: A364+A365+A366+modeset ---- */ + +static int dcp_peer_install_video(struct apple_dcp *dcp, struct apple_dcp *peer, + u32 timing, u32 color, u32 min_vrr) +{ + struct dcp_peer_get_video_req req = {}; + struct dcp_peer_get_video_resp resp; + struct dcp_peer_insert_video_req insert = {}; + struct dcp_peer_insert_video_resp inserted; + __le32 enabled = cpu_to_le32(1); + int ret; + + req.timing_id = cpu_to_le32(timing); + req.color_id = cpu_to_le32(color); + ret = dcp_peer_call(dcp, "A364", &req, sizeof(req), &resp, sizeof(resp)); + if (ret) + return ret; + dev_dbg(dcp->dev, "peer video timing=%u color=%u success=%u merge=%u\n", + timing, color, resp.success, resp.requires_merge); + if (!(resp.success & 1)) + return -ENOENT; + if (!(resp.requires_merge & 1)) { + if (dcp->peer_tiling.vftg || peer->peer_tiling.vftg) { + ret = dcp_peer_unpair(dcp, peer); + if (ret) + return ret; + } + return -EOPNOTSUPP; + } + + /* An HDMI sink plugged later connects the link again (HPD irq). */ + if (peer->dptxport[0].connected && dcp_peer_hdmi_sinkless(peer)) { + scoped_guard(mutex, &peer->hpd_mutex) + if (dptx_service_available(&peer->dptxport[0])) + dptxport_set_hpd(peer->dptxport[0].service, false); + ret = dcp_dptx_disconnect(peer, 0); + dev_dbg(dcp->dev, "peer HDMI link released: %d\n", ret); + if (ret) + return ret; + } + + ret = dcp_peer_quiesce(dcp, peer); + if (ret) + return ret; + + insert.video = resp.video; + dcp_peer_split_timing(insert.video.timing); + insert.timing_id = cpu_to_le32(timing + 10000); + insert.color_id = cpu_to_le32(color + 10000); + + ret = dcp_peer_prepare_vftg(dcp, peer, &insert); + if (ret) + return ret; + + /* A366: setup_peer_dcp on the peer */ + ret = dcp_peer_call(peer, "A366", &enabled, sizeof(enabled), NULL, 0); + if (ret) + return ret; + + /* A365: insertMergeModes on primary */ + ret = dcp_peer_call(dcp, "A365", &insert, sizeof(insert), + &inserted, sizeof(inserted)); + if (!ret && !(inserted.success & 1)) + ret = -EIO; + if (ret) + return ret; + + /* A365: insertMergeModes on peer with primary's output */ + insert.video = inserted.video; + ret = dcp_peer_call(peer, "A365", &insert, sizeof(insert), + &inserted, sizeof(inserted)); + if (!ret && !(inserted.success & 1)) + ret = -EIO; + if (ret) + return ret; + + dev_dbg(dcp->dev, "peer derived mode timing=%u color=%u width=%u height=%u\n", + timing + 10000, color + 10000, + get_unaligned_le32(insert.video.timing + 8), + get_unaligned_le32(insert.video.timing + 0x28)); + + /* Paired modeset: A473+A360/A361+A413 on both */ + return dcp_peer_modeset(dcp, peer, &insert, resp.requires_merge & 1, + min_vrr); +} + +/* ---- swaps on the peer pipe ---- */ + +static_assert(sizeof(struct dcp_swap_submit_req_v26_6_desktop) == DCP_V26_SWAP_SIZE); + +static const struct dcp_method_entry dcp_peer_swap_start_method = { + .name = "peer_swap_start", .tag = { 'A', '4', '0', '6' }, +}; + +static const struct dcp_method_entry dcp_peer_swap_submit_method = { + .name = "peer_swap_submit", .tag = { 'A', '4', '0', '7' }, +}; + +static const struct dcp_method_entry dcp_peer_set_master_method = { + .name = "peer_set_master_swap_id", .tag = { 'A', '3', '6', '1' }, +}; + +/* + * One swap on a pipe: swap_start, then swap_submit of @wire. The desktop + * 26.6 firmware does not use the IOMFB transaction queue (nothing advances + * it there: m4.134 timed every peer swap out after 10 s), so the two calls + * are chained directly. The firmware's swap ID must equal @swap_id unless + * that is U32_MAX; on a mismatch the peer's master swap ID is set to it + * once and the swap is started again. @owner is the primary of a paired + * DRM frame. + */ +struct dcp_peer_swap { + struct apple_dcp *owner; + u32 swap_id; + __le32 master; + bool retried; + struct dcp_swap_submit_req_v26_6_desktop wire; +}; + +static void dcp_peer_frame_failed(struct apple_dcp *owner) +{ + guard(mutex)(&dcp_peer_lock); + owner->peer_failed = true; +} + +static void dcp_peer_swap_finish(struct apple_dcp *dcp, struct dcp_peer_swap *ps, + int result, u32 swap_id) +{ + if (result) { + dev_err_ratelimited(dcp->dev, "peer swap %u failed: %d\n", swap_id, result); + if (ps->owner) + dcp_peer_frame_failed(ps->owner); + } + kfree(ps); +} + +static void dcp_peer_swap_submitted(struct apple_dcp *dcp, void *out, void *cookie) +{ + struct dcp_peer_swap *ps = cookie; + struct dcp_swap_submit_resp_v26_6_desktop *resp = out; + + dcp_peer_swap_finish(dcp, ps, resp && !resp->ret ? 0 : -EIO, + ps->wire.swap.swap_id); +} + +static void dcp_peer_swap_begin(struct apple_dcp *dcp, struct dcp_peer_swap *ps); + +static void dcp_peer_swap_resynced(struct apple_dcp *dcp, void *out, void *cookie) +{ + struct dcp_peer_swap *ps = cookie; + + dcp_peer_swap_begin(dcp, ps); +} + +static void dcp_peer_swap_started(struct apple_dcp *dcp, void *out, void *cookie) +{ + struct dcp_peer_swap *ps = cookie; + struct dcp_swap_start_resp_v26_6_desktop *resp = out; + + if (!resp || resp->ret) { + dcp_peer_swap_finish(dcp, ps, -EIO, resp ? resp->swap_id : 0); + return; + } + if (ps->swap_id != U32_MAX && resp->swap_id != ps->swap_id) { + if (!ps->retried && !READ_ONCE(dcp->crashed)) { + dev_info_ratelimited(dcp->dev, + "peer swap ID %u, primary %u: setting the master ID\n", + resp->swap_id, ps->swap_id); + ps->retried = true; + ps->master = cpu_to_le32(ps->swap_id); + dcp_push(dcp, false, &dcp_peer_set_master_method, + sizeof(ps->master), 0, &ps->master, + dcp_peer_swap_resynced, ps); + return; + } + dev_err(dcp->dev, "peer swap ID %u, primary %u\n", + resp->swap_id, ps->swap_id); + dcp_peer_swap_finish(dcp, ps, -EPROTO, resp->swap_id); + return; + } + ps->wire.swap.swap_id = resp->swap_id; + dcp_push(dcp, false, &dcp_peer_swap_submit_method, sizeof(ps->wire), + sizeof(struct dcp_swap_submit_resp_v26_6_desktop), &ps->wire, + dcp_peer_swap_submitted, ps); +} + +static void dcp_peer_swap_begin(struct apple_dcp *dcp, struct dcp_peer_swap *ps) +{ + struct dcp_swap_start_req_v26_6_desktop req = { + .client = dcp->desktop->client, + .padding = { 0xaa, 0xaa, 0xaa }, + }; + + dcp_push(dcp, false, &dcp_peer_swap_start_method, sizeof(req), + sizeof(struct dcp_swap_start_resp_v26_6_desktop), &req, + dcp_peer_swap_started, ps); +} + +/* Takes over @ps unless it returns an error. */ +static int dcp_peer_queue_swap(struct apple_dcp *dcp, struct dcp_peer_swap *ps) +{ + if (READ_ONCE(dcp->crashed) || !dcp->active || !dcp->desktop) { + kfree(ps); + return -EIO; + } + dcp_peer_swap_begin(dcp, ps); + return 0; +} + +/* + * The part of a layer inside the tile [@tile_x, @tile_x + @tile_w), in the + * tile's coordinates. The shared source boundary is rounded once, so the + * halves neither lose nor repeat a source column. + */ +static bool dcp_peer_clip(const struct dcp_rect *src, const struct dcp_rect *dst, + u32 tile_x, u32 tile_w, + struct dcp_rect *tile_src, struct dcp_rect *tile_dst) +{ + u64 left = max_t(u64, dst->x, tile_x); + u64 right = min_t(u64, (u64)dst->x + dst->w, (u64)tile_x + tile_w); + u64 first, last; + + if (!src->w || !src->h || !dst->w || !dst->h || left >= right) + return false; + first = div_u64((left - dst->x) * src->w + dst->w / 2, dst->w); + last = div_u64((right - dst->x) * src->w + dst->w / 2, dst->w); + if (first >= last || (u64)src->x + last > U32_MAX) + return false; + *tile_src = *src; + tile_src->x += first; + tile_src->w = last - first; + *tile_dst = *dst; + tile_dst->x = left - tile_x; + tile_dst->w = right - left; + return true; +} + +/* Both pipes scan the same buffers: the display DARTs share one domain. */ +static void dcp_peer_split_swap(struct dcp_swap_submit_req_v26_6_desktop *left, + struct dcp_swap_submit_req_v26_6_desktop *right, + u32 tile_w) +{ + *right = *left; + for (unsigned int i = 0; i < SWAP_SURFACES; i++) { + struct dcp_swap_submit_req_v26_6_desktop *halves[] = { left, right }; + struct dcp_rect src = left->swap.src_rect[i]; + struct dcp_rect dst = left->swap.dst_rect[i]; + bool present = !left->surf_null[i]; + + for (unsigned int side = 0; side < 2; side++) { + struct dcp_swap_submit_req_v26_6_desktop *req = halves[side]; + struct dcp_rect tile_src, tile_dst; + + if (present && dcp_peer_clip(&src, &dst, side * tile_w, tile_w, + &tile_src, &tile_dst)) { + req->swap.src_rect[i] = tile_src; + req->swap.dst_rect[i] = tile_dst; + } else { + req->surf_null[i] = true; + req->surf_iova[i] = 0; + memset(&req->swap.src_rect[i], 0, sizeof(struct dcp_rect)); + memset(&req->swap.dst_rect[i], 0, sizeof(struct dcp_rect)); + } + } + } +} + +/* + * Paired DRM frame: the primary's swap_start returned @swap_id for @req. + * @req keeps the left tile; the right tile goes to the peer as its own swap. + */ +void dcp_peer_swap_submit_26_6(struct apple_dcp *dcp, + struct dcp_swap_submit_req_v26_6_desktop *req, + u32 swap_id) +{ + struct dcp_peer_swap *ps; + struct apple_dcp *peer; + u32 tile_w; + + if (!READ_ONCE(dcp->peer_active)) + return; + scoped_guard(mutex, &dcp_peer_lock) { + peer = dcp->peer_reserved; + if (!dcp->peer_active || dcp->peer_failed || !peer || + peer->peer_owner != dcp || peer->crashed || peer->peer_unbinding || + !dcp->peer_tiling.modeset_complete || + !peer->peer_tiling.modeset_complete || !dcp->peer_tiling.width) + return; + tile_w = dcp->peer_tiling.width; + } + ps = kzalloc(sizeof(*ps), GFP_KERNEL); + if (!ps) { + dcp_peer_frame_failed(dcp); + return; + } + ps->owner = dcp; + ps->swap_id = swap_id; + dcp_peer_split_swap(req, &ps->wire, tile_w); + if (dcp_peer_queue_swap(peer, ps)) { + dev_err_ratelimited(dcp->dev, "peer swap %u not queued\n", swap_id); + dcp_peer_frame_failed(dcp); + } +} + +/* + * A swap completed on a reserved peer. Its own CRTC is idle; the frame is + * the primary's, which reports it from its own completion. + */ +bool dcp_peer_swap_complete_26_6(struct apple_dcp *dcp, u32 swap_id) +{ + if (!READ_ONCE(dcp->peer_owner)) + return false; + WRITE_ONCE(dcp->last_swap_id, swap_id); + WRITE_ONCE(dcp->have_swap_complete, true); + if (!dcp->peer_complete_logged) { + dcp->peer_complete_logged = true; + dev_info(dcp->dev, "peer pipe completed swap %u\n", swap_id); + } + return true; +} + +/* + * DRM mode set on a primary with a reserved peer. A mode wider than one + * pipe is set on both pipes through the merge path; -EOPNOTSUPP leaves the + * mode to the single-pipe path, also after a failed paired mode set. + */ +int dcp_peer_drm_modeset_26_6(struct apple_dcp *dcp, u32 hdisplay, + u32 timing, u32 color, u32 min_vrr) +{ + struct apple_dcp *peer; + bool restart = false; + int ret; + + scoped_guard(mutex, &dcp_peer_lock) { + peer = dcp->peer_reserved; + if (!peer || peer->peer_owner != dcp) + return -EOPNOTSUPP; + dcp->peer_active = false; + } + if (hdisplay <= DCP_DESKTOP_PIPE_MAX_WIDTH || timing > U32_MAX - 10000 || + color > U32_MAX - 10000) { + if (dcp->peer_tiling.vftg || peer->peer_tiling.vftg) + dcp_peer_unpair(dcp, peer); + return -EOPNOTSUPP; + } + /* + * DRM sets the same mode again right after the pair came up. The + * merged pipe no longer resolves the original timing in A364, so a + * second install would fail and tear the pair down: keep it. A + * changed adaptive-sync rate (VRR switched on or off) is applied with a + * mode set, as on one pipe: the pair is stopped and installed again. + */ + scoped_guard(mutex, &dcp_peer_lock) { + if (dcp->peer_tiling.modeset_complete && + peer->peer_tiling.modeset_complete && !dcp->peer_failed && + dcp->peer_mode_timing == timing && dcp->peer_mode_color == color) { + if (dcp->peer_min_vrr == min_vrr) { + dcp->peer_active = true; + dev_info(dcp->dev, + "paired DRM modeset %u px timing %u color %u: kept\n", + hdisplay, timing, color); + return 0; + } + restart = true; + } + } + if (restart) { + ret = dcp_peer_quiesce(dcp, peer); + dev_info(dcp->dev, "paired DRM modeset: adaptive sync %#x -> %#x, pair stopped: %d\n", + dcp->peer_min_vrr, min_vrr, ret); + if (ret) + return -EOPNOTSUPP; + } + ret = dcp_peer_install_video(dcp, peer, timing, color, min_vrr); + dev_info(dcp->dev, "paired DRM modeset %u px timing %u color %u adaptive sync %#x: %d\n", + hdisplay, timing, color, min_vrr, ret); + if (ret) { + if (dcp->peer_tiling.vftg || peer->peer_tiling.vftg) + dcp_peer_unpair(dcp, peer); + return -EOPNOTSUPP; + } + scoped_guard(mutex, &dcp_peer_lock) { + dcp->peer_active = true; + dcp->peer_failed = false; + dcp->peer_mode_timing = timing; + dcp->peer_mode_color = color; + dcp->peer_min_vrr = min_vrr; + peer->peer_complete_logged = false; + } + return 0; +} diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 82b33106d5012..447d882f933f4 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -60,6 +60,7 @@ #include "dcp-external-bw.h" #include "dptx-service.h" #include "iomfb.h" +#include "iomfb_internal.h" #include "iomfb_m4.h" #include "ibootep.h" #include "parser.h" @@ -230,6 +231,9 @@ bool dcp_t6041_ext_enabled(void) return t6041_dcpext; } +#include "iomfb_v26_6_swap.h" +#include "dcp-peer-v26_6.inc" + /* * The backlight maximum of a desktop 26.x panel whose node has no * apple,max-brightness: the J616s panel's user-accessible SDR maximum (ADT @@ -4869,6 +4873,7 @@ VISIBLE_IF_KUNIT void dcp_comp_unbind(struct device *dev, struct device *main, v } disable_work_sync(&dcp->dimensions_wq); + dcp_peer_forget(dcp); if (dcp->fw_compat == DCP_FIRMWARE_V_26_6) { iomfb_v26_6_unbind(dcp); return; @@ -5238,6 +5243,7 @@ static int dcp_platform_probe_pipe(struct platform_device *pdev) dcp->fixed_connector_type = dcp_connector_type_from_dt(dev->of_node); dcp->connector_type = dcp->fixed_connector_type; of_property_read_u32(dev->of_node, "apple,dcp-index", &dcp->index); + of_property_read_u32(dev->of_node, "apple,dcp-endpoint", &dcp->peer_endpoint); of_property_read_u32(dev->of_node, "apple,dptx-phy", &dcp->dptx_phy); of_property_read_u32(dev->of_node, "apple,dptx-die", &dcp->dptx_die); dcp->fixed_dptx_phy = dcp->dptx_phy; diff --git a/drivers/gpu/drm/apple/dcp.h b/drivers/gpu/drm/apple/dcp.h index 30b728820e2c7..f6f1c4c5282be 100644 --- a/drivers/gpu/drm/apple/dcp.h +++ b/drivers/gpu/drm/apple/dcp.h @@ -143,4 +143,21 @@ unsigned int apple_j613_25g83_clock_hz(void); /* A J613, or a J615 whose boot loader carries the experimental opt-in. */ bool apple_t8122_25g83_board(void); +/* Desktop 26.6 dual-pipe peer (dcp-peer-v26_6.inc, included from dcp.c) */ +struct dptx_port; +int dcp_peer_hints_26_6(struct apple_dcp *dcp, struct dptx_port *port, + const void *data, size_t size, + void *reply, size_t reply_size); +bool dcp_peer_tiling_state_26_6(struct apple_dcp *dcp, bool write, + u32 event, u32 param, u32 *value); +void dcp_peer_display_ids(struct apple_dcp *dcp, u32 own, u32 related); +struct dcp_swap_submit_req_v26_6_desktop; +int dcp_peer_drm_modeset_26_6(struct apple_dcp *dcp, u32 hdisplay, + u32 timing, u32 color, u32 min_vrr); +void dcp_peer_swap_submit_26_6(struct apple_dcp *dcp, + struct dcp_swap_submit_req_v26_6_desktop *req, + u32 swap_id); +bool dcp_peer_swap_complete_26_6(struct apple_dcp *dcp, u32 swap_id); +int dcp_peer_stop_26_6(struct apple_dcp *dcp); + #endif diff --git a/drivers/gpu/drm/apple/dptxep.c b/drivers/gpu/drm/apple/dptxep.c index a03efb93bdc91..66196c8c5cc61 100644 --- a/drivers/gpu/drm/apple/dptxep.c +++ b/drivers/gpu/drm/apple/dptxep.c @@ -694,6 +694,27 @@ static int dptxport_call_set_tiled_display_hint(struct apple_epic_service *servi return 0; } +/* + * Desktop 26.6 display allocation: the 160-byte SET_TILED_DISPLAY_HINTS + * record carries the sink's size and pixel rates and takes the main and + * peer endpoint IDs back (dcp_peer_hints_26_6()). A sink wider than one + * pipe gets the idle pipe above this one as its peer. + */ +static bool dptxport_dual_pipe_alloc(struct apple_epic_service *service, + const void *data, size_t data_size, + void *reply, size_t reply_size) +{ + struct apple_dcp *dcp = service->ep->dcp; + struct dptx_port *dptx = service->cookie; + + if (data_size != 160 || reply_size < 160 || + dcp->fw_compat != DCP_FIRMWARE_V_26_6_DESKTOP) + return false; + + return dcp_peer_hints_26_6(dcp, dptx, data, data_size, + reply, reply_size) == 0; +} + static int dptxport_call_activate(struct apple_epic_service *service, const void *data, size_t data_size, @@ -879,6 +900,9 @@ static int dptxport_call(struct apple_epic_service *service, u32 idx, case DPTX_APCALL_SET_TILED_DISPLAY_HINTS: { int ret; + if (dptxport_dual_pipe_alloc(service, data, data_size, reply, + reply_size)) + return 0; memcpy(reply, data, min(reply_size, data_size)); ret = dptxport_call_set_tiled_display_hint(service, data, data_size, reply, diff --git a/drivers/gpu/drm/apple/iomfb_template.c b/drivers/gpu/drm/apple/iomfb_template.c index 013e0073afeff..59f5c1bd622cb 100644 --- a/drivers/gpu/drm/apple/iomfb_template.c +++ b/drivers/gpu/drm/apple/iomfb_template.c @@ -425,6 +425,10 @@ static void dcpep_cb_swap_complete(struct apple_dcp *dcp, ktime_t now = ktime_get(); bool retire = true; +#if DCP_FW_DESKTOP_26(DCP_FW_VER) + if (dcp_peer_swap_complete_26_6(dcp, resp->swap_id)) + return; +#endif #if DCP_FW_H17(DCP_FW_VER) if (dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17G && !dcp_present_complete_h17p(&dcp->present_state_h17p, resp->swap_id)) { @@ -461,6 +465,7 @@ static void dcpep_cb_swap_complete(struct apple_dcp *dcp, #endif trace_iomfb_swap_complete(dcp, resp->swap_id); dcp->last_swap_id = resp->swap_id; + dcp->have_swap_complete = true; dcp_swap_watchdog_complete(dcp); if (retire) { dcp_release_retained_framebuffers(dcp, resp->swap_id); @@ -2141,6 +2146,15 @@ void DCP_FW_NAME(iomfb_poweroff)(struct apple_dcp *dcp) return; #endif +#if DCP_FW_DESKTOP_26(DCP_FW_VER) + /* A merged pair stops as a pair; its clear swap would not complete. */ + ret = dcp_peer_stop_26_6(dcp); + if (ret < 0) + dev_err(dcp->dev, "paired power-off failed: %d\n", ret); + if (ret > 0) + goto peer_stopped; +#endif + cookie = kzalloc(sizeof(*cookie), GFP_KERNEL); if (!cookie) return; @@ -2177,6 +2191,9 @@ void DCP_FW_NAME(iomfb_poweroff)(struct apple_dcp *dcp) dev_dbg(dcp->dev, "%s: clear swap submitted: %u\n", __func__, swap_id); +#if DCP_FW_DESKTOP_26(DCP_FW_VER) +peer_stopped: +#endif poff_cookie = kzalloc(sizeof(*poff_cookie), GFP_KERNEL); if (!poff_cookie) return; @@ -2269,6 +2286,12 @@ void DCP_FW_NAME(iomfb_sleep)(struct apple_dcp *dcp) return; #endif +#if DCP_FW_DESKTOP_26(DCP_FW_VER) + ret = dcp_peer_stop_26_6(dcp); + if (ret < 0) + dev_err(dcp->dev, "paired sleep failed: %d\n", ret); +#endif + cookie = kzalloc(sizeof(*cookie), GFP_KERNEL); if (!cookie) return; @@ -2385,6 +2408,24 @@ dcpep_cb_hotplug_v26_6(struct apple_dcp *dcp, struct dcp_hotplug_req_v26_6 *req) { struct dcp_hotplug_resp_v26_6 resp = { 0 }; u64 connected = req->connected; + const u8 *tiled = req->opaque; + bool tiled_valid = !(req->opaque[sizeof(resp.opaque)] & 1); + bool paired = READ_ONCE(dcp->peer_reserved) || READ_ONCE(dcp->peer_owner); + + /* + * With a peer reserved, the record names this pipe's and the related + * pipe's display at 0x3b and 0x47, as on firmware 27; it is echoed + * back as that firmware's client does. + */ + if (paired) { + dev_dbg(dcp->dev, "peer HPD connected=%llu null=%u rec=%*ph\n", + connected, req->opaque[sizeof(resp.opaque)], + (int)sizeof(resp.opaque), tiled); + if (tiled_valid && connected && + !(get_unaligned_le32(tiled + 0xc) & BIT(3))) + dcp_peer_display_ids(dcp, get_unaligned_le32(tiled + 0x3b), + get_unaligned_le32(tiled + 0x47)); + } /* A display that went away is bootstrapped again on its next mode. */ if (!dcp->main_display && @@ -2397,7 +2438,9 @@ dcpep_cb_hotplug_v26_6(struct apple_dcp *dcp, struct dcp_hotplug_req_v26_6 *req) dcpep_cb_hotplug(dcp, &connected); - if (connected) + if (paired && tiled_valid) + memcpy(resp.opaque, tiled, sizeof(resp.opaque)); + else if (connected) memset(&resp.opaque[0x47], 0xff, sizeof(u32)); return resp; @@ -2455,12 +2498,42 @@ static struct dcpep_get_tiling_state_resp dcpep_cb_get_tiling_state(struct apple_dcp *dcp, struct dcpep_get_tiling_state_req *req) { +#if DCP_FW_DESKTOP_26(DCP_FW_VER) + /* D115 reads the tiling state of a paired pipe; D116 writes it. */ + u32 value = req->value; + bool handled = dcp_peer_tiling_state_26_6(dcp, false, req->event, + req->param, &value); + + dev_dbg(dcp->dev, "peer tiling get event %u param %u value %u handled %d\n", + req->event, req->param, value, handled); + if (handled) + return (struct dcpep_get_tiling_state_resp){ + .value = value, + .ret = 1, + }; +#endif return (struct dcpep_get_tiling_state_resp){ .value = 0, .ret = 1, }; } +#if DCP_FW_DESKTOP_26(DCP_FW_VER) +/* D116 set_tiling_state: the D115 record in, a boolean (padded to 4) out. */ +static u32 dcpep_cb_set_tiling_state_v26_6(struct apple_dcp *dcp, + struct dcpep_get_tiling_state_req *req) +{ + u32 value = req->value; + bool handled = !req->value_null && + dcp_peer_tiling_state_26_6(dcp, true, req->event, + req->param, &value); + + dev_dbg(dcp->dev, "peer tiling set event %u param %u value %u null %u handled %d\n", + req->event, req->param, req->value, req->value_null, handled); + return handled; +} +#endif + static u8 dcpep_cb_create_backlight_service(struct apple_dcp *dcp) { #if DCP_FW_H17(DCP_FW_VER) @@ -2548,6 +2621,10 @@ TRAMPOLINE_IN(trampoline_pr_publish, iomfb_cb_pr_publish, struct iomfb_property); TRAMPOLINE_INOUT(trampoline_get_tiling_state, dcpep_cb_get_tiling_state, struct dcpep_get_tiling_state_req, struct dcpep_get_tiling_state_resp); +#if DCP_FW_DESKTOP_26(DCP_FW_VER) +TRAMPOLINE_INOUT(trampoline_set_tiling_state_v26_6, dcpep_cb_set_tiling_state_v26_6, + struct dcpep_get_tiling_state_req, u32); +#endif TRAMPOLINE_OUT(trampoline_create_backlight_service, dcpep_cb_create_backlight_service, u8); /* @@ -2603,6 +2680,9 @@ static void dcp_swap_started(struct apple_dcp *dcp, void *data, void *cookie) trace_iomfb_swap_submit(dcp, resp->swap_id); dcp_h17p_prepare_swap(dcp); +#if DCP_FW_DESKTOP_26(DCP_FW_VER) + dcp_peer_swap_submit_26_6(dcp, &DCP_FW_SWAP(dcp), resp->swap_id); +#endif dcp_swap_submit(dcp, false, &DCP_FW_SWAP(dcp), dcp_swapped, NULL); } @@ -2930,6 +3010,25 @@ int DCP_FW_NAME(iomfb_modeset)(struct apple_dcp *dcp, /* VRR modes need swap timestamps, see iomfb_flush(). */ dcp->use_timestamps = mode->vrr; + /* + * A mode wider than one pipe goes to both pipes of a reserved pair, + * with adaptive sync set the same on both. + */ + if (!dcp->main_display) { + dev_dbg(dcp->dev, "modeset %ux%u vrr %u min %#x enabled %u\n", + crtc_state->mode.hdisplay, crtc_state->mode.vdisplay, + mode->vrr, mode->min_vrr, crtc_state->vrr_enabled); + ret = dcp_peer_drm_modeset_26_6(dcp, crtc_state->mode.hdisplay, + mode->timing_mode_id, + mode->color_mode_id, + mode->vrr && crtc_state->vrr_enabled ? + mode->min_vrr : 0); + if (ret != -EOPNOTSUPP) { + dcp->desktop->mode_live = !ret; + return ret; + } + } + /* * The firmware changes a fixed panel's mode through the swap geometry, * not through get_mode_info and set_digital_out_mode. Accept the diff --git a/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c b/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c index 26b444b3ac6c0..b5e71329eb9ce 100644 --- a/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c +++ b/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c @@ -153,6 +153,7 @@ static const iomfb_cb_handler cb_handlers[IOMFB_MAX_CB] = { [114] = trampoline_passthrough_dict_v26_6, /* set_tiling_state: the value followed by success */ [115] = trampoline_get_tiling_state, + [116] = trampoline_set_tiling_state_v26_6, [121] = dcpep_cb_boot_1, /* start_hardware_boot */ [122] = trampoline_false, /* is_dark_boot / is_waking_from_hibernate */ [123] = trampoline_false, @@ -234,6 +235,7 @@ static const struct dcp_cb_min_v26_6_desktop cb_min[IOMFB_MAX_CB] = { sizeof(struct dcp_dict_passthrough_resp_v26_6)), [115] = CB_MIN(sizeof(struct dcpep_get_tiling_state_req), sizeof(struct dcpep_get_tiling_state_resp)), + [116] = CB_MIN(sizeof(struct dcpep_get_tiling_state_req), sizeof(u32)), [121] = CB_MIN(0, 0), [122] = CB_MIN(0, sizeof(u8)), [123] = CB_MIN(0, sizeof(u8)), From 50ba71c552f1f57ced1ccc9e2805e35972ea189f Mon Sep 17 00:00:00 2001 From: fbalazsy Date: Tue, 27 Oct 2026 10:03:52 +0100 Subject: [PATCH 314/388] drm/apple: fabric: let a Type-C port take the hybrid pipeline first The hybrid pipeline (a fixed output plus Type-C routes, dcpext0 on the J616s) scores last for a Type-C port, kept for the fixed display. On the J616s it is also the only pipe that can be the primary of a two-pipe display, so a 7680 px sink on USB-C can only be driven through it. appledrm.typec_hybrid_first drops that penalty: a Type-C port is planned onto the hybrid pipeline first and dcpext1 stays free as its peer. The HDMI display then has no pipe while the Type-C display is up; the default keeps today's order. --- drivers/gpu/drm/apple/dcp-fabric-core.c | 4 ++-- drivers/gpu/drm/apple/dcp-fabric-core.h | 2 ++ drivers/gpu/drm/apple/dcp-fabric.c | 20 ++++++++++++++++++-- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp-fabric-core.c b/drivers/gpu/drm/apple/dcp-fabric-core.c index 8c952dc024560..f703ea061fb50 100644 --- a/drivers/gpu/drm/apple/dcp-fabric-core.c +++ b/drivers/gpu/drm/apple/dcp-fabric-core.c @@ -136,8 +136,8 @@ unsigned int dcp_fabric_score(const struct dcp_fabric_pipeline *pipeline, * route would only follow it there. */ return pipeline->crtc_index + - (!policy->dual_stream && !policy->follow && pipeline->has_fixed ? - 100 : 0); + (!policy->dual_stream && !policy->follow && !policy->hybrid_first && + pipeline->has_fixed ? 100 : 0); } EXPORT_SYMBOL_GPL(dcp_fabric_score); diff --git a/drivers/gpu/drm/apple/dcp-fabric-core.h b/drivers/gpu/drm/apple/dcp-fabric-core.h index 1b8c59ac15a54..dc85dbc40c15b 100644 --- a/drivers/gpu/drm/apple/dcp-fabric-core.h +++ b/drivers/gpu/drm/apple/dcp-fabric-core.h @@ -64,6 +64,8 @@ struct dcp_fabric_policy { bool dual_stream; /* routes follow the CRTC a modeset pairs them with */ bool follow; + /* a hybrid pipeline is not kept back for a fixed display */ + bool hybrid_first; }; enum dcp_fabric_wiring { diff --git a/drivers/gpu/drm/apple/dcp-fabric.c b/drivers/gpu/drm/apple/dcp-fabric.c index 09b5ec9652a0c..b49e277cd840a 100644 --- a/drivers/gpu/drm/apple/dcp-fabric.c +++ b/drivers/gpu/drm/apple/dcp-fabric.c @@ -38,6 +38,18 @@ MODULE_PARM_DESC(tiled_split, static bool typec_follow_crtc = true; module_param(typec_follow_crtc, bool, 0444); + +/* + * The hybrid pipeline (dcpext0: fixed HDMI PHY + Type-C routes) normally + * comes last for a Type-C port, kept for an HDMI display. On the J616s it + * is also the only pipe that can be the primary of a two-pipe display + * (firmware endpoint 1; the pipes merge upwards only): with this set a + * Type-C port takes it first, so a 7680 px sink on USB-C can pair with + * dcpext1 as its peer. Read when a port is planned (replug to apply). + */ +static bool typec_hybrid_first; +module_param(typec_hybrid_first, bool, 0644); +MODULE_PARM_DESC(typec_hybrid_first, "Type-C ports take the hybrid (HDMI) pipeline first (dual-pipe primary)"); MODULE_PARM_DESC(typec_follow_crtc, "Let direct DP-alt routes follow their selected CRTC (default: on)"); @@ -193,7 +205,8 @@ static bool dcp_typec_route_available(struct apple_dcp_typec_route *route) .fixed_busy = dcp_typec_route_busy(route), }; - struct dcp_fabric_policy policy = { .dual_stream = dcp_typec_ordered_dual_stream() }; + struct dcp_fabric_policy policy = { .dual_stream = dcp_typec_ordered_dual_stream(), + .hybrid_first = typec_hybrid_first }; pipeline.presence = dcp_hdmi_presence(route->dcp); return dcp_fabric_available(&pipeline, &policy); @@ -1069,6 +1082,7 @@ dcp_typec_free_route(struct apple_dcp_typec_port *port) struct dcp_fabric_policy policy = { .dual_stream = dcp_typec_ordered_dual_stream(), .follow = dcp_typec_port_follows(port), + .hybrid_first = typec_hybrid_first, }; dcp_fabric_snapshot_port(port, false); @@ -2045,6 +2059,7 @@ static int dcp_tb_candidate(void *data) struct dcp_fabric_policy policy = { .dual_stream = dcp_typec_ordered_dual_stream(), .follow = dcp_typec_port_follows(port), + .hybrid_first = typec_hybrid_first, }; struct dcp_fabric_route *chosen; bool connector_present = dpin ? !!port->secondary_connector : !!port->connector; @@ -2776,7 +2791,8 @@ static int dcp_typec_follow_decide(struct apple_dcp *dcp, struct drm_crtc *crtc, struct apple_connector *connector, struct dcp_typec_follow *follow) { - struct dcp_fabric_policy policy = { .dual_stream = dcp_typec_ordered_dual_stream() }; + struct dcp_fabric_policy policy = { .dual_stream = dcp_typec_ordered_dual_stream(), + .hybrid_first = typec_hybrid_first }; struct apple_dcp_typec_route *from, *to, *holder, *back = NULL; struct apple_dcp_typec_port *port, *other = NULL; bool secondary, off = false; From 900841552fa6e5f7637adad4a0c15bbc694f3036 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 23:30:09 +1100 Subject: [PATCH 315/388] dt-bindings: spi: apple: Add the T6041 SPI controller The M4 Pro (T6040/T6041) SPI controllers are register compatible with T8103, like the T6020 and T8140 ones. Assisted-by: LLM Signed-off-by: Ryan Murray (cherry picked from commit fb63f95afe46ad66c35cf75a968ff201cd5de008) --- Documentation/devicetree/bindings/spi/apple,spi.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/Documentation/devicetree/bindings/spi/apple,spi.yaml b/Documentation/devicetree/bindings/spi/apple,spi.yaml index 4b30265686fa1..be47c5f9ff075 100644 --- a/Documentation/devicetree/bindings/spi/apple,spi.yaml +++ b/Documentation/devicetree/bindings/spi/apple,spi.yaml @@ -18,6 +18,7 @@ properties: - items: - enum: - apple,t6020-spi + - apple,t6041-spi - apple,t8140-spi - const: apple,t8103-spi - items: From b1ba68156118760e01f13cf26a2db65814b9800f Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 23:30:09 +1100 Subject: [PATCH 316/388] dt-bindings: input: apple,mesa-fingerprint: Name the signed root key Sensors whose enclave session needs a signed root key (the J700 and J616s part, 0x3356) take it as per-device factory data, like the calibration. Let firmware-name carry it as an optional second entry so the board description names it. Assisted-by: LLM Signed-off-by: Ryan Murray (cherry picked from commit ceca3fd9960240011d2609198a864d3a549d42cc) --- .../bindings/input/apple,mesa-fingerprint.yaml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Documentation/devicetree/bindings/input/apple,mesa-fingerprint.yaml b/Documentation/devicetree/bindings/input/apple,mesa-fingerprint.yaml index 53b304bcec7ff..631ba47a5a3ca 100644 --- a/Documentation/devicetree/bindings/input/apple,mesa-fingerprint.yaml +++ b/Documentation/devicetree/bindings/input/apple,mesa-fingerprint.yaml @@ -57,11 +57,17 @@ properties: description: Rail selector words, in power-on order. firmware-name: - maxItems: 1 + minItems: 1 + items: + - description: + Calibration blob for this sensor. + - description: + Signed root-key container, for sensors whose enclave session needs + one. description: - Calibration blob for this sensor. It is per-device factory data written - at manufacture, so it is neither redistributable nor derivable from the - compatible; the board description has to name it. + Per-device factory data written at manufacture, so neither + redistributable nor derivable from the compatible; the board + description has to name it. spi-cpha: true From cd2cdc5e1ed01fab13c75960ba2dc1a437d038c6 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 23:30:10 +1100 Subject: [PATCH 317/388] soc: apple: sep: Take the sensor root key name from the device tree The signed root-key container is per-device data, but its name was fixed to J700's. Read it from the sensor node's second firmware-name entry and keep J700's name when the board gives only the calibration, so the J700 device tree and its installed file keep working unchanged. Name the file in the error when it cannot be loaded. Assisted-by: LLM Signed-off-by: Ryan Murray (cherry picked from commit 1bcccae53e5bfa901859a4e29f85cdbd9278a47c) --- drivers/soc/apple/sbio.rs | 9 +++++---- drivers/soc/apple/sensor.rs | 15 +++++++++++++-- drivers/soc/apple/sensor_shim.c | 24 ++++++++++++++---------- drivers/soc/apple/sep.rs | 3 +++ drivers/soc/apple/shim.h | 2 +- 5 files changed, 36 insertions(+), 17 deletions(-) diff --git a/drivers/soc/apple/sbio.rs b/drivers/soc/apple/sbio.rs index 88be672817b1c..5a81a370dda38 100644 --- a/drivers/soc/apple/sbio.rs +++ b/drivers/soc/apple/sbio.rs @@ -2183,14 +2183,15 @@ impl SepData { self.sensor_trace(c"SENSOR_NONCE"); if !self.sensor_key_loaded.load(Relaxed) { - let firmware = match kernel::firmware::Firmware::request( - c"apple/msrk-j700.bin", &self.dev, - ) { + // Per-device data, so the board names it; J700 predates that. + let name = sensor::root_key_firmware_name().unwrap_or(ROOT_KEY_FIRMWARE); + let firmware = match kernel::firmware::Firmware::request(name, &self.dev) { Ok(firmware) => firmware, Err(error) => { dev_err!( self.dev, - "sensor: signed root-key container unavailable: {:?}\n", + "sensor: signed root-key container {} unavailable: {:?}\n", + name, error ); return false; diff --git a/drivers/soc/apple/sensor.rs b/drivers/soc/apple/sensor.rs index 63a4ae28fbc6d..13bb470c89b3b 100644 --- a/drivers/soc/apple/sensor.rs +++ b/drivers/soc/apple/sensor.rs @@ -25,15 +25,26 @@ extern "C" { fn sep_sensor_irq_available() -> c_int; fn sep_sensor_irq_arm(); fn sep_sensor_irq_wait(timeout_ms: c_uint) -> c_int; - fn sep_sensor_firmware_name() -> *const c_char; + fn sep_sensor_firmware_name(index: c_uint) -> *const c_char; } /// The calibration blob's name from the board description, or `None` when the /// sensor node has no `firmware-name` property (the caller then uses a default). pub(crate) fn firmware_name() -> Option<&'static CStr> { + firmware_name_at(0) +} + +/// The signed root-key container's name from the board description: the +/// second `firmware-name` entry, or `None` when the board gives only the +/// calibration (the caller then uses a default). +pub(crate) fn root_key_firmware_name() -> Option<&'static CStr> { + firmware_name_at(1) +} + +fn firmware_name_at(index: c_uint) -> Option<&'static CStr> { // SAFETY: the shim returns NULL or a pointer to a NUL-terminated string // owned by the device property and valid for the device's lifetime. - let ptr = unsafe { sep_sensor_firmware_name() }; + let ptr = unsafe { sep_sensor_firmware_name(index) }; if ptr.is_null() { return None; } diff --git a/drivers/soc/apple/sensor_shim.c b/drivers/soc/apple/sensor_shim.c index 68405e0a74507..bed228b4d1ca3 100644 --- a/drivers/soc/apple/sensor_shim.c +++ b/drivers/soc/apple/sensor_shim.c @@ -568,19 +568,23 @@ static int sep_sensor_power_line_locked(void) } /* - * The per-device calibration blob name, as the board description gives it in - * the sensor node's "firmware-name" property. NULL when absent, so the caller - * can fall back to a default. + * A per-device blob name, as the board description gives it in the sensor + * node's "firmware-name" list: index 0 is the calibration, index 1 the signed + * root key. NULL when that entry is absent, so the caller can fall back to a + * default. */ -static const char *sep_sensor_firmware_name_locked(void) +static const char *sep_sensor_firmware_name_locked(unsigned int index) { - const char *name = NULL; + const char *names[2] = { NULL, NULL }; + int count; - if (!sep_spi) + if (!sep_spi || index >= ARRAY_SIZE(names)) return NULL; - if (device_property_read_string(&sep_spi->dev, "firmware-name", &name)) + count = device_property_read_string_array(&sep_spi->dev, "firmware-name", + names, ARRAY_SIZE(names)); + if (count <= (int)index) return NULL; - return name; + return names[index]; } /* Powers the sensor on/off, holding the hardware settling delay. */ @@ -871,14 +875,14 @@ int sep_sensor_power_line(void) return result; } -const char *sep_sensor_firmware_name(void) +const char *sep_sensor_firmware_name(unsigned int index) { struct sep_sensor_lifetime *sensor = sep_lock_sensor(); const char *result; if (!sensor) return NULL; - result = sep_sensor_firmware_name_locked(); + result = sep_sensor_firmware_name_locked(index); mutex_unlock(&sensor->lock); return result; } diff --git a/drivers/soc/apple/sep.rs b/drivers/soc/apple/sep.rs index df71496a564fe..7d9f4f2af1869 100644 --- a/drivers/soc/apple/sep.rs +++ b/drivers/soc/apple/sep.rs @@ -449,6 +449,9 @@ const PATCH_POLL_MS: u32 = 20; const PATCH_POLL_ATTEMPTS: u32 = 250; const CALIBRATION_FIRMWARE: &CStr = c"apple/mesa_calibration.bin"; +/// The signed root-key container on boards whose sensor node names only the +/// calibration (J700). +const ROOT_KEY_FIRMWARE: &CStr = c"apple/msrk-j700.bin"; const SBIO_PROBE_USER_ID: i32 = 1000; static_assert!(SBIO_PROBE_USER_ID >= crate::sks::SKS_DESIGNATE_USER_MIN); diff --git a/drivers/soc/apple/shim.h b/drivers/soc/apple/shim.h index 0425bf2cdaa6e..99d2661e2cb1c 100644 --- a/drivers/soc/apple/shim.h +++ b/drivers/soc/apple/shim.h @@ -132,7 +132,7 @@ int sep_sensor_cs_timing_mode(void); int sep_sensor_power_cycle(void); int sep_sensor_power_source(void); int sep_sensor_power(int on); -const char *sep_sensor_firmware_name(void); +const char *sep_sensor_firmware_name(unsigned int index); int sep_sensor_xfer(const void *tx, void *rx, size_t len); int sep_sensor_xfer_tx(const void *tx, size_t len); int sep_sensor_xfer2(const void *tx, size_t tx_len, void *rx, size_t rx_len); From 0dda06f92f06d5e405692cc6a7ad00823972c181 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 23:30:10 +1100 Subject: [PATCH 318/388] soc: apple: sep: Add the T6040 (J616s) platform profile The J616s ADT (26.6.2) describes a SEP that iBoot boots before the AP OS, an iop-sep,ascwrap-v6 whose DART window starts at the BIT(40) alias, and the Mesa sensor J700 carries (0x3356) on spi2 at 0x429108000, 8 MHz, chip select 0. So this is a warm-registration platform with T8140's authenticated sensor session, and the xART guard on from the first boot because macOS shares the enclave. Only J616s is matched; the other T6040 Macs' sensor wiring is unchecked. Assisted-by: LLM Signed-off-by: Ryan Murray (cherry picked from commit 718202c86b1ff9f11fcc9aa957f017243c2d18b1) --- drivers/soc/apple/profile.rs | 40 ++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/drivers/soc/apple/profile.rs b/drivers/soc/apple/profile.rs index 0ba0eb510122b..0e632c65da81f 100644 --- a/drivers/soc/apple/profile.rs +++ b/drivers/soc/apple/profile.rs @@ -9,7 +9,7 @@ //! here once. The driver reads addresses from the device tree, never scans for //! them, and never patches properties at runtime. //! -//! Five bring-up targets are modelled: +//! These bring-up targets are modelled: //! //! * `T8103` / J313 (MacBook Air, M1): the host boots the SEP with the boot //! endpoint handshake over a 0x30000 shared-memory window. @@ -24,6 +24,8 @@ //! a powered sensor, so the driver uses the warm registration path. //! * `T8122` / J613, J615 (MacBook Air, M3): the T8112 warm registration with //! the T8140 SEP DART aperture above 4 GiB. +//! * `T6040` / J616s (MacBook Pro 16", M4 Pro): iBoot boots sepOS, so the +//! driver uses the warm registration path; the sensor is the J700 part. // The boot handshake, identity source, sensor and DART fields are consumed by // the boot-endpoint, identity and sensor-transport paths. @@ -426,6 +428,28 @@ pub(crate) const T8122: PlatformProfile = PlatformProfile { ..T8112 }; +/// MacBook Pro 16" M4 Pro (J616s). The J616s ADT (26.6.2, 25G83) records a +/// pre-booted SEP (`sepfw-load-at-boot`), an `iop-sep,ascwrap-v6` SEP whose +/// DART aperture starts at the BIT(40) alias (`vm-base` 0x10000004000), and +/// the Mesa sensor J700 carries (`sensor-id` 0x3356) on spi2 at +/// `0x429108000`, 8 MHz, chip select 0. Its power is NUB GPIO 53 and its +/// data-ready line AP GPIO 116. `/defaults` carries `cpx-encryption-mode = 2`. +/// +/// A warm-registration platform like T8140, with T8140's authenticated +/// sensor session: the enclave serves this sensor only after the nonce and +/// the signed root key. Device-tree nodes remain authoritative for addresses. +pub(crate) const T6040: PlatformProfile = PlatformProfile { + name: "T6040/J616s", + sensor: SensorProfile { + controller_base: 0x4_2910_8000, + ..T8140.sensor + }, + // macOS shares this enclave and Linux has no records here yet: keep + // Linux to its own xART records from the first boot. + xart_guard: 1, + ..T8140 +}; + static_assert!(T8103.shmem_capacity == 0x30000); static_assert!(T6020.shmem_capacity == 0x40000); static_assert!(T8140.shmem_capacity == 0x40000); @@ -441,6 +465,8 @@ static_assert!(T8112.sensor.controller_base == T8103.sensor.controller_base); static_assert!(T8122.shmem_capacity == T8112.shmem_capacity); static_assert!(T8122.sensor.controller_base == 0x2a1108000); static_assert!(T8122.wide_dma_mask); +static_assert!(T6040.sensor.controller_base == 0x429108000); +static_assert!(T6040.sensor.expected_id == T8140.sensor.expected_id); /// Whether the machine root declares `compatible`. /// @@ -587,6 +613,11 @@ pub(crate) fn detect() -> Result<&'static PlatformProfile> { if machine_has(b"apple,t8122") && (machine_has(b"apple,j613") || machine_has(b"apple,j615")) { return Ok(&T8122); } + // Only the board whose ADT was read: J616s. The other T6040 Macs share + // the SoC but their sensor wiring is unchecked. + if machine_has(b"apple,t6040") && machine_has(b"apple,j616s") { + return Ok(&T6040); + } Err(ENODEV) } @@ -637,6 +668,11 @@ mod tests { assert!(T8122.wide_dma_mask); assert!(T8122.shmem_capacity == T8112.shmem_capacity); assert!(T8122.sensor.controller_base == 0x2_a110_8000); + assert!(T6040.nonce_session); + assert!(T6040.wide_dma_mask); + assert!(T6040.bootstrap == Bootstrap::WarmRegister); + assert!(T6040.identity == IdentitySource::Chosen); + assert!(T6040.xart_guard == 1); } } @@ -667,7 +703,7 @@ mod dart_window_tests { #[test] fn the_other_profiles_keep_the_bit40_window() { - for profile in [&T8103, &T6000, &T6020, &T8140, &T6020_SEPOS13, &T8122] { + for profile in [&T8103, &T6000, &T6020, &T8140, &T6020_SEPOS13, &T8122, &T6040] { assert_eq!(offset(profile.dart_window), 1 << 40); assert_eq!(range(profile.dart_window), (0, 1 << 32)); } From 20aeee2bd4da085e19ae3af5a90249ed54e144af Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 23:30:10 +1100 Subject: [PATCH 319/388] arm64: dts: apple: t6040: Add the SEP, its DART and mailbox, and spi2 From the J616s ADT: the SEP at 0x406600000 with its mailbox at +0x8000 (AIC 666-669), its DART at 0x404c80000 (AIC 676) with the window iBoot programmed (vm-base 0x10000004000, vm-size 0x3ff00000), and spi2 at 0x429108000 (AIC 1569) with the Mesa sensor child. Add the sep, spi_p and spi2 power states from the ADT pmgr devices. The SEP and its DART stay disabled; apple_sep enables them when it loads. Assisted-by: LLM Signed-off-by: Ryan Murray (cherry picked from commit ccbc6f976c8ee65c2947cd5f6eb9d85413fffd46) --- arch/arm64/boot/dts/apple/t6040-pmgr.dtsi | 28 ++++++++++ arch/arm64/boot/dts/apple/t6040.dtsi | 66 +++++++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/arch/arm64/boot/dts/apple/t6040-pmgr.dtsi b/arch/arm64/boot/dts/apple/t6040-pmgr.dtsi index 8c05fb4d4d5d3..bf357745d581d 100644 --- a/arch/arm64/boot/dts/apple/t6040-pmgr.dtsi +++ b/arch/arm64/boot/dts/apple/t6040-pmgr.dtsi @@ -140,6 +140,24 @@ power-domains = <&ps_sio>; }; + ps_spi_p: power-controller@198 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x198 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "spi_p"; + power-domains = <&ps_sio>; + }; + + ps_spi2: power-controller@1c8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1c8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "spi2"; + power-domains = <&ps_spi_p>; + }; + ps_ans: power-controller@370 { compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; reg = <0x370 4>; @@ -178,6 +196,16 @@ power-domains = <&ps_ans>, <&ps_apcie_st1>; apple,always-on; }; + + ps_sep: power-controller@c00 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0xc00 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "sep"; + power-domains = <&ps_fab3_soc>; + apple,always-on; + }; }; /* diff --git a/arch/arm64/boot/dts/apple/t6040.dtsi b/arch/arm64/boot/dts/apple/t6040.dtsi index 01486e0f5ac65..c054f5accf531 100644 --- a/arch/arm64/boot/dts/apple/t6040.dtsi +++ b/arch/arm64/boot/dts/apple/t6040.dtsi @@ -702,6 +702,48 @@ status = "disabled"; }; + /* + * iBoot hands over a running SEP. Keep Linux off its DART at boot; + * apple_sep enables it when the driver binds. The SEP only accepts + * IOVAs in the window iBoot programmed (ADT vm-base 0x10000004000, + * vm-size 0x3ff00000), which carries the BIT(40) alias the DART + * translates. + */ + sep_dart: iommu@404c80000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x4 0x04c80000 0x0 0x20000>; + interrupt-parent = <&aic>; + interrupts = ; + #iommu-cells = <1>; + apple,dma-range = /bits/ 64 <0x10000004000 0x3ff00000>; + apple,dma-offset = /bits/ 64 <0x0>; + status = "disabled"; + }; + + sep: sep@406600000 { + compatible = "apple,sep"; + reg = <0x4 0x06600000 0x0 0x88000>; + mboxes = <&sep_mbox>; + mbox-names = "mbox"; + iommus = <&sep_dart 0>; + power-domains = <&ps_sep>; + status = "disabled"; + }; + + sep_mbox: mailbox@406608000 { + compatible = "apple,t6041-asc-mailbox", "apple,asc-mailbox-v4"; + reg = <0x4 0x06608000 0x0 0x4000>; + interrupt-parent = <&aic>; + interrupts = , + , + , + ; + interrupt-names = "send-empty", "send-not-empty", + "recv-empty", "recv-not-empty"; + #mbox-cells = <0>; + status = "disabled"; + }; + ans_mbox: mailbox@409608000 { compatible = "apple,t6041-asc-mailbox", "apple,asc-mailbox-v4"; reg = <0x4 0x09608000 0x0 0x4000>; @@ -827,6 +869,30 @@ status = "disabled"; }; + /* + * Touch ID. iBoot leaves the pads muxed and the controller uses its + * native chip select, so there is no pinctrl here. + */ + spi2: spi@429108000 { + compatible = "apple,t6041-spi", "apple,t8103-spi"; + reg = <0x4 0x29108000 0x0 0x4000>; + interrupt-parent = <&aic>; + interrupts = ; + clocks = <&clkref>; + power-domains = <&ps_spi2>; + #address-cells = <1>; + #size-cells = <0>; + status = "disabled"; + + mesa: fingerprint@0 { + compatible = "apple,mesa-fingerprint"; + reg = <0>; + spi-max-frequency = <8000000>; + spi-cpol; + status = "disabled"; + }; + }; + nvme: nvme@44dcc0000 { compatible = "apple,t6041-nvme-ans2"; reg = <0x4 0x4dcc0000 0x0 0x10000>, From 1c85b4d66fdf6fbbe5f3a5fe300127f2b067caa3 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 23:30:10 +1100 Subject: [PATCH 320/388] arm64: dts: apple: t6040-j616s: Enable Touch ID Give the boot loader the sep alias it reserves the SEP firmware through, enable the SEP mailbox and spi2, and describe the sensor: NUB GPIO 53 powers it, AP GPIO 116 is its data-ready line, and its sealed calibration and root key come from this Mac's own iBoot system container. Assisted-by: LLM Signed-off-by: Ryan Murray (cherry picked from commit a4b962a20add7dd09ff167f8cfc7cc4bd53ad2e9) --- arch/arm64/boot/dts/apple/t6040-j616s.dts | 29 +++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/arch/arm64/boot/dts/apple/t6040-j616s.dts b/arch/arm64/boot/dts/apple/t6040-j616s.dts index aa17241c57dd2..6a85dda0804ac 100644 --- a/arch/arm64/boot/dts/apple/t6040-j616s.dts +++ b/arch/arm64/boot/dts/apple/t6040-j616s.dts @@ -29,6 +29,8 @@ disp0-piodma = &disp0_piodma; /* the boot loader finds the GPU node through this alias */ gpu = &gpu; + /* the boot loader reserves the SEP firmware through this alias */ + sep = &sep; }; chosen { @@ -1350,6 +1352,33 @@ status = "okay"; }; +/* apple_sep enables the SEP and its DART when it loads. */ +&sep_mbox { + status = "okay"; +}; + +/* + * Touch ID sensor (Mesa 0x3356). Its power is NUB GPIO 53 and its + * data-ready line is AP GPIO 116, as the J616s ADT describes them. + */ +&spi2 { + status = "okay"; +}; + +&mesa { + status = "okay"; + enable-gpios = <&pinctrl_nub 53 GPIO_ACTIVE_HIGH>; + interrupt-parent = <&pinctrl>; + interrupts = <116 IRQ_TYPE_EDGE_RISING>; + spi-cs-setup-delay-ns = <20>; + spi-cs-hold-delay-ns = <20>; + /* + * Per-device factory data from this Mac's own iBoot system container: + * the sealed calibration and the sealed root key. + */ + firmware-name = "apple/mesacal-j616s.bin", "apple/msrk-j616s.bin"; +}; + /* The Type-C retimers (ADT atcrt at 0x18/0x19/0x1a) sit on this bus; no driver yet. */ &i2c6 { status = "okay"; From 781883b8c9ef47a1dc7b42f6ce5f4bf1cef614d3 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Tue, 27 Oct 2026 01:08:00 -0400 Subject: [PATCH 321/388] drm/asahi: kill G17 contexts left with only executed render work A closed context whose failed compute command was retired on the firmware's fault record has a clean compute side, yet the render work it queued behind the failure ran without storing its stamp and stays linked in the context's work list. Such a pair is released only after a kill of its context, and that kill was only ever published through one of the context's compute queues, of which none is left to carry it. The context, its pairs and its memory therefore stayed until the GPU firmware stopped: 4 contexts after ten minutes of ray query stress beside heavy geometry. Publish the kill for such a context on its render pairs' behalf. A context qualifies once it has been closed for 250 ms, every pair of it is idle or has executed all of its commands, at least one has, and no compute queue still runs it, switches away from it or has published its kill. The kill names one of the context's own render queues that its scheduler page lists and is refused if the page names a queue of another context; the pairs it holds must have a single admission owner. The rules of a failed context's kill apply unchanged: the pairs are held for the kill, their memory is released only once the scheduler page reports it, the kill waits for a moment without other contexts' compute unless GPU memory is pressed, and the held pairs become killed pairs, reused last, once the context's scheduler release is consumed. The teardown entry keeps the kill's cookie, so only that exact cookie acknowledges it, and its doorbell is retried without duplicating the record. debugfs reports render-only kills, their acknowledgements, the contexts refused by first reason and the kills waiting. On Neo, ten minutes of two ray query stress loops beside heavy geometry had 53 faults, 53 kills through compute queues and 4 render-only kills, all acknowledged, with 0 GPU recoveries and nothing left pending afterwards (4 teardowns before). Ten minutes of the five-lane stress load had 48 faults, 8 kills through compute queues and 2 render-only kills, all acknowledged, with 0 GPU recoveries and nothing left pending. No render-only kill was refused in either run. The asahi_g17_teardown tracepoint records render-only kills as kind 7 and their acknowledgements with queue 0x100. Signed-off-by: Ryan Murray (cherry picked from commit 72771fbd90f04f17fef4a8bdd41cc29611c2e42f) Assisted-by: LLM --- .../gpu/drm/asahi/g17/queue/compute/exit.rs | 4 + drivers/gpu/drm/asahi/g17/runtime.rs | 2 + .../gpu/drm/asahi/g17/runtime/compute_exit.rs | 201 +++++++++++++++++- .../gpu/drm/asahi/g17/runtime/render/exit.rs | 8 + drivers/gpu/drm/asahi/g17/teardown.rs | 148 +++++++++++++ drivers/gpu/drm/asahi/g17/trace.rs | 5 +- 6 files changed, 360 insertions(+), 8 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/queue/compute/exit.rs b/drivers/gpu/drm/asahi/g17/queue/compute/exit.rs index ae812dd41fc12..719a74787ca70 100644 --- a/drivers/gpu/drm/asahi/g17/queue/compute/exit.rs +++ b/drivers/gpu/drm/asahi/g17/queue/compute/exit.rs @@ -111,6 +111,10 @@ impl Queue { CANCELLED_DELAY_NS } } + /// A kill of `context` was published through this queue. + pub(crate) fn exit_published(&self, context: &Arc) -> bool { + self.exit.is_some() && self.context().is_some_and(|bound| Arc::ptr_eq(bound, context)) + } pub(crate) fn exit_owner(&self, context: &Arc) -> Option { if !self .active diff --git a/drivers/gpu/drm/asahi/g17/runtime.rs b/drivers/gpu/drm/asahi/g17/runtime.rs index 1e721a51f726e..2ff40e4c8924b 100644 --- a/drivers/gpu/drm/asahi/g17/runtime.rs +++ b/drivers/gpu/drm/asahi/g17/runtime.rs @@ -2380,6 +2380,8 @@ impl Registry { let waits = || self.compute.iter().flatten().filter_map(|entry| entry.queue.as_deref()) .filter_map(|queue| queue.kill_waiting(now)); compute_exit::write_kill_waits(f, waits().count(), waits().max().unwrap_or(0))?; + compute_exit::write_render_kills(f, self.teardown.kill_waits(now).count(), + self.teardown.kill_waits(now).max().unwrap_or(0))?; writeln!(f, "compute_slots used={} total={} reusable={} building={}", self.compute.iter().flatten().count(), self.compute.len(), self.compute.iter().flatten().filter_map(|entry| entry.queue.as_deref()).filter(|queue| queue.reusable()).count(), diff --git a/drivers/gpu/drm/asahi/g17/runtime/compute_exit.rs b/drivers/gpu/drm/asahi/g17/runtime/compute_exit.rs index 791c1fccae6ae..c8e6de387d91c 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/compute_exit.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/compute_exit.rs @@ -31,6 +31,47 @@ static PRESSED_KILLS: AtomicU32 = AtomicU32::new(0); static PRESSED_KILL_NS: AtomicU64 = AtomicU64::new(0); const PRESSED_KILL_INTERVAL_NS: u64 = 250_000_000; +/// Render-only kills (see `Firmware::publish_render_kill`): published, acknowledged, and +/// contexts refused (each counted once, by its first reason) because the scheduler page names +/// a queue outside the context, because the held pairs have no single owner, or because +/// admission could not hold them. +static RENDER_KILLS: [AtomicU32; 5] = [const { AtomicU32::new(0) }; 5]; +const RENDER_KILL_PUBLISHED: usize = 0; +const RENDER_KILL_ACKNOWLEDGED: usize = 1; +const RENDER_KILL_SCOPE: usize = 2; +const RENDER_KILL_OWNER: usize = 3; +const RENDER_KILL_HOLD: usize = 4; +/// A render-only candidate is left this long after close, as a quarantined compute queue is, +/// so that ordinary retirement goes first. +const RENDER_KILL_DELAY_NS: u64 = 250_000_000; + +/// The render queue a render-only kill names: one of the context's own render queues that +/// its scheduler page lists, never the reserved queues 0 and 1. +fn render_kill_target(listed: u128, own: u128) -> Option { + let candidates = listed & own & !0b11; + (candidates != 0).then(|| candidates.trailing_zeros() as u8) +} + +/// The one admission owner of every pair a kill holds, if there is one. +fn hold_owner(owners: impl Iterator>) -> Option { + let mut owner = None; + for slot_owner in owners { + match (owner, slot_owner) { + (_, None) | (_, Some(0)) => return None, + (None, Some(next)) => owner = Some(next), + (Some(current), Some(next)) if current != next => return None, + _ => {} + } + } + owner +} + +fn refuse_render_kill(teardown: &mut crate::g17::teardown::Pending, index: usize, reason: usize) { + if teardown.refuse_render_kill(index) { + RENDER_KILLS[reason].fetch_add(1, Ordering::Relaxed); + } +} + /// GPU memory is within an eighth of the device limit. A failed context holds its memory /// until its kill is acknowledged, so kills waiting for other contexts' compute to pause can /// push the device to its limit, and then every client's allocations fail: on Neo, @@ -63,6 +104,15 @@ pub(in crate::g17) fn write_kill_waits(f: &mut kernel::fmt::Formatter<'_>, waiti writeln!(f, "failed_kills waiting={} longest_wait_ms={}", waiting, longest / 1_000_000) } +pub(in crate::g17) fn write_render_kills(f: &mut kernel::fmt::Formatter<'_>, waiting: usize, longest: u64) + -> kernel::fmt::Result +{ + let count = |index: usize| RENDER_KILLS[index].load(Ordering::Relaxed); + writeln!(f, "render_kills published={} acknowledged={} refused_scope={} refused_owner={} refused_hold={} waiting={} longest_wait_ms={}", + count(RENDER_KILL_PUBLISHED), count(RENDER_KILL_ACKNOWLEDGED), count(RENDER_KILL_SCOPE), + count(RENDER_KILL_OWNER), count(RENDER_KILL_HOLD), waiting, longest / 1_000_000) +} + impl super::Registry { /// ACK storage belongs to the already-retained transaction, so a full event /// batch cannot overflow a separate cache. No allocation or wait is needed. @@ -74,6 +124,10 @@ impl super::Registry { } } } + if self.teardown.acknowledge_render_kill(cookie) { + RENDER_KILLS[RENDER_KILL_ACKNOWLEDGED].fetch_add(1, Ordering::Relaxed); + crate::g17::trace::teardown(4, cookie, 0x100, 0); + } } } @@ -101,6 +155,8 @@ impl Firmware { let _ = queue.announce_exit(now, || self.primary.notify(MSG_CONTROL_NOTIFY)); } } + let primary = &mut self.primary; + self.queues.teardown.announce_render_kills(|| primary.notify(MSG_CONTROL_NOTIFY)); // Under memory pressure one kill skips the wait, at most one per interval. let last = PRESSED_KILL_NS.load(Ordering::Relaxed); let mut pressed = @@ -109,14 +165,24 @@ impl Firmware { let Some((context, age)) = self.queues.teardown.closed(index, now) else { continue; }; - // A failed context's executed pairs keep their pool leases until the kill is - // acknowledged (`Pair::firmware_drained`), so its kill cannot wait for them. - if !self.queues.render.teardown_idle(&context)? - || (!self.compute_failed(&context) && !self.queues.render.teardown_pools_released(&context)) - { + if !self.queues.render.teardown_idle(&context)? { + self.queues.teardown.clear_kill_wait(index); continue; } - if let Err(error) = self.publish_compute_exit(&context, age, now, &mut pressed) { + // A failed context's executed pairs keep their pool leases until the kill is + // acknowledged (`Pair::firmware_drained`), so its kill cannot wait for them. + let result = if self.compute_failed(&context) || self.queues.render.teardown_pools_released(&context) { + // Only a render-only kill's wait lives on the entry; a compute kill keeps its + // own on the queue. + self.queues.teardown.clear_kill_wait(index); + self.publish_compute_exit(&context, age, now, &mut pressed) + } else if self.render_kill_candidate(index, &context, age) { + self.publish_render_kill(index, &context, now, &mut pressed) + } else { + self.queues.teardown.clear_kill_wait(index); + Ok(()) + }; + if let Err(error) = result { dev_err!( self.primary.state.shared.dev.as_ref(), "Scheduler {} exit request failed: {:?}\n", @@ -243,6 +309,102 @@ impl Firmware { Ok(()) } + /// A closed context whose compute side is clean, with render pairs whose commands all ran + /// but were never stamped (`Pair::executed_for_kill`). Its failed compute command was + /// retired on the firmware's fault record, so no compute queue is left to carry a kill, + /// yet those pairs are released only after one: without this the context, its pairs and + /// its memory stay until the processors stop. + fn render_kill_candidate(&self, index: usize, context: &Arc, age: u64) -> bool { + age >= RENDER_KILL_DELAY_NS + && !self.queues.teardown.render_kill_published(index) + && self.queues.render.executed_slots(context) != 0 + && self.queues.compute.iter().flatten().filter_map(|entry| entry.queue.as_deref()).all(|queue| { + queue.idle_for_context(context) && !queue.switching_from(context) && !queue.exit_published(context) + }) + } + + /// Kill a render-only candidate (`render_kill_candidate`). The rules of a failed + /// context's kill apply: the pairs are held for the kill and their memory is released only + /// once the scheduler page reports it, the kill waits for a moment without other contexts' + /// compute unless GPU memory is pressed, and it names only queues of this context. + fn publish_render_kill(&mut self, index: usize, context: &Arc, now: u64, pressed: &mut bool) -> Result { + if self.render_control_backpressured() { + return Ok(()); + } + let beside = self.foreign_compute_running(context, now); + if beside && !*pressed { + let (waited, crossed) = self.queues.teardown.note_kill_wait(index, now, SLOW_KILL_WAIT_NS); + let last = SLOW_KILL_WARNED_NS.load(Ordering::Relaxed); + if crossed + && (last == 0 || now.saturating_sub(last) >= SLOW_KILL_WAIT_NS) + && SLOW_KILL_WARNED_NS + .compare_exchange(last, now.max(1), Ordering::Relaxed, Ordering::Relaxed) + .is_ok() + { + dev_warn!( + self.primary.state.shared.dev.as_ref(), + "Scheduler {} kill has waited {} ms for other contexts' compute to pause\n", + context.id(), + waited / 1_000_000 + ); + } + return Ok(()); + } + let (mut own, mut foreign, slots) = self.queues.render.exit_scope(context); + for entry in self.queues.compute.iter().flatten() { + if let Some(queue) = entry.queue.as_deref() { + let bit = 1u128 << queue.qid(); + if queue.context().is_some_and(|bound| Arc::ptr_eq(bound, context)) { + own |= bit; + } else { + foreign |= bit; + } + } + } + let state = exit::State::read(context)?; + let Some(qids) = render_kill_target(state.qids, own).and_then(|target| state.scope(target, own, foreign)) else { + refuse_render_kill(&mut self.queues.teardown, index, RENDER_KILL_SCOPE); + return Ok(()); + }; + let held = |slot: u8| slots & (1u64 << slot) != 0 && self.queues.render.qids_for_slot(slot) & qids != 0; + let range = 0..crate::hw::t8140::queues::RENDER_SLOTS as u8; + let hold = range.clone().filter(|slot| held(*slot)).fold(0u64, |hold, slot| hold | 1u64 << slot); + let owner = hold_owner(range.filter(|slot| held(*slot)).map(|slot| self.queues.render.slot_owner(slot))); + let Some(owner) = owner.filter(|_| hold != 0) else { + refuse_render_kill(&mut self.queues.teardown, index, RENDER_KILL_OWNER); + return Ok(()); + }; + if !self + .queues + .admission + .hold_render_slots_for_exit_kill(owner, hold, self.queues.render.executed_slots(context) & hold) + { + refuse_render_kill(&mut self.queues.teardown, index, RENDER_KILL_HOLD); + return Ok(()); + } + let cookie = exit::next_cookie()?; + let record = ControlRecord::ContextKill(ContextKill::new(context.scheduler_va(), cookie)); + if let Err(error) = Rings::publish_control(&self.init, &record) { + self.queues.admission.release_exit_kill_hold(hold); + return if error == EAGAIN { Ok(()) } else { Err(error) }; + } + // Publication is irrevocable. The held pairs stay held until this context's + // scheduler release is consumed (`release_killed_slots` at detach). + self.queues.teardown.record_render_kill(index, cookie, now)?; + record_kill_wait(self.queues.teardown.take_kill_wait(index, now)); + if beside { + *pressed = false; + PRESSED_KILLS.fetch_add(1, Ordering::Relaxed); + PRESSED_KILL_NS.store(now.max(1), Ordering::Relaxed); + } + RENDER_KILLS[RENDER_KILL_PUBLISHED].fetch_add(1, Ordering::Relaxed); + crate::g17::trace::teardown(7, cookie, u64::from(context.id()), hold | u64::from(beside)); + fence(Ordering::SeqCst); + let primary = &mut self.primary; + self.queues.teardown.announce_render_kills(|| primary.notify(MSG_CONTROL_NOTIFY)); + Ok(()) + } + /// Another context's compute command is in flight. On Neo, kills of failed contexts beside /// other clients' compute occasionally made the firmware recover the GPU (twice in ten /// minutes of a five-lane load with 67 faults), even with nothing freed before the kill's @@ -304,6 +466,9 @@ impl Firmware { { return true; } + if self.queues.teardown.render_kill_polling(now, exit::POLL_NS) { + return true; + } (0..self.queues.teardown.len()).any(|index| { self.queues .teardown @@ -321,3 +486,27 @@ impl Firmware { }) } } + +#[kernel::macros::kunit_tests(asahi_g17_render_kill_target)] +mod render_kill_target_tests { + use super::{hold_owner, render_kill_target}; + + #[test] + fn a_render_kill_holds_pairs_of_one_owner_only() { + assert_eq!(hold_owner([Some(5), Some(5)].into_iter()), Some(5)); + assert_eq!(hold_owner([Some(5), Some(6)].into_iter()), None); + assert_eq!(hold_owner([Some(5), None].into_iter()), None); + assert_eq!(hold_owner([Some(0)].into_iter()), None); + assert_eq!(hold_owner(core::iter::empty()), None); + } + + #[test] + fn a_render_kill_names_an_own_listed_queue_never_0_or_1() { + // Listed by the scheduler page but not the context's own: no target. + assert_eq!(render_kill_target(1 << 40, 1 << 41), None); + // The reserved queues are never named. + assert_eq!(render_kill_target(0b11, 0b11), None); + assert_eq!(render_kill_target(0b111 | 1 << 40, 0b111 | 1 << 40), Some(2)); + assert_eq!(render_kill_target(1 << 40 | 1 << 90, 1 << 90), Some(90)); + } +} diff --git a/drivers/gpu/drm/asahi/g17/runtime/render/exit.rs b/drivers/gpu/drm/asahi/g17/runtime/render/exit.rs index 25e1335c5ba13..6c58f728b3eb5 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/render/exit.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/render/exit.rs @@ -39,6 +39,14 @@ impl RenderState { slots } + /// The admission owner whose identities the pair in `slot` was built with. + pub(in crate::g17::runtime) fn slot_owner(&self, slot: u8) -> Option { + self.entries + .get(usize::from(slot)) + .and_then(Option::as_ref) + .map(|entry| entry.reservation.owner) + } + pub(in crate::g17::runtime) fn qids_for_slot(&self, slot: u8) -> u128 { self.entries .get(usize::from(slot)) diff --git a/drivers/gpu/drm/asahi/g17/teardown.rs b/drivers/gpu/drm/asahi/g17/teardown.rs index 53518a76e91a4..fa2a72a77f060 100644 --- a/drivers/gpu/drm/asahi/g17/teardown.rs +++ b/drivers/gpu/drm/asahi/g17/teardown.rs @@ -24,6 +24,45 @@ struct Entry { phase: Phase, armed: u64, announce_pending: bool, + render_kill: Option, + /// Since when, and whether reported, a render-only kill has waited for other contexts' + /// compute to pause. + kill_wait: Option<(u64, bool)>, + /// A render-only kill of this context was refused (counted once per context). + kill_refused: bool, +} + +/// The kill of a closed context whose compute side is clean, published so that its render +/// pairs' executed but unstamped commands can be released (see `Pair::firmware_drained`). +/// The scheduler page's killed state, not this record, proves the kill to the pairs. +#[derive(Clone, Copy)] +struct RenderKill { + cookie: u64, + published: u64, + announced: bool, + acknowledged: bool, +} + +impl RenderKill { + fn new(cookie: u64, now: u64) -> Self { + Self { + cookie, + published: now, + announced: false, + acknowledged: false, + } + } + /// Only this kill's exact cookie acknowledges it, once. + fn acknowledge(&mut self, cookie: u64) -> bool { + if cookie == 0 || cookie != self.cookie || self.acknowledged { + return false; + } + self.acknowledged = true; + true + } + fn awaiting(&self, now: u64, window: u64) -> bool { + !self.acknowledged && now.saturating_sub(self.published) < window + } } /// Publication and announcement errors retain the entry; no timeout frees @@ -104,6 +143,9 @@ impl Pending { phase, armed: now, announce_pending: false, + render_kill: None, + kill_wait: None, + kill_refused: false, }; self.entries.push(entry, GFP_KERNEL)?; Ok(()) @@ -233,6 +275,94 @@ impl Pending { Ok(None) } + /// A render-only kill of the closed context at `index` was published. + pub(crate) fn render_kill_published(&self, index: usize) -> bool { + self.entries.get(index).is_some_and(|entry| entry.render_kill.is_some()) + } + + /// Record a published render-only kill. Publication is irrevocable: the record stays + /// with the entry until the context's scheduler release is consumed. + pub(crate) fn record_render_kill(&mut self, index: usize, cookie: u64, now: u64) -> Result { + let entry = self.entries.get_mut(index).ok_or(EINVAL)?; + if entry.render_kill.is_some() { + return Err(EINVAL); + } + entry.render_kill = Some(RenderKill::new(cookie, now)); + Ok(()) + } + + /// Ring the doorbell for published render-only kills not yet announced. A failed + /// doorbell is retried on a later pass and never duplicates the record. + pub(crate) fn announce_render_kills(&mut self, mut notify: impl FnMut() -> Result) { + for entry in self.entries.iter_mut() { + if let Some(kill) = entry.render_kill.as_mut() { + if !kill.announced && !kill.acknowledged && notify().is_ok() { + kill.announced = true; + } + } + } + } + + /// Only the exact cookie of an unacknowledged render-only kill is accepted. + pub(crate) fn acknowledge_render_kill(&mut self, cookie: u64) -> bool { + self.entries + .iter_mut() + .filter_map(|entry| entry.render_kill.as_mut()) + .any(|kill| kill.acknowledge(cookie)) + } + + /// A render-only kill awaits its acknowledgement within the polling window. + pub(crate) fn render_kill_polling(&self, now: u64, window: u64) -> bool { + self.entries + .iter() + .any(|entry| entry.render_kill.is_some_and(|kill| kill.awaiting(now, window))) + } + + /// The render-only kill of the entry at `index` waits; how long, and whether this is the + /// first time it crossed `slow`. + pub(crate) fn note_kill_wait(&mut self, index: usize, now: u64, slow: u64) -> (u64, bool) { + let Some(entry) = self.entries.get_mut(index) else { + return (0, false); + }; + let (since, warned) = entry.kill_wait.get_or_insert((now, false)); + let waited = now.saturating_sub(*since); + let crossed = waited >= slow && !*warned; + *warned |= crossed; + (waited, crossed) + } + + /// The render-only kill was published: how long it waited, if it did. + pub(crate) fn take_kill_wait(&mut self, index: usize, now: u64) -> u64 { + self.entries + .get_mut(index) + .and_then(|entry| entry.kill_wait.take()) + .map_or(0, |(since, _)| now.saturating_sub(since)) + } + + /// A render-only kill of the context at `index` was refused: it no longer waits, and this + /// returns whether it is the first refusal for this context. + pub(crate) fn refuse_render_kill(&mut self, index: usize) -> bool { + let Some(entry) = self.entries.get_mut(index) else { + return false; + }; + entry.kill_wait = None; + !core::mem::replace(&mut entry.kill_refused, true) + } + + /// The context at `index` is not waiting for a render-only kill (any more). + pub(crate) fn clear_kill_wait(&mut self, index: usize) { + if let Some(entry) = self.entries.get_mut(index) { + entry.kill_wait = None; + } + } + + /// How long each waiting render-only kill has waited. + pub(crate) fn kill_waits(&self, now: u64) -> impl Iterator + '_ { + self.entries + .iter() + .filter_map(move |entry| entry.kill_wait.map(|(since, _)| now.saturating_sub(since))) + } + /// Poll for a bounded window at close and again at actual release publication. /// Failed announcements remain retryable on later service even after polling /// expires. Time passage never substitutes for a firmware-consumption witness. @@ -242,3 +372,21 @@ impl Pending { }) } } + +#[kernel::macros::kunit_tests(asahi_g17_render_kill)] +mod render_kill_tests { + use super::RenderKill; + + #[test] + fn only_the_exact_cookie_acknowledges_a_render_kill_once() { + let mut kill = RenderKill::new(7, 1_000); + assert!(kill.awaiting(1_000, 10)); + assert!(!kill.acknowledge(0)); + assert!(!kill.acknowledge(8)); + assert!(kill.awaiting(1_009, 10)); + assert!(!kill.awaiting(1_010, 10)); + assert!(kill.acknowledge(7)); + assert!(!kill.acknowledge(7)); + assert!(!kill.awaiting(1_001, 10)); + } +} diff --git a/drivers/gpu/drm/asahi/g17/trace.rs b/drivers/gpu/drm/asahi/g17/trace.rs index 72197f0eac0b7..78d88db4718c6 100644 --- a/drivers/gpu/drm/asahi/g17/trace.rs +++ b/drivers/gpu/drm/asahi/g17/trace.rs @@ -77,8 +77,9 @@ pub(super) fn message(kind: u8, value: u64) { /// Failed-context teardown: 1 compute fault (qid, stamp), 2 unstamped render retire (slot, /// ordinal), 3 kill published (cookie, context, held slots | 1 if it skipped the wait for other -/// contexts' compute under memory pressure; slot 0 is never held), 4 kill acknowledged (cookie, qid), -/// 5 detach (context, slots, linked), 6 render slot to a new owner (slot, old, new | killed << 63). +/// contexts' compute under memory pressure; slot 0 is never held), 4 kill acknowledged (cookie, +/// qid, or 0x100 for a render-only kill), 5 detach (context, slots, linked), 6 render slot to a +/// new owner (slot, old, new | killed << 63), 7 render-only kill published (as 3). pub(crate) fn teardown(kind: u8, a: u64, b: u64, c: u64) { // SAFETY: Plain values only. unsafe { asahi_g17_trace_teardown(kind, a, b, c) }; From a6a9f961ec9797edba701eb0bfa789dc2addacd5 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Tue, 27 Oct 2026 02:19:20 -0400 Subject: [PATCH 322/388] drm/asahi: take a G17 render-only kill's cookie before its admission hold A render-only kill took its admission hold on the context's render pairs and only then its cookie. If the cookie counter was exhausted, the EOVERFLOW returned with the hold still installed, so those pairs stayed held for a kill that was never published. Take the cookie first, as a failed context's kill already does. Signed-off-by: Ryan Murray (cherry picked from commit d6d0c93e679fdb31a290b6d3ffa1fbd334411836) Assisted-by: LLM --- drivers/gpu/drm/asahi/g17/runtime/compute_exit.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/asahi/g17/runtime/compute_exit.rs b/drivers/gpu/drm/asahi/g17/runtime/compute_exit.rs index c8e6de387d91c..cd7f2999d0312 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/compute_exit.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/compute_exit.rs @@ -374,6 +374,9 @@ impl Firmware { refuse_render_kill(&mut self.queues.teardown, index, RENDER_KILL_OWNER); return Ok(()); }; + // As for a failed context's kill: the cookie first, so that running out of cookies + // leaves no admission hold behind. + let cookie = exit::next_cookie()?; if !self .queues .admission @@ -382,7 +385,6 @@ impl Firmware { refuse_render_kill(&mut self.queues.teardown, index, RENDER_KILL_HOLD); return Ok(()); } - let cookie = exit::next_cookie()?; let record = ControlRecord::ContextKill(ContextKill::new(context.scheduler_va(), cookie)); if let Err(error) = Rings::publish_control(&self.init, &record) { self.queues.admission.release_exit_kill_hold(hold); From 8a97e866ef16506d01b978a42f7158cef05bc23f Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Tue, 27 Oct 2026 03:27:37 -0400 Subject: [PATCH 323/388] drm/asahi: kill a G17 context at most once A closed context qualifies for a render-only kill while one of its pairs holds executed but unstamped commands and no compute queue still runs it, switches away from it or killed it. A context killed through its compute queue normally leaves that state at once, since the acknowledged kill releases the pair in the same service pass, but only that timing kept the render-only path from killing it again: the compute queue's own record of the kill ends when the queue is rebound. A second kill of a killed context is harmful. In a diagnostic run that held the pair's release back for 100 ms after the kill, 31 contexts were killed twice, 0.1 to 2.5 ms apart, and twice the firmware recovered the GPU 0.3 ms after acknowledging the second kill, on a page fault in the killed pair's own tiling queue. The 35 contexts killed once in that run caused no recovery. Record on the context that a kill was published, by either path, and never publish a render-only kill for a context that has one. Signed-off-by: Ryan Murray (cherry picked from commit 7a9ebf019036a7fae177e3cc626625e903f4332d) Assisted-by: LLM --- drivers/gpu/drm/asahi/g17/context.rs | 14 ++++++++ .../gpu/drm/asahi/g17/runtime/compute_exit.rs | 35 +++++++++++++++---- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/context.rs b/drivers/gpu/drm/asahi/g17/context.rs index ba74140d4f243..0b3bd97fb42fe 100644 --- a/drivers/gpu/drm/asahi/g17/context.rs +++ b/drivers/gpu/drm/asahi/g17/context.rs @@ -114,6 +114,9 @@ pub(crate) struct Context { scheduler: Arc, scheduler_generation: u32, published: AtomicBool, + /// A kill of this context was published, through a compute queue or on its render pairs' + /// behalf. A context is killed at most once. + kill_published: AtomicBool, /// Advisory only: publication still checks the physical binding under the /// device mutex. False skips an inline attempt known to need admission. compute_binding_hint: AtomicBool, @@ -197,6 +200,7 @@ impl Context { scheduler, scheduler_generation, published: AtomicBool::new(false), + kill_published: AtomicBool::new(false), compute_binding_hint: AtomicBool::new(false), render_seen: AtomicBool::new(false), qos, @@ -316,6 +320,16 @@ impl Context { Ok(head) } + /// Record that a kill of this context was published (see `kill_published`). + pub(crate) fn note_kill_published(&self) { + self.kill_published.store(true, Ordering::Release); + } + + /// A kill of this context was published. + pub(crate) fn kill_published(&self) -> bool { + self.kill_published.load(Ordering::Acquire) + } + pub(crate) fn is_published(&self) -> bool { self.published.load(Ordering::Acquire) } diff --git a/drivers/gpu/drm/asahi/g17/runtime/compute_exit.rs b/drivers/gpu/drm/asahi/g17/runtime/compute_exit.rs index cd7f2999d0312..41960369b04a3 100644 --- a/drivers/gpu/drm/asahi/g17/runtime/compute_exit.rs +++ b/drivers/gpu/drm/asahi/g17/runtime/compute_exit.rs @@ -52,6 +52,16 @@ fn render_kill_target(listed: u128, own: u128) -> Option { (candidates != 0).then(|| candidates.trailing_zeros() as u8) } +/// A closed context may be killed on its render pairs' behalf once it has been closed for +/// [`RENDER_KILL_DELAY_NS`], it was never killed, a pair of it holds executed but unstamped +/// commands and no compute queue still runs it, switches away from it or killed it. A second kill +/// of a killed context made the firmware fault on the killed pair's own tiling queue within a +/// millisecond of acknowledging it: with the pair's release held back, a context killed through +/// its compute queue still looked like a render-only candidate. +fn render_kill_eligible(age: u64, killed: bool, executed: bool, compute_clear: bool) -> bool { + age >= RENDER_KILL_DELAY_NS && !killed && executed && compute_clear +} + /// The one admission owner of every pair a kill holds, if there is one. fn hold_owner(owners: impl Iterator>) -> Option { let mut owner = None; @@ -296,6 +306,7 @@ impl Firmware { PRESSED_KILL_NS.store(now.max(1), Ordering::Relaxed); } queue.record_exit(cookie, now); + context.note_kill_published(); crate::g17::trace::teardown(3, cookie, u64::from(context.id()), hold | u64::from(beside)); fence(Ordering::SeqCst); if let Err(error) = queue.announce_exit(now, || self.primary.notify(MSG_CONTROL_NOTIFY)) { @@ -315,12 +326,14 @@ impl Firmware { /// yet those pairs are released only after one: without this the context, its pairs and /// its memory stay until the processors stop. fn render_kill_candidate(&self, index: usize, context: &Arc, age: u64) -> bool { - age >= RENDER_KILL_DELAY_NS - && !self.queues.teardown.render_kill_published(index) - && self.queues.render.executed_slots(context) != 0 - && self.queues.compute.iter().flatten().filter_map(|entry| entry.queue.as_deref()).all(|queue| { + render_kill_eligible( + age, + self.queues.teardown.render_kill_published(index) || context.kill_published(), + self.queues.render.executed_slots(context) != 0, + self.queues.compute.iter().flatten().filter_map(|entry| entry.queue.as_deref()).all(|queue| { queue.idle_for_context(context) && !queue.switching_from(context) && !queue.exit_published(context) - }) + }), + ) } /// Kill a render-only candidate (`render_kill_candidate`). The rules of a failed @@ -393,6 +406,7 @@ impl Firmware { // Publication is irrevocable. The held pairs stay held until this context's // scheduler release is consumed (`release_killed_slots` at detach). self.queues.teardown.record_render_kill(index, cookie, now)?; + context.note_kill_published(); record_kill_wait(self.queues.teardown.take_kill_wait(index, now)); if beside { *pressed = false; @@ -491,7 +505,16 @@ impl Firmware { #[kernel::macros::kunit_tests(asahi_g17_render_kill_target)] mod render_kill_target_tests { - use super::{hold_owner, render_kill_target}; + use super::{hold_owner, render_kill_eligible, render_kill_target, RENDER_KILL_DELAY_NS}; + + #[test] + fn a_killed_context_is_never_killed_again() { + assert!(render_kill_eligible(RENDER_KILL_DELAY_NS, false, true, true)); + assert!(!render_kill_eligible(RENDER_KILL_DELAY_NS, true, true, true)); + assert!(!render_kill_eligible(RENDER_KILL_DELAY_NS - 1, false, true, true)); + assert!(!render_kill_eligible(RENDER_KILL_DELAY_NS, false, false, true)); + assert!(!render_kill_eligible(RENDER_KILL_DELAY_NS, false, true, false)); + } #[test] fn a_render_kill_holds_pairs_of_one_owner_only() { From 33e0e46b60829208e3f3d0752fb8025162899920 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 18:06:29 +1100 Subject: [PATCH 324/388] hwmon: macsmc: Unlock fan control on firmware that keeps fans in system mode On the M4 Pro MacBook Pro (J616s) both fans start in mode 3, the firmware's own fan policy, and the SMC refuses the manual mode write of the fan_control path with an error, so fan control from Linux did nothing. Public reports for M4 describe the same refusal and the Ftst key that releases the fans to the OS. When userspace first asks for a target on a fan whose mode write is refused while it reads mode 3, set Ftst, wait for the fan to leave system mode and ask again. When the last manual fan goes back to auto, clear Ftst and wait for every fan to return to mode 3. Firmware that takes the mode write directly keeps the mode 0 restore; firmware that refuses it without an Ftst key gets the error as before. Hand every manual fan back on unbind or remove, at shutdown and before suspend, and whenever a write in the sequence fails. Targets are now clamped to the fan's minimum and maximum instead of refused, so a target can never go below the minimum. Everything stays behind the existing fan_control parameter; without it nothing is written. The new KUnit suite models locked and legacy firmware. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/hwmon/Kconfig | 7 +- drivers/hwmon/macsmc-hwmon-test.c | 262 +++++++++++++++++++++++++++++- drivers/hwmon/macsmc-hwmon.c | 261 +++++++++++++++++++++++++---- 3 files changed, 497 insertions(+), 33 deletions(-) diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig index 54eb5745105ff..9b6b0593310da 100644 --- a/drivers/hwmon/Kconfig +++ b/drivers/hwmon/Kconfig @@ -1252,10 +1252,13 @@ config SENSORS_MACSMC_HWMON_KUNIT_TEST depends on SENSORS_MACSMC_HWMON && KUNIT=y default KUNIT_ALL_TESTS help - Unit tests for the channel numbering of the Apple SMC hwmon driver. + Unit tests for the channel numbering and the manual fan control of + the Apple SMC hwmon driver. Thermal zones refer to temperature channels by index, so a sensor that cannot be set up must keep its channel; the tests check that. - No SMC is needed. Only useful for kernel developers. + No SMC is needed: the fan tests model its fan keys and need + MFD_MACSMC_KUNIT_TEST for its read and write stubs. Only useful for + kernel developers. config SENSORS_MAX1111 tristate "Maxim MAX1111 Serial 8-bit ADC chip and compatibles" diff --git a/drivers/hwmon/macsmc-hwmon-test.c b/drivers/hwmon/macsmc-hwmon-test.c index e7239986ce3c9..596e904a6f3e8 100644 --- a/drivers/hwmon/macsmc-hwmon-test.c +++ b/drivers/hwmon/macsmc-hwmon-test.c @@ -1,11 +1,12 @@ // SPDX-License-Identifier: GPL-2.0-only OR MIT /* * KUnit tests for the Apple SMC hwmon driver: channel numbering of the - * temperature sensors, which thermal zones depend on. + * temperature sensors, which thermal zones depend on, and manual fan control. * * Included by macsmc-hwmon.c. */ +#include #include /* What macsmc_hwmon_create_sensor() leaves behind for one node. */ @@ -84,4 +85,261 @@ static struct kunit_suite macsmc_hwmon_test_suite = { .test_cases = macsmc_hwmon_test_cases, }; -kunit_test_suite(macsmc_hwmon_test_suite); +/* + * Manual fan control against a model of the SMC fan keys. In "locked" firmware + * the fans start in system mode (3) and refuse manual mode until Ftst is 1; + * Ftst back to 0 returns them to system mode. "Legacy" firmware takes the mode + * write directly. The model needs the SMC read/write stubs of + * CONFIG_MFD_MACSMC_KUNIT_TEST. + */ +#define FAN_TEST_F1350 0x44a8c000 /* 1350.0f */ +#define FAN_TEST_F2500 0x451c4000 /* 2500.0f */ +#define FAN_TEST_F5777 0x45b48800 /* 5777.0f */ + +struct macsmc_fan_test { + struct apple_smc smc; + struct kunit *test; + struct macsmc_hwmon hwmon; + struct macsmc_hwmon_fan fans[2]; + bool legacy; + u8 mode[2]; + u32 target[2]; + u8 ftst; + int ftst_writes; + int refused; + smc_key fail_key; +}; + +static int fan_test_index(smc_key key) +{ + return ((key >> 16) & 0xff) - '0'; +} + +static int fan_test_read(struct apple_smc *smc, smc_key key, void *buf, size_t size) +{ + struct macsmc_fan_test *t = container_of(smc, struct macsmc_fan_test, smc); + u32 value; + int i = fan_test_index(key); + + switch (key & 0xff00ffff) { + case __SMC_KEY('F', 0, 'M', 'd'): + value = t->mode[i]; + break; + case __SMC_KEY('F', 0, 'M', 'n'): + value = FAN_TEST_F1350; + break; + case __SMC_KEY('F', 0, 'M', 'x'): + value = FAN_TEST_F5777; + break; + case __SMC_KEY('F', 0, 'T', 'g'): + value = t->target[i]; + break; + default: + KUNIT_FAIL(t->test, "unexpected read of %08x", key); + return -EINVAL; + } + memcpy(buf, &value, size); + return size; +} + +static int fan_test_write(struct apple_smc *smc, smc_key key, const void *buf, size_t size) +{ + struct macsmc_fan_test *t = container_of(smc, struct macsmc_fan_test, smc); + u32 value = 0; + int i = fan_test_index(key); + + memcpy(&value, buf, min(size, sizeof(value))); + if (key == t->fail_key) + return -EIO; + + if (key == SMC_KEY(Ftst)) { + t->ftst_writes++; + t->ftst = value; + for (i = 0; i < ARRAY_SIZE(t->mode); i++) { + if (value && t->mode[i] == MACSMC_FAN_MODE_SYSTEM) + t->mode[i] = MACSMC_FAN_MODE_AUTO; + else if (!value) + t->mode[i] = MACSMC_FAN_MODE_SYSTEM; + } + return size; + } + + switch (key & 0xff00ffff) { + case __SMC_KEY('F', 0, 'M', 'd'): + if (!t->legacy && t->mode[i] == MACSMC_FAN_MODE_SYSTEM) { + t->refused++; + return -EIO; + } + t->mode[i] = value; + return size; + case __SMC_KEY('F', 0, 'T', 'g'): + t->target[i] = value; + return size; + } + KUNIT_FAIL(t->test, "unexpected write of %08x", key); + return -EINVAL; +} + +static void fan_test_key(struct macsmc_hwmon_sensor *sensor, const char *key, u32 type) +{ + sensor->macsmc_key = _SMC_KEY(key); + sensor->info.type_code = type; + sensor->info.flags = APPLE_SMC_READABLE | APPLE_SMC_WRITABLE; +} + +static int macsmc_fan_test_init(struct kunit *test) +{ + static const char *const keys[2][5] = { + { "F0Ac", "F0Mn", "F0Mx", "F0Tg", "F0Md" }, + { "F1Ac", "F1Mn", "F1Mx", "F1Tg", "F1Md" }, + }; + const u32 flt = __SMC_KEY('f', 'l', 't', ' '), ui8 = __SMC_KEY('u', 'i', '8', ' '); + struct macsmc_fan_test *t; + int i; + + if (!IS_ENABLED(CONFIG_MFD_MACSMC_KUNIT_TEST)) + kunit_skip(test, "needs CONFIG_MFD_MACSMC_KUNIT_TEST for the SMC stubs"); + + t = kunit_kzalloc(test, sizeof(*t), GFP_KERNEL); + if (!t) + return -ENOMEM; + t->test = test; + for (i = 0; i < 2; i++) { + fan_test_key(&t->fans[i].now, keys[i][0], flt); + fan_test_key(&t->fans[i].min, keys[i][1], flt); + fan_test_key(&t->fans[i].max, keys[i][2], flt); + fan_test_key(&t->fans[i].set, keys[i][3], flt); + fan_test_key(&t->fans[i].mode, keys[i][4], ui8); + t->mode[i] = MACSMC_FAN_MODE_SYSTEM; + } + t->hwmon.smc = &t->smc; + t->hwmon.fan.fans = t->fans; + t->hwmon.fan.count = 2; + t->hwmon.fan.unlock_key = SMC_KEY(Ftst); + mutex_init(&t->hwmon.fan.lock); + test->priv = t; + fan_control = true; + kunit_activate_static_stub(test, apple_smc_read, fan_test_read); + kunit_activate_static_stub(test, apple_smc_write, fan_test_write); + return 0; +} + +static void macsmc_fan_test_exit(struct kunit *test) +{ + fan_control = false; +} + +static void fan_test_released(struct kunit *test, struct macsmc_fan_test *t) +{ + KUNIT_EXPECT_EQ(test, t->ftst, 0); + KUNIT_EXPECT_EQ(test, t->mode[0], MACSMC_FAN_MODE_SYSTEM); + KUNIT_EXPECT_EQ(test, t->mode[1], MACSMC_FAN_MODE_SYSTEM); + KUNIT_EXPECT_EQ(test, t->hwmon.fan.manual, 0); + KUNIT_EXPECT_FALSE(test, t->hwmon.fan.unlocked); + KUNIT_EXPECT_FALSE(test, t->fans[0].manual); +} + +/* Locked firmware: unlock on the first manual request, release on auto. */ +static void macsmc_fan_test_unlock_and_release(struct kunit *test) +{ + struct macsmc_fan_test *t = test->priv; + + KUNIT_ASSERT_EQ(test, macsmc_hwmon_set_fan(&t->hwmon, 0, 2500), 0); + KUNIT_EXPECT_EQ(test, t->ftst, 1); + KUNIT_EXPECT_EQ(test, t->mode[0], MACSMC_FAN_MODE_MANUAL); + KUNIT_EXPECT_EQ(test, t->target[0], FAN_TEST_F2500); + KUNIT_EXPECT_EQ(test, t->hwmon.fan.manual, 1); + + /* A second target needs no new unlock. */ + KUNIT_ASSERT_EQ(test, macsmc_hwmon_set_fan(&t->hwmon, 0, 2500), 0); + KUNIT_EXPECT_EQ(test, t->ftst_writes, 1); + + KUNIT_ASSERT_EQ(test, macsmc_hwmon_set_fan(&t->hwmon, 0, 0), 0); + fan_test_released(test, t); + KUNIT_EXPECT_EQ(test, t->ftst_writes, 2); +} + +/* Targets are clamped to [min, max]; negative ones are refused. */ +static void macsmc_fan_test_clamp(struct kunit *test) +{ + struct macsmc_fan_test *t = test->priv; + + KUNIT_ASSERT_EQ(test, macsmc_hwmon_set_fan(&t->hwmon, 0, 100), 0); + KUNIT_EXPECT_EQ(test, t->target[0], FAN_TEST_F1350); + KUNIT_ASSERT_EQ(test, macsmc_hwmon_set_fan(&t->hwmon, 0, 100000), 0); + KUNIT_EXPECT_EQ(test, t->target[0], FAN_TEST_F5777); + KUNIT_EXPECT_EQ(test, macsmc_hwmon_set_fan(&t->hwmon, 0, -1), -EINVAL); + KUNIT_ASSERT_EQ(test, macsmc_hwmon_set_fan(&t->hwmon, 0, 0), 0); + fan_test_released(test, t); +} + +/* A failed target write hands the fan straight back. */ +static void macsmc_fan_test_target_write_fails(struct kunit *test) +{ + struct macsmc_fan_test *t = test->priv; + + t->fail_key = SMC_KEY(F0Tg); + KUNIT_EXPECT_EQ(test, macsmc_hwmon_set_fan(&t->hwmon, 0, 2500), -EIO); + fan_test_released(test, t); +} + +/* Two manual fans: the unlock is released only with the last one. */ +static void macsmc_fan_test_two_fans(struct kunit *test) +{ + struct macsmc_fan_test *t = test->priv; + + KUNIT_ASSERT_EQ(test, macsmc_hwmon_set_fan(&t->hwmon, 0, 2500), 0); + KUNIT_ASSERT_EQ(test, macsmc_hwmon_set_fan(&t->hwmon, 1, 2500), 0); + KUNIT_EXPECT_EQ(test, t->hwmon.fan.manual, 2); + KUNIT_ASSERT_EQ(test, macsmc_hwmon_set_fan(&t->hwmon, 0, 0), 0); + KUNIT_EXPECT_EQ(test, t->ftst, 1); + KUNIT_EXPECT_EQ(test, t->mode[1], MACSMC_FAN_MODE_MANUAL); + /* Removal, shutdown and suspend all restore through this. */ + macsmc_hwmon_fans_restore(&t->hwmon); + fan_test_released(test, t); +} + +/* Locked firmware without the unlock key: refused, nothing else written. */ +static void macsmc_fan_test_no_unlock_key(struct kunit *test) +{ + struct macsmc_fan_test *t = test->priv; + + t->hwmon.fan.unlock_key = 0; + KUNIT_EXPECT_EQ(test, macsmc_hwmon_set_fan(&t->hwmon, 0, 2500), -EIO); + KUNIT_EXPECT_EQ(test, t->ftst_writes, 0); + KUNIT_EXPECT_EQ(test, t->mode[0], MACSMC_FAN_MODE_SYSTEM); + KUNIT_EXPECT_EQ(test, t->hwmon.fan.manual, 0); +} + +/* Firmware that takes the mode directly keeps the mode 0 restore. */ +static void macsmc_fan_test_legacy(struct kunit *test) +{ + struct macsmc_fan_test *t = test->priv; + + t->legacy = true; + t->mode[0] = MACSMC_FAN_MODE_AUTO; + KUNIT_ASSERT_EQ(test, macsmc_hwmon_set_fan(&t->hwmon, 0, 2500), 0); + KUNIT_EXPECT_EQ(test, t->mode[0], MACSMC_FAN_MODE_MANUAL); + KUNIT_ASSERT_EQ(test, macsmc_hwmon_set_fan(&t->hwmon, 0, 0), 0); + KUNIT_EXPECT_EQ(test, t->mode[0], MACSMC_FAN_MODE_AUTO); + KUNIT_EXPECT_EQ(test, t->ftst_writes, 0); + KUNIT_EXPECT_EQ(test, t->refused, 0); +} + +static struct kunit_case macsmc_fan_test_cases[] = { + KUNIT_CASE(macsmc_fan_test_unlock_and_release), + KUNIT_CASE(macsmc_fan_test_clamp), + KUNIT_CASE(macsmc_fan_test_target_write_fails), + KUNIT_CASE(macsmc_fan_test_two_fans), + KUNIT_CASE(macsmc_fan_test_no_unlock_key), + KUNIT_CASE(macsmc_fan_test_legacy), + {} +}; + +static struct kunit_suite macsmc_fan_test_suite = { + .name = "macsmc-hwmon-fan", + .init = macsmc_fan_test_init, + .exit = macsmc_fan_test_exit, + .test_cases = macsmc_fan_test_cases, +}; +kunit_test_suites(&macsmc_hwmon_test_suite, &macsmc_fan_test_suite); diff --git a/drivers/hwmon/macsmc-hwmon.c b/drivers/hwmon/macsmc-hwmon.c index 06d2c6e2d0ac3..76f07add003d3 100644 --- a/drivers/hwmon/macsmc-hwmon.c +++ b/drivers/hwmon/macsmc-hwmon.c @@ -23,12 +23,15 @@ #include #include #include +#include #include #include #include #include +#include #include #include +#include #include #include #include @@ -189,10 +192,30 @@ struct macsmc_hwmon_sensors { u32 count; }; +/* Fan mode key values */ +#define MACSMC_FAN_MODE_AUTO 0 +#define MACSMC_FAN_MODE_MANUAL 1 +/* The firmware's own fan policy; it refuses a manual mode request. */ +#define MACSMC_FAN_MODE_SYSTEM 3 + +/* How long the firmware gets to leave or re-enter its system mode */ +#define MACSMC_FAN_MODE_TIMEOUT_MS 10000 +#define MACSMC_FAN_MODE_POLL_MS 100 + struct macsmc_hwmon_fans { struct hwmon_channel_info channel_info; struct macsmc_hwmon_fan *fans; u32 count; + /* Serialises manual fan control and its unlock. */ + struct mutex lock; + /* + * Firmware that keeps its fans in system mode releases them to the + * OS only while this key is 1. Zero if the SMC has no such key. + */ + smc_key unlock_key; + bool unlocked; + /* Fans in manual mode */ + u32 manual; }; struct macsmc_hwmon { @@ -466,57 +489,199 @@ static int macsmc_hwmon_read_fan(struct macsmc_hwmon *hwmon, u32 attr, int chan, } } -static int macsmc_hwmon_write_fan(struct device *dev, u32 attr, int channel, - long val) +static int macsmc_hwmon_fan_read_mode(struct macsmc_hwmon *hwmon, + struct macsmc_hwmon_fan *fan, u8 *mode) { - struct macsmc_hwmon *hwmon = dev_get_drvdata(dev); + return apple_smc_read_u8(hwmon->smc, fan->mode.macsmc_key, mode); +} + +/* + * Hand the fans back to the firmware once no fan is manual: clear the unlock + * key and wait for every fan to return to system mode. Called with the fan + * lock held. + */ +static int macsmc_hwmon_fans_relock(struct macsmc_hwmon *hwmon) +{ + unsigned long timeout; + u32 i; + u8 mode; + int ret; + + ret = apple_smc_write_u8(hwmon->smc, hwmon->fan.unlock_key, 0); + if (ret < 0) { + dev_err(hwmon->dev, "cannot hand the fans back to the SMC (%d)\n", ret); + return ret; + } + hwmon->fan.unlocked = false; + + timeout = jiffies + msecs_to_jiffies(MACSMC_FAN_MODE_TIMEOUT_MS); + for (i = 0; i < hwmon->fan.count; i++) { + struct macsmc_hwmon_fan *fan = &hwmon->fan.fans[i]; + + if (!fan->mode.macsmc_key) + continue; + for (;;) { + ret = macsmc_hwmon_fan_read_mode(hwmon, fan, &mode); + if (ret) + return ret; + if (mode == MACSMC_FAN_MODE_SYSTEM) + break; + if (time_after(jiffies, timeout)) { + dev_warn(hwmon->dev, "%s stays in mode %u after release\n", + fan->label, mode); + return -ETIMEDOUT; + } + msleep(MACSMC_FAN_MODE_POLL_MS); + } + } + + return 0; +} + +/* Return a fan to firmware control. Called with the fan lock held. */ +static int macsmc_hwmon_fan_auto(struct macsmc_hwmon *hwmon, + struct macsmc_hwmon_fan *fan) +{ + int ret, relock; + + if (!fan->manual) + return 0; + + ret = macsmc_hwmon_write_key(hwmon->smc, &fan->mode, MACSMC_FAN_MODE_AUTO); + fan->manual = false; + hwmon->fan.manual--; + + if (hwmon->fan.unlocked && !hwmon->fan.manual) { + relock = macsmc_hwmon_fans_relock(hwmon); + if (relock) + return relock; + } + + return ret < 0 ? ret : 0; +} + +/* + * Put a fan in manual mode. Firmware in system mode refuses that until the + * unlock key is set; it then leaves system mode and accepts the request. + * Called with the fan lock held. + */ +static int macsmc_hwmon_fan_manual(struct macsmc_hwmon *hwmon, + struct macsmc_hwmon_fan *fan) +{ + unsigned long timeout; + u8 mode; + int ret; + + if (fan->manual) + return 0; + + ret = macsmc_hwmon_write_key(hwmon->smc, &fan->mode, MACSMC_FAN_MODE_MANUAL); + if (ret < 0 && hwmon->fan.unlock_key && !hwmon->fan.unlocked && + !macsmc_hwmon_fan_read_mode(hwmon, fan, &mode) && + mode == MACSMC_FAN_MODE_SYSTEM) { + ret = apple_smc_write_u8(hwmon->smc, hwmon->fan.unlock_key, 1); + if (ret < 0) { + /* The key may have been set anyway; clear it. */ + apple_smc_write_u8(hwmon->smc, hwmon->fan.unlock_key, 0); + return ret; + } + hwmon->fan.unlocked = true; + + /* Ask again once the fan has left system mode. */ + timeout = jiffies + msecs_to_jiffies(MACSMC_FAN_MODE_TIMEOUT_MS); + for (;;) { + ret = macsmc_hwmon_fan_read_mode(hwmon, fan, &mode); + if (!ret && mode == MACSMC_FAN_MODE_SYSTEM) + ret = -EBUSY; + else if (!ret) + ret = macsmc_hwmon_write_key(hwmon->smc, &fan->mode, + MACSMC_FAN_MODE_MANUAL); + if (ret >= 0 || time_after(jiffies, timeout)) + break; + msleep(MACSMC_FAN_MODE_POLL_MS); + } + if (ret < 0) { + dev_err(hwmon->dev, "%s refused manual mode after unlock (%d)\n", + fan->label, ret); + if (!hwmon->fan.manual) + macsmc_hwmon_fans_relock(hwmon); + return ret; + } + } + if (ret < 0) + return ret; + + fan->manual = true; + hwmon->fan.manual++; + return 0; +} + +static int macsmc_hwmon_set_fan(struct macsmc_hwmon *hwmon, int channel, long val) +{ + struct macsmc_hwmon_fan *fan = &hwmon->fan.fans[channel]; long min, max; int ret; - if (!fan_control || hwmon->fan.fans[channel].mode.macsmc_key == 0) + if (!fan_control || fan->mode.macsmc_key == 0) return -EOPNOTSUPP; + if (val < 0) + return -EINVAL; + + guard(mutex)(&hwmon->fan.lock); + + /* 0 hands the fan back to the firmware. */ + if (!val) + return macsmc_hwmon_fan_auto(hwmon, fan); /* * The SMC does no sanity checks on requested fan speeds, so we need to. */ - ret = macsmc_hwmon_read_key(hwmon->smc, &hwmon->fan.fans[channel].min, - 1, &min); + ret = macsmc_hwmon_read_key(hwmon->smc, &fan->min, 1, &min); if (ret) return ret; - ret = macsmc_hwmon_read_key(hwmon->smc, &hwmon->fan.fans[channel].max, - 1, &max); + ret = macsmc_hwmon_read_key(hwmon->smc, &fan->max, 1, &max); if (ret) return ret; - if (val >= min && val <= max) { - if (!hwmon->fan.fans[channel].manual) { - /* Write 1 to mode key for manual control */ - ret = macsmc_hwmon_write_key(hwmon->smc, - &hwmon->fan.fans[channel].mode, 1); - if (ret < 0) - return ret; + if (min <= 0 || max < min) + return -EIO; + val = clamp(val, min, max); - hwmon->fan.fans[channel].manual = true; - } - return macsmc_hwmon_write_key(hwmon->smc, - &hwmon->fan.fans[channel].set, val); - } else if (!val) { - if (hwmon->fan.fans[channel].manual) { - ret = macsmc_hwmon_write_key(hwmon->smc, - &hwmon->fan.fans[channel].mode, 0); - if (ret < 0) - return ret; + ret = macsmc_hwmon_fan_manual(hwmon, fan); + if (ret) + return ret; - hwmon->fan.fans[channel].manual = false; - } - } else { - return -EINVAL; + ret = macsmc_hwmon_write_key(hwmon->smc, &fan->set, val); + if (ret < 0) { + macsmc_hwmon_fan_auto(hwmon, fan); + return ret; } return 0; } +static int macsmc_hwmon_write_fan(struct device *dev, u32 attr, int channel, + long val) +{ + return macsmc_hwmon_set_fan(dev_get_drvdata(dev), channel, val); +} + +/* Hand every manual fan back to the firmware. */ +static void macsmc_hwmon_fans_restore(struct macsmc_hwmon *hwmon) +{ + u32 i; + + guard(mutex)(&hwmon->fan.lock); + for (i = 0; i < hwmon->fan.count; i++) + macsmc_hwmon_fan_auto(hwmon, &hwmon->fan.fans[i]); +} + +static void macsmc_hwmon_fans_restore_action(void *data) +{ + macsmc_hwmon_fans_restore(data); +} + static int macsmc_hwmon_read(struct device *dev, enum hwmon_sensor_types type, u32 attr, int channel, long *val) { @@ -1343,6 +1508,11 @@ static int macsmc_hwmon_probe(struct platform_device *pdev) hwmon->dev = &pdev->dev; hwmon->smc = smc; hwmon->register_thermal_zones = true; + platform_set_drvdata(pdev, hwmon); + + ret = devm_mutex_init(&pdev->dev, &hwmon->fan.lock); + if (ret) + return ret; ret = macsmc_hwmon_populate_sensors(hwmon, hwmon->dev->of_node); if (ret) { @@ -1372,6 +1542,21 @@ static int macsmc_hwmon_probe(struct platform_device *pdev) if (ret) return ret; + if (hwmon->fan.count) { + struct apple_smc_key_info info; + + if (!apple_smc_get_key_info(smc, SMC_KEY(Ftst), &info) && + info.type_code == __SMC_KEY('u', 'i', '8', ' ') && + (info.flags & APPLE_SMC_WRITABLE)) + hwmon->fan.unlock_key = SMC_KEY(Ftst); + + /* Runs after the hwmon device is gone, so no write races it. */ + ret = devm_add_action_or_reset(&pdev->dev, + macsmc_hwmon_fans_restore_action, hwmon); + if (ret) + return ret; + } + hwmon->chip_info.ops = &macsmc_hwmon_ops; hwmon->chip_info.info = (const struct hwmon_channel_info *const *)&hwmon->channel_infos; @@ -1407,11 +1592,29 @@ static const struct of_device_id macsmc_hwmon_of_table[] = { }; MODULE_DEVICE_TABLE(of, macsmc_hwmon_of_table); +static void macsmc_hwmon_shutdown(struct platform_device *pdev) +{ + struct macsmc_hwmon *hwmon = platform_get_drvdata(pdev); + + if (hwmon) + macsmc_hwmon_fans_restore(hwmon); +} + +static int macsmc_hwmon_suspend(struct device *dev) +{ + macsmc_hwmon_fans_restore(dev_get_drvdata(dev)); + return 0; +} + +static DEFINE_SIMPLE_DEV_PM_OPS(macsmc_hwmon_pm_ops, macsmc_hwmon_suspend, NULL); + static struct platform_driver macsmc_hwmon_driver = { .probe = macsmc_hwmon_probe, + .shutdown = macsmc_hwmon_shutdown, .driver = { .name = "macsmc-hwmon", .of_match_table = macsmc_hwmon_of_table, + .pm = pm_sleep_ptr(&macsmc_hwmon_pm_ops), }, }; module_platform_driver(macsmc_hwmon_driver); From dac4a3ae7858f8ef1b4f054d11efbac9c4785c67 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 17:59:10 +1100 Subject: [PATCH 325/388] arm64: dts: apple: t6040-j616s: Shed CPU power at the GPU firmware limit The GPU zones shed CPU power from 105 C, a pure backstop. But the J616s fans give no extra headroom under Linux: the SMC keeps them in its own mode (3), turns them on only at minimum speed (seen from 93-99 C die on a cold machine, from about 60 C once it is heat-soaked) and refuses an OS write of manual mode. The GPU firmware itself starts limiting at 101 C, and conformance runs reached 103.8 C. Start the GPU zones' passive trip at that same 101 C, so that once the GPU is at its own limit the CPU clusters give up power to it instead of both being limited separately. The critical trip stays at 118 C. Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/boot/dts/apple/t6040-j616s.dts | 28 +++++++++++++---------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/arch/arm64/boot/dts/apple/t6040-j616s.dts b/arch/arm64/boot/dts/apple/t6040-j616s.dts index 6a85dda0804ac..69a891d7111d5 100644 --- a/arch/arm64/boot/dts/apple/t6040-j616s.dts +++ b/arch/arm64/boot/dts/apple/t6040-j616s.dts @@ -313,11 +313,15 @@ * failure of that loop; they never act in normal use. * * The GPU has no cooling device of its own: its firmware sets the GPU - * performance state. The seven hottest GPU die sensors (Tg05, Tg0S, - * Tg0Y, Tg0k, Tg0z, Tg1V, Tg1l) shed package power through the CPU - * clusters from 105 C, above the 103.8 C peak measured during - * conformance runs, and shut down at 118 C. While the GPU is powered - * off its sensors report placeholder values from -4 to 4 C. + * performance state and starts limiting at 101 C. The SMC runs the + * fans in its own mode (3), only at their minimum speed in every load + * measured under Linux, and refuses manual fan modes from the OS. So + * when a GPU die sensor reaches the GPU firmware's limit, the seven + * hottest ones (Tg05, Tg0S, Tg0Y, Tg0k, Tg0z, Tg1V, Tg1l) also shed + * CPU power through the CPU clusters, from the same 101 C, leaving the + * shared heat budget to the GPU. They shut down at 118 C. While the + * GPU is powered off its sensors report placeholder values from -4 to + * 4 C. */ thermal-zones { cpu-die-thermal { @@ -886,7 +890,7 @@ trips { gpu0_passive: trip-passive { - temperature = <105000>; + temperature = <101000>; hysteresis = <5000>; type = "passive"; }; @@ -937,7 +941,7 @@ trips { gpu1_passive: trip-passive { - temperature = <105000>; + temperature = <101000>; hysteresis = <5000>; type = "passive"; }; @@ -988,7 +992,7 @@ trips { gpu2_passive: trip-passive { - temperature = <105000>; + temperature = <101000>; hysteresis = <5000>; type = "passive"; }; @@ -1039,7 +1043,7 @@ trips { gpu3_passive: trip-passive { - temperature = <105000>; + temperature = <101000>; hysteresis = <5000>; type = "passive"; }; @@ -1090,7 +1094,7 @@ trips { gpu4_passive: trip-passive { - temperature = <105000>; + temperature = <101000>; hysteresis = <5000>; type = "passive"; }; @@ -1141,7 +1145,7 @@ trips { gpu5_passive: trip-passive { - temperature = <105000>; + temperature = <101000>; hysteresis = <5000>; type = "passive"; }; @@ -1192,7 +1196,7 @@ trips { gpu6_passive: trip-passive { - temperature = <105000>; + temperature = <101000>; hysteresis = <5000>; type = "passive"; }; From 2c01e788b9a7dbc6cd5e05c161379df2a47675b6 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 15:02:20 +1100 Subject: [PATCH 326/388] arm64: dts: apple: describe the remaining T6040 PMGR power states Describe the power-state registers and their parent graph. Leave providers without a qualified Linux consumer disabled, since registering an unused domain may power down hardware still owned by firmware. Keep the existing tested providers unchanged. Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/boot/dts/apple/t6040-pmgr.dtsi | 1762 +++++++++++++++++++++ arch/arm64/boot/dts/apple/t6040.dtsi | 21 + 2 files changed, 1783 insertions(+) diff --git a/arch/arm64/boot/dts/apple/t6040-pmgr.dtsi b/arch/arm64/boot/dts/apple/t6040-pmgr.dtsi index bf357745d581d..18ec5df7d33bf 100644 --- a/arch/arm64/boot/dts/apple/t6040-pmgr.dtsi +++ b/arch/arm64/boot/dts/apple/t6040-pmgr.dtsi @@ -506,3 +506,1765 @@ power-domains = <&ps_atc2_phymxwrap>; }; }; + +/* + * Remaining ADT power states. Keep providers without a qualified consumer + * disabled: registering an unused genpd can power down firmware-owned blocks. + * Enable the provider and its parents together with the consumer. + */ +&pmgr { + ps_apcie_gp: power-controller@100 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x100 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "apcie_gp"; + status = "disabled"; + }; + + ps_fab4_soc: power-controller@128 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x128 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "fab4_soc"; + power-domains = <&ps_afc>; + apple,always-on; + status = "disabled"; + }; + + ps_sio_cpu: power-controller@130 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x130 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "sio_cpu"; + power-domains = <&ps_sio>; + apple,always-on; + status = "disabled"; + }; + + ps_fpwm1: power-controller@140 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x140 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "fpwm1"; + power-domains = <&ps_sio>; + status = "disabled"; + }; + + ps_fpwm2: power-controller@148 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x148 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "fpwm2"; + power-domains = <&ps_sio>; + status = "disabled"; + }; + + ps_i2c0: power-controller@150 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x150 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "i2c0"; + power-domains = <&ps_sio>; + status = "disabled"; + }; + + ps_i2c5: power-controller@178 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x178 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "i2c5"; + power-domains = <&ps_sio>; + status = "disabled"; + }; + + ps_i2c7: power-controller@188 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x188 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "i2c7"; + power-domains = <&ps_sio>; + status = "disabled"; + }; + + ps_spi_p: power-controller@198 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x198 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "spi_p"; + power-domains = <&ps_sio>; + status = "disabled"; + }; + + ps_uart_p: power-controller@1a0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1a0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "uart_p"; + power-domains = <&ps_sio>; + status = "disabled"; + }; + + ps_dpa_p: power-controller@1a8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1a8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dpa_p"; + power-domains = <&ps_sio>; + apple,always-on; + status = "disabled"; + }; + + ps_aes: power-controller@1b0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1b0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "aes"; + power-domains = <&ps_sio>; + apple,always-on; + status = "disabled"; + }; + + ps_spi0: power-controller@1b8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1b8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "spi0"; + power-domains = <&ps_spi_p>; + status = "disabled"; + }; + + ps_spi1: power-controller@1c0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1c0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "spi1"; + power-domains = <&ps_spi_p>; + status = "disabled"; + }; + + ps_spi2: power-controller@1c8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1c8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "spi2"; + power-domains = <&ps_spi_p>; + status = "disabled"; + }; + + ps_spi3: power-controller@1d0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1d0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "spi3"; + power-domains = <&ps_spi_p>; + status = "disabled"; + }; + + ps_spi4: power-controller@1d8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1d8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "spi4"; + power-domains = <&ps_spi_p>; + status = "disabled"; + }; + + ps_spi5: power-controller@1e0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1e0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "spi5"; + power-domains = <&ps_spi_p>; + status = "disabled"; + }; + + ps_qspi: power-controller@1e8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1e8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "qspi"; + power-domains = <&ps_spi_p>; + status = "disabled"; + }; + + ps_uart_n: power-controller@1f0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1f0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "uart_n"; + power-domains = <&ps_uart_p>; + status = "disabled"; + }; + + ps_uart0: power-controller@1f8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1f8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "uart0"; + power-domains = <&ps_uart_p>; + status = "disabled"; + }; + + ps_uart1: power-controller@200 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x200 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "uart1"; + power-domains = <&ps_uart_p>; + status = "disabled"; + }; + + ps_uart2: power-controller@208 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x208 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "uart2"; + power-domains = <&ps_uart_p>; + status = "disabled"; + }; + + ps_uart3: power-controller@210 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x210 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "uart3"; + power-domains = <&ps_uart_p>; + status = "disabled"; + }; + + ps_uart4: power-controller@218 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x218 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "uart4"; + power-domains = <&ps_uart_p>; + status = "disabled"; + }; + + ps_uart5: power-controller@220 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x220 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "uart5"; + power-domains = <&ps_uart_p>; + status = "disabled"; + }; + + ps_uart6: power-controller@228 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x228 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "uart6"; + power-domains = <&ps_uart_p>; + status = "disabled"; + }; + + ps_dpa0: power-controller@230 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x230 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dpa0"; + power-domains = <&ps_dpa_p>; + status = "disabled"; + }; + + ps_dpa1: power-controller@238 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x238 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dpa1"; + power-domains = <&ps_dpa_p>; + status = "disabled"; + }; + + ps_dpa2: power-controller@240 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x240 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dpa2"; + power-domains = <&ps_dpa_p>; + status = "disabled"; + }; + + ps_dpa3: power-controller@248 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x248 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dpa3"; + power-domains = <&ps_dpa_p>; + status = "disabled"; + }; + + ps_dpa4: power-controller@250 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x250 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dpa4"; + power-domains = <&ps_dpa_p>; + status = "disabled"; + }; + + ps_disp_sys: power-controller@258 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x258 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "disp_sys"; + power-domains = <&ps_fab6_soc>; + status = "disabled"; + }; + + ps_amcc4: power-controller@260 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x260 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "amcc4"; + power-domains = <&ps_afc>; + apple,always-on; + status = "disabled"; + }; + + ps_amcc5: power-controller@268 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x268 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "amcc5"; + power-domains = <&ps_afc>; + apple,always-on; + status = "disabled"; + }; + + ps_amcc6: power-controller@270 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x270 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "amcc6"; + power-domains = <&ps_afc>; + apple,always-on; + status = "disabled"; + }; + + ps_amcc7: power-controller@278 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x278 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "amcc7"; + power-domains = <&ps_afc>; + apple,always-on; + status = "disabled"; + }; + + ps_amcc12: power-controller@280 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x280 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "amcc12"; + power-domains = <&ps_afc>; + apple,always-on; + status = "disabled"; + }; + + ps_amcc13: power-controller@288 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x288 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "amcc13"; + power-domains = <&ps_afc>; + apple,always-on; + status = "disabled"; + }; + + ps_amcc14: power-controller@290 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x290 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "amcc14"; + power-domains = <&ps_afc>; + apple,always-on; + status = "disabled"; + }; + + ps_amcc15: power-controller@298 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x298 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "amcc15"; + power-domains = <&ps_afc>; + apple,always-on; + status = "disabled"; + }; + + ps_dcs_04: power-controller@2e0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x2e0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dcs_04"; + power-domains = <&ps_amcc4>; + apple,always-on; + status = "disabled"; + }; + + ps_dcs_05: power-controller@2e8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x2e8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dcs_05"; + power-domains = <&ps_amcc5>; + apple,always-on; + status = "disabled"; + }; + + ps_dcs_06: power-controller@2f0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x2f0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dcs_06"; + power-domains = <&ps_amcc6>; + apple,always-on; + status = "disabled"; + }; + + ps_dcs_07: power-controller@2f8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x2f8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dcs_07"; + power-domains = <&ps_amcc7>; + apple,always-on; + status = "disabled"; + }; + + ps_dcs_12: power-controller@300 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x300 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dcs_12"; + power-domains = <&ps_amcc12>; + apple,always-on; + status = "disabled"; + }; + + ps_dcs_13: power-controller@308 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x308 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dcs_13"; + power-domains = <&ps_amcc13>; + apple,always-on; + status = "disabled"; + }; + + ps_dcs_14: power-controller@310 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x310 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dcs_14"; + power-domains = <&ps_amcc14>; + apple,always-on; + status = "disabled"; + }; + + ps_dcs_15: power-controller@318 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x318 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dcs_15"; + power-domains = <&ps_amcc15>; + apple,always-on; + status = "disabled"; + }; + + ps_disp_fe: power-controller@368 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x368 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "disp_fe"; + power-domains = <&ps_disp_sys>; + status = "disabled"; + }; + + ps_apcie_sys_gp: power-controller@390 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x390 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "apcie_sys_gp"; + power-domains = <&ps_fab3_soc>, <&ps_apcie_gp>; + status = "disabled"; + }; + + ps_apcie_phy_sw: power-controller@398 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x398 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "apcie_phy_sw"; + power-domains = <&ps_apcie_sys_st0>, <&ps_apcie_sys_st1>; + status = "disabled"; + }; + + ps_jpg: power-controller@3a0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x3a0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "jpg"; + power-domains = <&ps_fab4_soc>; + status = "disabled"; + }; + + ps_ane_sys: power-controller@3c0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x3c0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "ane_sys"; + power-domains = <&ps_fab5_soc>, <&ps_fab4_soc>; + status = "disabled"; + }; + + ps_sep: power-controller@c00 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0xc00 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "sep"; + power-domains = <&ps_fab3_soc>; + apple,always-on; + status = "disabled"; + }; +}; + +&pmgr_aux { + ps_ecpu_0: power-controller@0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "ecpu_0"; + status = "disabled"; + }; + + ps_ecpu_1: power-controller@8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "ecpu_1"; + status = "disabled"; + }; + + ps_ecpu_2: power-controller@10 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x10 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "ecpu_2"; + status = "disabled"; + }; + + ps_ecpu_3: power-controller@18 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x18 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "ecpu_3"; + status = "disabled"; + }; + + ps_pcpu0_0: power-controller@20 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x20 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "pcpu0_0"; + status = "disabled"; + }; + + ps_pcpu0_1: power-controller@28 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x28 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "pcpu0_1"; + status = "disabled"; + }; + + ps_pcpu0_2: power-controller@30 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x30 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "pcpu0_2"; + status = "disabled"; + }; + + ps_pcpu0_3: power-controller@38 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x38 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "pcpu0_3"; + status = "disabled"; + }; + + ps_pcpu0_4: power-controller@40 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x40 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "pcpu0_4"; + status = "disabled"; + }; + + ps_pcpu0_5: power-controller@48 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x48 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "pcpu0_5"; + status = "disabled"; + }; + + ps_pcpu1_0: power-controller@50 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x50 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "pcpu1_0"; + status = "disabled"; + }; + + ps_pcpu1_1: power-controller@58 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x58 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "pcpu1_1"; + status = "disabled"; + }; + + ps_pcpu1_2: power-controller@60 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x60 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "pcpu1_2"; + status = "disabled"; + }; + + ps_pcpu1_3: power-controller@68 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x68 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "pcpu1_3"; + status = "disabled"; + }; + + ps_pcpu1_4: power-controller@70 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x70 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "pcpu1_4"; + status = "disabled"; + }; + + ps_pcpu1_5: power-controller@78 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x78 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "pcpu1_5"; + status = "disabled"; + }; + + ps_ecpm: power-controller@80 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x80 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "ecpm"; + status = "disabled"; + }; + + ps_pcpm0: power-controller@88 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x88 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "pcpm0"; + status = "disabled"; + }; + + ps_pcpm1: power-controller@90 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x90 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "pcpm1"; + status = "disabled"; + }; + + ps_dwi: power-controller@108 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x108 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dwi"; + status = "disabled"; + }; + + ps_aic: power-controller@118 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x118 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "aic"; + power-domains = <&ps_pms>; + status = "disabled"; + }; + + ps_pmc: power-controller@120 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x120 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "pmc"; + power-domains = <&ps_pms>; + apple,always-on; + status = "disabled"; + }; + + ps_pms_c1ppt: power-controller@128 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x128 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "pms_c1ppt"; + power-domains = <&ps_pms>; + apple,always-on; + status = "disabled"; + }; + + ps_soc_dpe: power-controller@130 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x130 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "soc_dpe"; + power-domains = <&ps_pms>; + apple,always-on; + status = "disabled"; + }; + + ps_pmgr_soc_ocla: power-controller@138 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x138 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "pmgr_soc_ocla"; + status = "disabled"; + }; + + ps_pms_fpwm0: power-controller@140 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x140 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "pms_fpwm0"; + power-domains = <&ps_pms>; + apple,always-on; + status = "disabled"; + }; + + ps_pms_fpwm1: power-controller@148 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x148 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "pms_fpwm1"; + power-domains = <&ps_pms>; + apple,always-on; + status = "disabled"; + }; + + ps_pms_fpwm2: power-controller@150 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x150 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "pms_fpwm2"; + power-domains = <&ps_pms>; + apple,always-on; + status = "disabled"; + }; + + ps_pms_fpwm3: power-controller@158 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x158 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "pms_fpwm3"; + power-domains = <&ps_pms>; + apple,always-on; + status = "disabled"; + }; + + ps_pms_fpwm4: power-controller@160 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x160 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "pms_fpwm4"; + power-domains = <&ps_pms>; + apple,always-on; + status = "disabled"; + }; + + ps_fab0_soc: power-controller@168 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x168 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "fab0_soc"; + power-domains = <&ps_afi>; + apple,always-on; + status = "disabled"; + }; + + ps_amcc0: power-controller@180 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x180 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "amcc0"; + power-domains = <&ps_afc>; + apple,always-on; + status = "disabled"; + }; + + ps_dcs_00: power-controller@188 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x188 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dcs_00"; + power-domains = <&ps_amcc0>; + apple,always-on; + status = "disabled"; + }; + + ps_rom: power-controller@190 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x190 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "rom"; + power-domains = <&ps_amcc0>; + status = "disabled"; + }; + + ps_amcc1: power-controller@198 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x198 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "amcc1"; + power-domains = <&ps_afc>; + apple,always-on; + status = "disabled"; + }; + + ps_dcs_01: power-controller@1a0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1a0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dcs_01"; + power-domains = <&ps_amcc1>; + apple,always-on; + status = "disabled"; + }; + + ps_amcc2: power-controller@1a8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1a8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "amcc2"; + power-domains = <&ps_afc>; + apple,always-on; + status = "disabled"; + }; + + ps_dcs_02: power-controller@1b0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1b0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dcs_02"; + power-domains = <&ps_amcc2>; + apple,always-on; + status = "disabled"; + }; + + ps_amcc3: power-controller@1b8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1b8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "amcc3"; + power-domains = <&ps_afc>; + apple,always-on; + status = "disabled"; + }; + + ps_dcs_03: power-controller@1c0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1c0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dcs_03"; + power-domains = <&ps_amcc3>; + apple,always-on; + status = "disabled"; + }; + + ps_amcc8: power-controller@1c8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1c8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "amcc8"; + power-domains = <&ps_afc>; + apple,always-on; + status = "disabled"; + }; + + ps_dcs_08: power-controller@1d0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1d0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dcs_08"; + power-domains = <&ps_amcc8>; + apple,always-on; + status = "disabled"; + }; + + ps_amcc9: power-controller@1d8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1d8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "amcc9"; + power-domains = <&ps_afc>; + apple,always-on; + status = "disabled"; + }; + + ps_dcs_09: power-controller@1e0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1e0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dcs_09"; + power-domains = <&ps_amcc9>; + apple,always-on; + status = "disabled"; + }; + + ps_amcc10: power-controller@1e8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1e8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "amcc10"; + power-domains = <&ps_afc>; + apple,always-on; + status = "disabled"; + }; + + ps_dcs_10: power-controller@1f0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1f0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dcs_10"; + power-domains = <&ps_amcc10>; + apple,always-on; + status = "disabled"; + }; + + ps_amcc11: power-controller@1f8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x1f8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "amcc11"; + power-domains = <&ps_afc>; + apple,always-on; + status = "disabled"; + }; + + ps_dcs_11: power-controller@200 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x200 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dcs_11"; + power-domains = <&ps_amcc11>; + apple,always-on; + status = "disabled"; + }; + + ps_venc0_sys: power-controller@278 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x278 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "venc0_sys"; + power-domains = <&ps_fab0_soc>; + status = "disabled"; + }; + + ps_fab_gw_0: power-controller@288 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x288 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "fab_gw_0"; + apple,always-on; + status = "disabled"; + }; + + ps_fab_gw_1: power-controller@290 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x290 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "fab_gw_1"; + apple,always-on; + status = "disabled"; + }; + + ps_fab_afr: power-controller@298 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x298 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "fab_afr"; + apple,always-on; + status = "disabled"; + }; + + ps_fab1_soc: power-controller@2d0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x2d0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "fab1_soc"; + power-domains = <&ps_afi>; + apple,always-on; + status = "disabled"; + }; + + ps_scodec: power-controller@328 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x328 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "scodec"; + power-domains = <&ps_fab1_soc>; + status = "disabled"; + }; + + ps_scodec_streaming: power-controller@330 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x330 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "scodec_streaming"; + power-domains = <&ps_scodec>; + status = "disabled"; + }; + + ps_fab2_soc: power-controller@338 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x338 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "fab2_soc"; + power-domains = <&ps_afi>; + apple,always-on; + status = "disabled"; + }; + + ps_dispext0_sys: power-controller@340 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x340 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dispext0_sys"; + power-domains = <&ps_fab2_soc>; + status = "disabled"; + }; + + ps_dispext0_fe: power-controller@348 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x348 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dispext0_fe"; + power-domains = <&ps_dispext0_sys>; + status = "disabled"; + }; + + ps_dispext0_cpu: power-controller@350 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x350 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dispext0_cpu"; + power-domains = <&ps_dispext0_fe>; + status = "disabled"; + }; + + ps_atc0_cio: power-controller@398 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x398 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc0_cio"; + power-domains = <&ps_atc0_common>; + status = "disabled"; + }; + + ps_atc0_pcie: power-controller@3a0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x3a0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc0_pcie"; + power-domains = <&ps_atc0_common>; + status = "disabled"; + }; + + ps_atc0_cio_pcie: power-controller@3a8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x3a8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc0_cio_pcie"; + power-domains = <&ps_atc0_cio>; + status = "disabled"; + }; + + ps_atc0_cio_usb: power-controller@3b0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x3b0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc0_cio_usb"; + power-domains = <&ps_atc0_cio>; + status = "disabled"; + }; + + ps_atc1_cio: power-controller@3c0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x3c0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc1_cio"; + power-domains = <&ps_atc1_common>; + status = "disabled"; + }; + + ps_atc1_pcie: power-controller@3c8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x3c8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc1_pcie"; + power-domains = <&ps_atc1_common>; + status = "disabled"; + }; + + ps_atc1_cio_pcie: power-controller@3d0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x3d0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc1_cio_pcie"; + power-domains = <&ps_atc1_cio>; + status = "disabled"; + }; + + ps_atc1_cio_usb: power-controller@3d8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x3d8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc1_cio_usb"; + power-domains = <&ps_atc1_cio>; + status = "disabled"; + }; + + ps_atc2_cio: power-controller@3e8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x3e8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc2_cio"; + power-domains = <&ps_atc2_common>; + status = "disabled"; + }; + + ps_atc2_pcie: power-controller@3f0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x3f0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc2_pcie"; + power-domains = <&ps_atc2_common>; + status = "disabled"; + }; + + ps_atc2_cio_pcie: power-controller@3f8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x3f8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc2_cio_pcie"; + power-domains = <&ps_atc2_cio>; + status = "disabled"; + }; + + ps_atc2_cio_usb: power-controller@400 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x400 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc2_cio_usb"; + power-domains = <&ps_atc2_cio>; + status = "disabled"; + }; + + ps_atc3_common: power-controller@408 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x408 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc3_common"; + power-domains = <&ps_fab2_soc>; + status = "disabled"; + }; + + ps_atc3_cio: power-controller@420 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x420 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc3_cio"; + power-domains = <&ps_atc3_common>; + status = "disabled"; + }; + + ps_atc3_pcie: power-controller@428 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x428 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc3_pcie"; + power-domains = <&ps_atc3_common>; + status = "disabled"; + }; + + ps_atc3_cio_pcie: power-controller@430 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x430 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc3_cio_pcie"; + power-domains = <&ps_atc3_cio>; + status = "disabled"; + }; + + ps_atc3_cio_usb: power-controller@438 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x438 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc3_cio_usb"; + power-domains = <&ps_atc3_cio>; + status = "disabled"; + }; + + ps_fab5_soc: power-controller@440 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x440 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "fab5_soc"; + power-domains = <&ps_afi>; + apple,always-on; + status = "disabled"; + }; + + ps_sbr: power-controller@448 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x448 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "sbr"; + power-domains = <&ps_fab5_soc>; + apple,always-on; + status = "disabled"; + }; + + ps_gpio: power-controller@450 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x450 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "gpio"; + power-domains = <&ps_sbr>; + status = "disabled"; + }; + + ps_msg: power-controller@458 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x458 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "msg"; + power-domains = <&ps_sbr>; + status = "disabled"; + }; + + ps_ap_tmm: power-controller@468 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x468 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "ap_tmm"; + power-domains = <&ps_sbr>; + status = "disabled"; + }; + + ps_msr0: power-controller@470 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x470 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "msr0"; + power-domains = <&ps_fab5_soc>; + status = "disabled"; + }; + + ps_msr0_ase_core: power-controller@478 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x478 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "msr0_ase_core"; + power-domains = <&ps_msr0>; + status = "disabled"; + }; + + ps_fab7_soc: power-controller@488 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x488 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "fab7_soc"; + power-domains = <&ps_afi>; + apple,always-on; + status = "disabled"; + }; + + ps_dispext1_sys: power-controller@490 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x490 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dispext1_sys"; + power-domains = <&ps_fab7_soc>; + status = "disabled"; + }; + + ps_dispext1_fe: power-controller@498 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x498 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dispext1_fe"; + power-domains = <&ps_dispext1_sys>; + status = "disabled"; + }; + + ps_dispext1_cpu: power-controller@4a0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x4a0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dispext1_cpu"; + power-domains = <&ps_dispext1_fe>; + status = "disabled"; + }; +}; + +&pmgr_ane { + ps_ane_mpm: power-controller@0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "ane_mpm"; + power-domains = <&ps_ane_sys>; + status = "disabled"; + }; + + ps_ane_cpu: power-controller@8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "ane_cpu"; + power-domains = <&ps_ane_sys>; + status = "disabled"; + }; + + ps_ane_td: power-controller@10 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x10 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "ane_td"; + power-domains = <&ps_ane_sys>; + status = "disabled"; + }; + + ps_ane_base: power-controller@18 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x18 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "ane_base"; + power-domains = <&ps_ane_td>; + status = "disabled"; + }; +}; + +&pmgr_mini { + ps_debug_gated: power-controller@0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "debug_gated"; + apple,always-on; + status = "disabled"; + }; + + ps_nub_spmi_ahb: power-controller@58 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x58 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "nub_spmi_ahb"; + apple,always-on; + status = "disabled"; + }; + + ps_nub_spmi0: power-controller@60 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x60 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "nub_spmi0"; + power-domains = <&ps_nub_spmi_ahb>; + apple,always-on; + status = "disabled"; + }; + + ps_nub_spmi_a2: power-controller@78 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x78 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "nub_spmi_a2"; + apple,always-on; + status = "disabled"; + }; + + ps_nub_spi0: power-controller@80 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x80 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "nub_spi0"; + apple,always-on; + status = "disabled"; + }; + + ps_nub_aon: power-controller@88 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x88 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "nub_aon"; + apple,always-on; + status = "disabled"; + }; + + ps_nub_ocla: power-controller@98 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x98 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "nub_ocla"; + apple,always-on; + status = "disabled"; + }; + + ps_atc3_common_dp: power-controller@b8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0xb8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc3_common_dp"; + status = "disabled"; + }; + + ps_nub_spmi1: power-controller@c0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0xc0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "nub_spmi1"; + power-domains = <&ps_nub_spmi_ahb>; + apple,always-on; + status = "disabled"; + }; + + ps_atc3_usb_aon: power-controller@e0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0xe0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc3_usb_aon"; + status = "disabled"; + }; + + ps_nub_spmi2: power-controller@e8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0xe8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "nub_spmi2"; + power-domains = <&ps_nub_spmi_ahb>; + apple,always-on; + status = "disabled"; + }; + + ps_nub_spmi3: power-controller@f0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0xf0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "nub_spmi3"; + power-domains = <&ps_nub_spmi_ahb>; + apple,always-on; + status = "disabled"; + }; + + ps_nub_spmi4: power-controller@f8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0xf8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "nub_spmi4"; + power-domains = <&ps_nub_spmi_ahb>; + apple,always-on; + status = "disabled"; + }; + + ps_nub_fabric: power-controller@100 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x100 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "nub_fabric"; + apple,always-on; + status = "disabled"; + }; + + ps_nub_sram: power-controller@108 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x108 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "nub_sram"; + power-domains = <&ps_nub_fabric>; + apple,always-on; + status = "disabled"; + }; + + ps_debug_switch: power-controller@110 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x110 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "debug_switch"; + apple,always-on; + status = "disabled"; + }; + + ps_atc3_usb: power-controller@148 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x148 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc3_usb"; + power-domains = <&ps_atc3_common>, <&ps_atc3_usb_aon>; + status = "disabled"; + }; + + ps_atc3_phymxwrap: power-controller@168 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x168 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc3_phymxwrap"; + power-domains = <&ps_atc3_common>, <&ps_atc3_usb_aon>; + status = "disabled"; + }; +}; + +&pmgr_disp { + ps_disp_cpu: power-controller@0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "disp_cpu"; + power-domains = <&ps_disp_fe>; + status = "disabled"; + }; +}; + +&pmgr_gfx { + ps_gpx: power-controller@0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "gpx"; + apple,always-on; + status = "disabled"; + }; +}; + +&pmgr_venc { + ps_venc0_dma: power-controller@0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "venc0_dma"; + power-domains = <&ps_venc0_sys>; + status = "disabled"; + }; + + ps_venc0_pipe4: power-controller@8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "venc0_pipe4"; + power-domains = <&ps_venc0_dma>; + status = "disabled"; + }; + + ps_venc0_pipe5: power-controller@10 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x10 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "venc0_pipe5"; + power-domains = <&ps_venc0_dma>; + status = "disabled"; + }; + + ps_venc0_me0: power-controller@18 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x18 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "venc0_me0"; + power-domains = <&ps_venc0_dma>; + status = "disabled"; + }; + + ps_venc0_me1: power-controller@20 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x20 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "venc0_me1"; + power-domains = <&ps_venc0_me0>; + status = "disabled"; + }; +}; diff --git a/arch/arm64/boot/dts/apple/t6040.dtsi b/arch/arm64/boot/dts/apple/t6040.dtsi index c054f5accf531..ba79030262ad6 100644 --- a/arch/arm64/boot/dts/apple/t6040.dtsi +++ b/arch/arm64/boot/dts/apple/t6040.dtsi @@ -1030,6 +1030,27 @@ /* child nodes are added in t6040-pmgr.dtsi */ }; + pmgr_ane: power-management@50228c000 { + compatible = "apple,t6041-pmgr", "apple,t8103-pmgr", "syscon", "simple-mfd"; + reg = <0x5 0x0228c000 0x0 0x4000>; + #address-cells = <1>; + #size-cells = <1>; + }; + + pmgr_disp: power-management@502290000 { + compatible = "apple,t6041-pmgr", "apple,t8103-pmgr", "syscon", "simple-mfd"; + reg = <0x5 0x02290000 0x0 0x4000>; + #address-cells = <1>; + #size-cells = <1>; + }; + + pmgr_venc: power-management@502808000 { + compatible = "apple,t6041-pmgr", "apple,t8103-pmgr", "syscon", "simple-mfd"; + reg = <0x5 0x02808000 0x0 0x4000>; + #address-cells = <1>; + #size-cells = <1>; + }; + pmgr_isp: power-management@502284000 { compatible = "apple,t6041-pmgr", "apple,t8103-pmgr", "syscon", "simple-mfd"; #address-cells = <1>; From 7a2b3a73e6f6516cf6bc88add0d0b5fe6a34a363 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 15:04:10 +1100 Subject: [PATCH 327/388] arm64: dts: apple: describe the remaining T6040 DART banks Describe the separate ANE and AVE translation banks, PCIe port 1, SIO, external displays, scaler, APR and JPEG IOMMUs. Keep each disabled until its DMA consumer owns its power and streams. Shared DAPF and remap apertures are not independent IOMMU instances. Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/boot/dts/apple/t6040.dtsi | 173 +++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/arch/arm64/boot/dts/apple/t6040.dtsi b/arch/arm64/boot/dts/apple/t6040.dtsi index ba79030262ad6..fb4cde463cc2d 100644 --- a/arch/arm64/boot/dts/apple/t6040.dtsi +++ b/arch/arm64/boot/dts/apple/t6040.dtsi @@ -1808,6 +1808,179 @@ phy-names = "usb2-phy", "usb3-phy"; status = "disabled"; }; + /* Additional DARTs; enabled together with their DMA consumers. */ + ave0_dart1: iommu@283080000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x2 0x83080000 0x0 0xc000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + power-domains = <&ps_venc0_sys>; + status = "disabled"; + }; + + ave0_dart0: iommu@2830a0000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x2 0x830a0000 0x0 0xc000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + power-domains = <&ps_venc0_sys>; + status = "disabled"; + }; + + apr0_dart: iommu@301020000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x3 0x01020000 0x0 0xc000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + power-domains = <&ps_scodec>; + status = "disabled"; + }; + + apciec0_dart: iommu@384000000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x3 0x84000000 0x0 0x20000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + status = "disabled"; + }; + + apciec1_dart: iommu@38c000000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x3 0x8c000000 0x0 0x20000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + status = "disabled"; + }; + + apciec2_dart: iommu@394000000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x3 0x94000000 0x0 0x20000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + status = "disabled"; + }; + + dispext0_dart: iommu@3a2300000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x3 0xa2300000 0x0 0xc000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + status = "disabled"; + }; + + dcpext0_dart: iommu@3a2340000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x3 0xa2340000 0x0 0x20000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + status = "disabled"; + }; + + apcie1_dart: iommu@411000000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x4 0x11000000 0x0 0x20000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + status = "disabled"; + }; + + sio_dart: iommu@429060000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x4 0x29060000 0x0 0x20000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + power-domains = <&ps_sio>; + status = "disabled"; + }; + + ane0_dart0: iommu@485800000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x4 0x85800000 0x0 0xc000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + power-domains = <&ps_ane_sys>; + status = "disabled"; + }; + + ane0_dart1: iommu@485820000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x4 0x85820000 0x0 0xc000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + power-domains = <&ps_ane_sys>; + status = "disabled"; + }; + + ane0_dart2: iommu@485840000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x4 0x85840000 0x0 0xc000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + power-domains = <&ps_ane_sys>; + status = "disabled"; + }; + + jpeg0_dart: iommu@487020000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x4 0x87020000 0x0 0xc000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + power-domains = <&ps_jpg>; + status = "disabled"; + }; + + jpeg1_dart: iommu@487080000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x4 0x87080000 0x0 0xc000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + power-domains = <&ps_jpg>; + status = "disabled"; + }; + + scaler0_dart: iommu@521260000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x5 0x21260000 0x0 0xc000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + power-domains = <&ps_msr0>; + status = "disabled"; + }; + + dispext1_dart: iommu@602300000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x6 0x02300000 0x0 0xc000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + status = "disabled"; + }; + + dcpext1_dart: iommu@602340000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x6 0x02340000 0x0 0xc000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + status = "disabled"; + }; + }; #include "t6040-pmgr.dtsi" From 7d819964d0bcf133a082dddc8caba8f09ad361bb Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 15:06:22 +1100 Subject: [PATCH 328/388] arm64: dts: apple: describe the remaining T6040 GPIO and SPMI controllers Add the AOP and SMC pin banks with the ADT pin counts and interrupt groups, and the four remaining NUB SPMI buses plus the AOP SPMI bus. Leave coprocessor-owned banks disabled until their ownership can be transferred. Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/boot/dts/apple/t6040.dtsi | 89 ++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/arch/arm64/boot/dts/apple/t6040.dtsi b/arch/arm64/boot/dts/apple/t6040.dtsi index fb4cde463cc2d..0a2d7104a103b 100644 --- a/arch/arm64/boot/dts/apple/t6040.dtsi +++ b/arch/arm64/boot/dts/apple/t6040.dtsi @@ -1981,6 +1981,95 @@ status = "disabled"; }; + pinctrl_aop: pinctrl@510824000 { + compatible = "apple,t6041-pinctrl", "apple,t8103-pinctrl"; + reg = <0x5 0x10824000 0x0 0x4000>; + clocks = <&clkref>; + gpio-controller; + #gpio-cells = <2>; + gpio-ranges = <&pinctrl_aop 0 0 85>; + apple,npins = <85>; + interrupt-controller; + #interrupt-cells = <2>; + interrupt-parent = <&aic>; + interrupts = , + , + , + , + , + , + ; + /* Coprocessor pins and interrupt groups need an ownership handoff. */ + status = "disabled"; + }; + + pinctrl_smc: pinctrl@50c824000 { + compatible = "apple,t6041-pinctrl", "apple,t8103-pinctrl"; + reg = <0x5 0x0c824000 0x0 0x4000>; + clocks = <&clkref>; + gpio-controller; + #gpio-cells = <2>; + gpio-ranges = <&pinctrl_smc 0 0 24>; + apple,npins = <24>; + interrupt-controller; + #interrupt-cells = <2>; + interrupt-parent = <&aic>; + interrupts = , + , + , + , + , + , + ; + /* Coprocessor pins and interrupt groups need an ownership handoff. */ + status = "disabled"; + }; + + nub_spmi1: spmi@509054000 { + compatible = "apple,t6041-spmi", "apple,t8103-spmi"; + reg = <0x5 0x09054000 0x0 0x100>; + #address-cells = <2>; + #size-cells = <0>; + power-domains = <&ps_nub_spmi1>; + status = "disabled"; + }; + + nub_spmi2: spmi@509094000 { + compatible = "apple,t6041-spmi", "apple,t8103-spmi"; + reg = <0x5 0x09094000 0x0 0x100>; + #address-cells = <2>; + #size-cells = <0>; + power-domains = <&ps_nub_spmi2>; + status = "disabled"; + }; + + nub_spmi3: spmi@5090d4000 { + compatible = "apple,t6041-spmi", "apple,t8103-spmi"; + reg = <0x5 0x090d4000 0x0 0x100>; + #address-cells = <2>; + #size-cells = <0>; + power-domains = <&ps_nub_spmi3>; + status = "disabled"; + }; + + nub_spmi4: spmi@509114000 { + compatible = "apple,t6041-spmi", "apple,t8103-spmi"; + reg = <0x5 0x09114000 0x0 0x100>; + #address-cells = <2>; + #size-cells = <0>; + power-domains = <&ps_nub_spmi4>; + status = "disabled"; + }; + + aop_spmi0: spmi@510914000 { + compatible = "apple,t6041-spmi", "apple,t8103-spmi"; + reg = <0x5 0x10914000 0x0 0x100>; + #address-cells = <2>; + #size-cells = <0>; + /* Shared with AOP firmware; no AP bus ownership handoff yet. */ + status = "disabled"; + }; + }; #include "t6040-pmgr.dtsi" From c5b1a0547e5ab38add5caa5374eb87fcd982edb8 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 15:06:33 +1100 Subject: [PATCH 329/388] arm64: dts: apple: describe the J616s additional Abbey PMICs Expose the Abbey PMICs at USID 14 on NUB SPMI buses 1 and 2 through the existing NVMEM binding. Enable their bus power states and shared parent. Use polled bus access as on NUB SPMI0 so firmware-owned peripheral interrupts remain untouched. Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/boot/dts/apple/t6040-j616s.dts | 30 +++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/arch/arm64/boot/dts/apple/t6040-j616s.dts b/arch/arm64/boot/dts/apple/t6040-j616s.dts index 69a891d7111d5..5fa132fea4fb3 100644 --- a/arch/arm64/boot/dts/apple/t6040-j616s.dts +++ b/arch/arm64/boot/dts/apple/t6040-j616s.dts @@ -4370,3 +4370,33 @@ #include "t6040-j616s-extdisp-mem-loader.dtsi" #include "t6040-j616s-extdisp-hdmi.dtsi" #include "t6040-j616s-extdisp-usbc.dtsi" +/* Additional Abbey PMICs on the two independent NUB SPMI buses. */ +&nub_spmi1 { + status = "okay"; + + pmic@e { + compatible = "apple,abbey-pmic", "apple,spmi-nvmem"; + reg = <0xe SPMI_USID>; + }; +}; + +&nub_spmi2 { + status = "okay"; + + pmic@e { + compatible = "apple,abbey-pmic", "apple,spmi-nvmem"; + reg = <0xe SPMI_USID>; + }; +}; + +&ps_nub_spmi_ahb { + status = "okay"; +}; + +&ps_nub_spmi1 { + status = "okay"; +}; + +&ps_nub_spmi2 { + status = "okay"; +}; From 4766298ee81470cc6284facdb967a2ccd3314f67 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 15:09:30 +1100 Subject: [PATCH 330/388] arm64: dts: apple: describe T6040 SIO, panel SPI and QSPIMC Describe the SIO DMA coprocessor and its mailbox, panel SPI controller and firmware NOR controller with their register, interrupt, clock and power resources. Extend the SIO and QSPIMC bindings for T6041. Keep them disabled pending firmware-table, panel-ownership and flash-write-policy qualification. Assisted-by: LLM Signed-off-by: Ryan Murray --- .../devicetree/bindings/dma/apple,sio.yaml | 1 + .../devicetree/bindings/spi/apple,qspi.yaml | 1 + arch/arm64/boot/dts/apple/t6040.dtsi | 61 +++++++++++++++++++ 3 files changed, 63 insertions(+) diff --git a/Documentation/devicetree/bindings/dma/apple,sio.yaml b/Documentation/devicetree/bindings/dma/apple,sio.yaml index dbaaa2d3d1a1a..ab6bdf3686971 100644 --- a/Documentation/devicetree/bindings/dma/apple,sio.yaml +++ b/Documentation/devicetree/bindings/dma/apple,sio.yaml @@ -23,6 +23,7 @@ properties: - enum: - apple,t6000-sio - apple,t6030-sio + - apple,t6041-sio - apple,t8103-sio - const: apple,sio diff --git a/Documentation/devicetree/bindings/spi/apple,qspi.yaml b/Documentation/devicetree/bindings/spi/apple,qspi.yaml index 39924afc9a8b6..0b98af1ad68d2 100644 --- a/Documentation/devicetree/bindings/spi/apple,qspi.yaml +++ b/Documentation/devicetree/bindings/spi/apple,qspi.yaml @@ -22,6 +22,7 @@ properties: enum: - apple,t8122-qspi - apple,t8132-qspi + - apple,t6041-qspi - apple,t8140-qspi reg: diff --git a/arch/arm64/boot/dts/apple/t6040.dtsi b/arch/arm64/boot/dts/apple/t6040.dtsi index 0a2d7104a103b..1be0417d0a8b8 100644 --- a/arch/arm64/boot/dts/apple/t6040.dtsi +++ b/arch/arm64/boot/dts/apple/t6040.dtsi @@ -2070,6 +2070,67 @@ status = "disabled"; }; + /* The QSPIMC input is ADT clock 434 (200 MHz). */ + clk_200m: clock-200m { + compatible = "fixed-clock"; + #clock-cells = <0>; + clock-frequency = <200000000>; + clock-output-names = "clk_200m"; + }; + + spi4: spi@429110000 { + compatible = "apple,t6041-spi", "apple,t8103-spi"; + reg = <0x4 0x29110000 0x0 0x4000>; + interrupt-parent = <&aic>; + interrupts = ; + clocks = <&clkref>; + power-domains = <&ps_spi4>; + #address-cells = <1>; + #size-cells = <0>; + /* Panel/TCON control belongs to the display firmware. */ + status = "disabled"; + }; + + qspi: spi@429118000 { + compatible = "apple,t6041-qspi"; + reg = <0x4 0x29118000 0x0 0x4800>; + interrupt-parent = <&aic>; + interrupts = ; + clocks = <&clk_200m>; + power-domains = <&ps_qspi>; + #address-cells = <1>; + #size-cells = <0>; + /* The firmware NOR needs a qualified partition/write policy. */ + status = "disabled"; + }; + + /* The loader supplies the SIO firmware regions and protocol tables. */ + sio: dma-controller@429e00000 { + compatible = "apple,t6041-sio", "apple,sio"; + reg = <0x4 0x29e00000 0x0 0x8000>; + dma-channels = <128>; + #dma-cells = <1>; + mboxes = <&sio_mbox>; + iommus = <&sio_dart 0>; + power-domains = <&ps_sio_cpu>; + status = "disabled"; + }; + + sio_mbox: mailbox@429e08000 { + compatible = "apple,t6041-asc-mailbox", "apple,asc-mailbox-v4"; + reg = <0x4 0x29e08000 0x0 0x4000>; + interrupt-parent = <&aic>; + interrupts = , + , + , + ; + interrupt-names = "send-empty", "send-not-empty", + "recv-empty", "recv-not-empty"; + #mbox-cells = <0>; + power-domains = <&ps_sio_cpu>; + status = "disabled"; + }; + }; #include "t6040-pmgr.dtsi" From 6d9b58b2fe74cb67585e34c5253d3fde4b373f73 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 15:14:05 +1100 Subject: [PATCH 331/388] arm64: dts: apple: describe the T6040 Neural Engine Describe the engine, its interrupt and three DART translation banks. Extend the ANE binding for the single-interrupt T6041 firmware interface and leave the engine disabled until that interface has a Linux backend. Assisted-by: LLM Signed-off-by: Ryan Murray --- .../devicetree/bindings/npu/apple,ane.yaml | 27 ++++++++++++++++--- arch/arm64/boot/dts/apple/t6040.dtsi | 12 +++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/Documentation/devicetree/bindings/npu/apple,ane.yaml b/Documentation/devicetree/bindings/npu/apple,ane.yaml index 79cf942654f7b..feae5b133ac2f 100644 --- a/Documentation/devicetree/bindings/npu/apple,ane.yaml +++ b/Documentation/devicetree/bindings/npu/apple,ane.yaml @@ -15,7 +15,9 @@ description: drives the engine through its register window. On T602x and T8112 a coprocessor runs the ANE firmware and the host talks to it through an ASC mailbox. T8140 has two interrupts and one system power domain; its - coprocessor uses a different firmware interface. + coprocessor uses a different firmware interface. T6041 likewise needs a + generation-specific firmware backend; it has one interrupt and its CPU + power state supplies the engine through the ANE system domain. properties: compatible: @@ -25,6 +27,7 @@ properties: - apple,t6020-ane - apple,t6021-ane - apple,t6022-ane + - apple,t6041-ane - apple,t8112-ane - apple,t8140-ane @@ -87,19 +90,35 @@ required: - power-domains allOf: + - if: + properties: + compatible: + const: apple,t8140-ane + then: + properties: + interrupts: + minItems: 2 + - if: + properties: + compatible: + const: apple,t6041-ane + then: + properties: + interrupts: + maxItems: 1 - if: properties: compatible: contains: - const: apple,t8140-ane + enum: + - apple,t8140-ane + - apple,t6041-ane then: properties: reg: maxItems: 1 reg-names: maxItems: 1 - interrupts: - minItems: 2 interrupt-names: false iommus: minItems: 3 diff --git a/arch/arm64/boot/dts/apple/t6040.dtsi b/arch/arm64/boot/dts/apple/t6040.dtsi index 1be0417d0a8b8..dd58eab792838 100644 --- a/arch/arm64/boot/dts/apple/t6040.dtsi +++ b/arch/arm64/boot/dts/apple/t6040.dtsi @@ -2131,6 +2131,18 @@ status = "disabled"; }; + /* Firmware interface support is required before enabling the ANE. */ + ane: ane@484000000 { + compatible = "apple,t6041-ane"; + reg = <0x4 0x84000000 0x0 0x2000000>; + reg-names = "engine"; + interrupt-parent = <&aic>; + interrupts = ; + iommus = <&ane0_dart0 0>, <&ane0_dart1 0>, <&ane0_dart2 0>; + power-domains = <&ps_ane_cpu>; + status = "disabled"; + }; + }; #include "t6040-pmgr.dtsi" From cc9f3a23e3972b456cf5161f74a984f034ff68c8 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 15:14:14 +1100 Subject: [PATCH 332/388] arm64: dts: apple: describe the T6040 video encoder Describe the codec, mailbox and coprocessor windows, all five interrupts, the five hardware power states and both DART banks using the AVE binding. Keep it disabled until firmware support is available. Assisted-by: LLM Signed-off-by: Ryan Murray --- .../bindings/media/apple,t8140-ave.yaml | 8 +++++--- arch/arm64/boot/dts/apple/t6040.dtsi | 20 +++++++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/Documentation/devicetree/bindings/media/apple,t8140-ave.yaml b/Documentation/devicetree/bindings/media/apple,t8140-ave.yaml index 110ebbc3d09e2..61e10feb5df71 100644 --- a/Documentation/devicetree/bindings/media/apple,t8140-ave.yaml +++ b/Documentation/devicetree/bindings/media/apple,t8140-ave.yaml @@ -4,19 +4,21 @@ $id: http://devicetree.org/schemas/media/apple,t8140-ave.yaml# $schema: http://devicetree.org/meta-schemas/core.yaml# -title: Apple T8140 video encoder +title: Apple video encoder maintainers: - Miguel Cruz <7362641+Acelogic@users.noreply.github.com> description: | - The T8140 AVE coprocessor encodes video using firmware preloaded by the + The T6041 and T8140 AVE coprocessors encode video using firmware preloaded by the bootloader. Its two DART instances translate the same stream. The bootloader supplies two reserved firmware regions and their IOVA mappings. properties: compatible: - const: apple,t8140-ave + enum: + - apple,t6041-ave + - apple,t8140-ave reg: items: diff --git a/arch/arm64/boot/dts/apple/t6040.dtsi b/arch/arm64/boot/dts/apple/t6040.dtsi index dd58eab792838..f113708592ddf 100644 --- a/arch/arm64/boot/dts/apple/t6040.dtsi +++ b/arch/arm64/boot/dts/apple/t6040.dtsi @@ -2143,6 +2143,26 @@ status = "disabled"; }; + /* The video encoder stays disabled until its firmware backend is available. */ + ave: video-codec@283100000 { + compatible = "apple,t6041-ave"; + reg = <0x2 0x83100000 0x0 0x45c000>, + <0x2 0x83050000 0x0 0x4000>, + <0x2 0x83800000 0x0 0x800000>; + reg-names = "codec", "mailbox", "coproc"; + interrupt-parent = <&aic>; + interrupts = , + , + , + , + ; + power-domains = <&ps_venc0_dma>, <&ps_venc0_pipe4>, + <&ps_venc0_pipe5>, <&ps_venc0_me0>, <&ps_venc0_me1>; + power-domain-names = "dma", "pipe4", "pipe5", "me0", "me1"; + iommus = <&ave0_dart0 0>, <&ave0_dart1 0>; + status = "disabled"; + }; + }; #include "t6040-pmgr.dtsi" From 8640ed5980bc2373bc822536e3aba88ea049ad5d Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 15:14:43 +1100 Subject: [PATCH 333/388] arm64: dts: apple: describe the J616s SD reader PCIe port Describe port 1, its interrupt, reset and clock-request pins and its separate DART mapping. Leave the port disabled because its SMC power switch is not a hexadecimal gPxx GPIO and requires a separate service binding. Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/boot/dts/apple/t6040-j616s.dts | 33 +++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/arch/arm64/boot/dts/apple/t6040-j616s.dts b/arch/arm64/boot/dts/apple/t6040-j616s.dts index 5fa132fea4fb3..f4e37feca2475 100644 --- a/arch/arm64/boot/dts/apple/t6040-j616s.dts +++ b/arch/arm64/boot/dts/apple/t6040-j616s.dts @@ -4400,3 +4400,36 @@ &ps_nub_spmi2 { status = "okay"; }; + +&pinctrl { + sd_clkreq_pins: sd-clkreq-pins { + pinmux = ; + }; +}; + +&pcie0 { + interrupts = , + ; + bus-range = <0 2>; + iommu-map = <0x100 &pcie_dart 1 2>, <0x200 &apcie1_dart 1 1>; + + port01: pci@1,0 { + device_type = "pci"; + reg = <0x800 0 0 0 0>; + reset-gpios = <&pinctrl 5 GPIO_ACTIVE_LOW>; + pinctrl-names = "default"; + pinctrl-0 = <&sd_clkreq_pins>; + #address-cells = <3>; + #size-cells = <2>; + ranges; + interrupt-controller; + #interrupt-cells = <1>; + interrupt-map-mask = <0 0 0 7>; + interrupt-map = <0 0 0 1 &port01 0 0 0 0>, + <0 0 0 2 &port01 0 0 0 1>, + <0 0 0 3 &port01 0 0 0 2>, + <0 0 0 4 &port01 0 0 0 3>; + /* The SD power switch needs an SMC service binding. */ + status = "disabled"; + }; +}; From a47d8eceb40a8adee654bfd80b98c130f25d7057 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 15:15:26 +1100 Subject: [PATCH 334/388] arm64: dts: apple: describe the J616s console transports and tether variant Move the UART and DockChannel descriptions into the SoC tree, disabled by default, and provide a tether variant that enables its consoles while leaving the host-owned ATC0 port untouched. Give the normal board the PMP alias needed for its firmware handoff as well. Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/boot/dts/apple/Makefile | 1 + .../boot/dts/apple/t6040-j616s-tether.dts | 45 +++++++++++++++++++ arch/arm64/boot/dts/apple/t6040-j616s.dts | 1 + arch/arm64/boot/dts/apple/t6040.dtsi | 25 +++++++++++ 4 files changed, 72 insertions(+) create mode 100644 arch/arm64/boot/dts/apple/t6040-j616s-tether.dts diff --git a/arch/arm64/boot/dts/apple/Makefile b/arch/arm64/boot/dts/apple/Makefile index 007e204fdbc56..0f6e6714f7de7 100644 --- a/arch/arm64/boot/dts/apple/Makefile +++ b/arch/arm64/boot/dts/apple/Makefile @@ -95,6 +95,7 @@ dtb-$(CONFIG_ARCH_APPLE) += t6032-j575d.dtb dtb-$(CONFIG_ARCH_APPLE) += t6034-j514m.dtb dtb-$(CONFIG_ARCH_APPLE) += t6034-j516m.dtb dtb-$(CONFIG_ARCH_APPLE) += t6040-j616s.dtb +dtb-$(CONFIG_ARCH_APPLE) += t6040-j616s-tether.dtb dtb-$(CONFIG_ARCH_APPLE) += t8112-j413.dtb dtb-$(CONFIG_ARCH_APPLE) += t8112-j415.dtb dtb-$(CONFIG_ARCH_APPLE) += t8112-j473.dtb diff --git a/arch/arm64/boot/dts/apple/t6040-j616s-tether.dts b/arch/arm64/boot/dts/apple/t6040-j616s-tether.dts new file mode 100644 index 0000000000000..619e09a88d091 --- /dev/null +++ b/arch/arm64/boot/dts/apple/t6040-j616s-tether.dts @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-2.0+ OR MIT +/* J616s tether console; the host owns ATC0 during the debug session. */ +#include "t6040-j616s.dts" + +/ { + aliases { + serial0 = &serial0; + }; +}; + +&serial0 { + status = "okay"; +}; + +&ps_uart0 { + status = "okay"; +}; + +&dcuart { + status = "okay"; +}; + +&hpm0 { + status = "disabled"; +}; + +&atcphy0 { + status = "disabled"; +}; + +&atcphy0_mbox { + status = "disabled"; +}; + +&usb_drd0 { + status = "disabled"; +}; + +&usb_drd0_dart_0 { + status = "disabled"; +}; + +&usb_drd0_dart_1 { + status = "disabled"; +}; diff --git a/arch/arm64/boot/dts/apple/t6040-j616s.dts b/arch/arm64/boot/dts/apple/t6040-j616s.dts index f4e37feca2475..d9db0cb08d603 100644 --- a/arch/arm64/boot/dts/apple/t6040-j616s.dts +++ b/arch/arm64/boot/dts/apple/t6040-j616s.dts @@ -29,6 +29,7 @@ disp0-piodma = &disp0_piodma; /* the boot loader finds the GPU node through this alias */ gpu = &gpu; + pmp = &pmp; /* the boot loader reserves the SEP firmware through this alias */ sep = &sep; }; diff --git a/arch/arm64/boot/dts/apple/t6040.dtsi b/arch/arm64/boot/dts/apple/t6040.dtsi index f113708592ddf..74ac312cdf8d6 100644 --- a/arch/arm64/boot/dts/apple/t6040.dtsi +++ b/arch/arm64/boot/dts/apple/t6040.dtsi @@ -2163,6 +2163,31 @@ status = "disabled"; }; + serial0: serial@429200000 { + compatible = "apple,s5l-uart"; + reg = <0x4 0x29200000 0x0 0x4000>; + reg-io-width = <4>; + interrupt-parent = <&aic>; + interrupts = ; + clocks = <&clkref>, <&clkref>; + clock-names = "uart", "clk_uart_baud0"; + power-domains = <&ps_uart0>; + status = "disabled"; + }; + + dcuart: serial@50883c000 { + compatible = "apple,dockchannel-uart"; + reg = <0x5 0x0883c000 0x0 0x4000>, + <0x5 0x08838000 0x0 0x4000>, + <0x5 0x0880c000 0x0 0x4000>; + reg-names = "data", "config", "irq"; + interrupt-parent = <&aic>; + interrupts = ; + apple,channel = <2>; + apple,sw-drain; + status = "disabled"; + }; + }; #include "t6040-pmgr.dtsi" From 01ac06477cbdb05210bbd446b45c8dee6800d2de Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 15:19:09 +1100 Subject: [PATCH 335/388] arm64: dts: apple: complete J616s console supply and fixed-PHY constraints Enable the UART supply parent with the tether UART and omit USB reset-provider cells on the fixed DisplayPort PHY, as required by its binding. Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/boot/dts/apple/t6040-j616s-tether.dts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/arch/arm64/boot/dts/apple/t6040-j616s-tether.dts b/arch/arm64/boot/dts/apple/t6040-j616s-tether.dts index 619e09a88d091..8027360847204 100644 --- a/arch/arm64/boot/dts/apple/t6040-j616s-tether.dts +++ b/arch/arm64/boot/dts/apple/t6040-j616s-tether.dts @@ -12,6 +12,10 @@ status = "okay"; }; +&ps_uart_p { + status = "okay"; +}; + &ps_uart0 { status = "okay"; }; From 422be5b4013e60eb05d0c290c76df85729faf7b3 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 15:20:29 +1100 Subject: [PATCH 336/388] arm64: dts: apple: describe the T6040 ACIO mailboxes and DARTs Describe all three USB4 coprocessor mailboxes and their NHI translation banks with their interrupt and power resources. Keep them disabled until the host-router reset, SRAM and stream handoff is qualified. Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/boot/dts/apple/t6040.dtsi | 75 ++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/arch/arm64/boot/dts/apple/t6040.dtsi b/arch/arm64/boot/dts/apple/t6040.dtsi index 74ac312cdf8d6..5c833124a04f0 100644 --- a/arch/arm64/boot/dts/apple/t6040.dtsi +++ b/arch/arm64/boot/dts/apple/t6040.dtsi @@ -2188,6 +2188,81 @@ status = "disabled"; }; + /* ACIO 0 remains disabled until its host-router handoff is supported. */ + usb4_0_mbox: mailbox@381108000 { + compatible = "apple,t6041-asc-mailbox", "apple,asc-mailbox-v4"; + reg = <0x3 0x81108000 0x0 0x4000>; + interrupt-parent = <&aic>; + interrupts = , + , + , + ; + interrupt-names = "send-empty", "send-not-empty", + "recv-empty", "recv-not-empty"; + #mbox-cells = <0>; + power-domains = <&ps_atc0_cio>; + status = "disabled"; + }; + + usb4_0_dart: iommu@381a80000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x3 0x81a80000 0x0 0xc000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + status = "disabled"; + }; + + /* ACIO 1 remains disabled until its host-router handoff is supported. */ + usb4_1_mbox: mailbox@389108000 { + compatible = "apple,t6041-asc-mailbox", "apple,asc-mailbox-v4"; + reg = <0x3 0x89108000 0x0 0x4000>; + interrupt-parent = <&aic>; + interrupts = , + , + , + ; + interrupt-names = "send-empty", "send-not-empty", + "recv-empty", "recv-not-empty"; + #mbox-cells = <0>; + power-domains = <&ps_atc1_cio>; + status = "disabled"; + }; + + usb4_1_dart: iommu@389a80000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x3 0x89a80000 0x0 0xc000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + status = "disabled"; + }; + + /* ACIO 2 remains disabled until its host-router handoff is supported. */ + usb4_2_mbox: mailbox@391108000 { + compatible = "apple,t6041-asc-mailbox", "apple,asc-mailbox-v4"; + reg = <0x3 0x91108000 0x0 0x4000>; + interrupt-parent = <&aic>; + interrupts = , + , + , + ; + interrupt-names = "send-empty", "send-not-empty", + "recv-empty", "recv-not-empty"; + #mbox-cells = <0>; + power-domains = <&ps_atc2_cio>; + status = "disabled"; + }; + + usb4_2_dart: iommu@391a80000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x3 0x91a80000 0x0 0xc000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + status = "disabled"; + }; + }; #include "t6040-pmgr.dtsi" From 0f12f5891a36c8ed4ac6674db6d0e9353d40a263 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 15:26:18 +1100 Subject: [PATCH 337/388] arm64: dts: apple: complete active T6040 power-state parent links Connect the three ATC common states, AVD, PMP and PMS SRAM to their ADT hardware parents. Enable the two missing fabric providers, both marked critical by the ADT, so active consumers never depend on disabled suppliers. Retain the existing additional PHY wrapper ordering dependencies. Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/boot/dts/apple/t6040-pmgr.dtsi | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/arch/arm64/boot/dts/apple/t6040-pmgr.dtsi b/arch/arm64/boot/dts/apple/t6040-pmgr.dtsi index 18ec5df7d33bf..eab3825a95c26 100644 --- a/arch/arm64/boot/dts/apple/t6040-pmgr.dtsi +++ b/arch/arm64/boot/dts/apple/t6040-pmgr.dtsi @@ -297,6 +297,7 @@ #power-domain-cells = <0>; #reset-cells = <0>; label = "avd_sys"; + power-domains = <&ps_fab0_soc>; }; ps_afc: power-controller@178 { @@ -315,6 +316,7 @@ #reset-cells = <0>; label = "pmp"; apple,always-on; + power-domains = <&ps_pms>; }; ps_pms_sram: power-controller@418 { @@ -324,6 +326,7 @@ #reset-cells = <0>; label = "pms_sram"; apple,always-on; + power-domains = <&ps_pms>; }; ps_fab6_soc: power-controller@480 { @@ -358,6 +361,7 @@ #reset-cells = <0>; label = "atc0_common"; apple,always-on; + power-domains = <&ps_fab2_soc>; }; ps_atc1_common: power-controller@3b8 { @@ -367,6 +371,7 @@ #reset-cells = <0>; label = "atc1_common"; apple,always-on; + power-domains = <&ps_fab2_soc>; }; ps_atc2_common: power-controller@3e0 { @@ -376,6 +381,7 @@ #reset-cells = <0>; label = "atc2_common"; apple,always-on; + power-domains = <&ps_fab2_soc>; }; }; @@ -1380,7 +1386,6 @@ label = "fab0_soc"; power-domains = <&ps_afi>; apple,always-on; - status = "disabled"; }; ps_amcc0: power-controller@180 { @@ -1648,7 +1653,6 @@ label = "fab2_soc"; power-domains = <&ps_afi>; apple,always-on; - status = "disabled"; }; ps_dispext0_sys: power-controller@340 { From cb347bca1a0a2e4a6ef205ed3bf6827a156452a1 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 15:56:32 +1100 Subject: [PATCH 338/388] arm64: dts: apple: keep T6041 SIO on its own firmware compatible The T6041 firmware protocol has not been qualified against the legacy SIO backend. Describe its resources with a generation-specific compatible, so a loader cannot accidentally bind that backend by enabling the node. Assisted-by: LLM Signed-off-by: Ryan Murray --- .../devicetree/bindings/dma/apple,sio.yaml | 16 +++++++++------- arch/arm64/boot/dts/apple/t6040.dtsi | 2 +- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Documentation/devicetree/bindings/dma/apple,sio.yaml b/Documentation/devicetree/bindings/dma/apple,sio.yaml index ab6bdf3686971..495adc52c7e6e 100644 --- a/Documentation/devicetree/bindings/dma/apple,sio.yaml +++ b/Documentation/devicetree/bindings/dma/apple,sio.yaml @@ -19,13 +19,15 @@ allOf: properties: compatible: - items: - - enum: - - apple,t6000-sio - - apple,t6030-sio - - apple,t6041-sio - - apple,t8103-sio - - const: apple,sio + oneOf: + - items: + - enum: + - apple,t6000-sio + - apple,t6030-sio + - apple,t8103-sio + - const: apple,sio + - description: T6041 firmware requires a generation-specific backend. + const: apple,t6041-sio reg: maxItems: 1 diff --git a/arch/arm64/boot/dts/apple/t6040.dtsi b/arch/arm64/boot/dts/apple/t6040.dtsi index 5c833124a04f0..77c74accdaa35 100644 --- a/arch/arm64/boot/dts/apple/t6040.dtsi +++ b/arch/arm64/boot/dts/apple/t6040.dtsi @@ -2106,7 +2106,7 @@ /* The loader supplies the SIO firmware regions and protocol tables. */ sio: dma-controller@429e00000 { - compatible = "apple,t6041-sio", "apple,sio"; + compatible = "apple,t6041-sio"; reg = <0x4 0x29e00000 0x0 0x8000>; dma-channels = <128>; #dma-cells = <1>; From 608c9f78845a6613a90b28ebf9a582469f2a8e1a Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 16:14:43 +1100 Subject: [PATCH 339/388] arm64: dts: apple: retain the active J616s PCIe scan range Keep the existing host bus range while the SD port is disabled. Generic ECAM access does not filter unavailable OF ports, so describing that port must not expand downstream configuration probing before its power handoff is qualified. Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/boot/dts/apple/t6040-j616s.dts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arch/arm64/boot/dts/apple/t6040-j616s.dts b/arch/arm64/boot/dts/apple/t6040-j616s.dts index d9db0cb08d603..933edf6ff2d63 100644 --- a/arch/arm64/boot/dts/apple/t6040-j616s.dts +++ b/arch/arm64/boot/dts/apple/t6040-j616s.dts @@ -4411,7 +4411,7 @@ &pcie0 { interrupts = , ; - bus-range = <0 2>; + /* Widen bus-range only with the SD power/port ownership handoff. */ iommu-map = <0x100 &pcie_dart 1 2>, <0x200 &apcie1_dart 1 1>; port01: pci@1,0 { From 40b5c45bd09d3375990e4d5bb943418107ea9498 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 17:45:07 +1100 Subject: [PATCH 340/388] ASoC: apple: macaudio: expose speaker variant straps from the device tree Allow a machine card to describe speaker identity inputs with speaker-id-gpios, least significant bit first. Expose their logical levels in a read-only volatile Speaker Variant ID control. Cards without the property retain their existing controls. Describe the two J616s speaker variant inputs on AP GPIO 52 and 53. Assisted-by: LLM Signed-off-by: Ryan Murray --- .../bindings/sound/apple,macaudio.yaml | 9 +++ arch/arm64/boot/dts/apple/t6040-j616s.dts | 2 + sound/soc/apple/macaudio.c | 60 +++++++++++++++++++ 3 files changed, 71 insertions(+) diff --git a/Documentation/devicetree/bindings/sound/apple,macaudio.yaml b/Documentation/devicetree/bindings/sound/apple,macaudio.yaml index fdf3d1883a99d..755c5d195cf0f 100644 --- a/Documentation/devicetree/bindings/sound/apple,macaudio.yaml +++ b/Documentation/devicetree/bindings/sound/apple,macaudio.yaml @@ -36,6 +36,15 @@ properties: Model name for presentation to users $ref: /schemas/types.yaml#/definitions/string + speaker-id-gpios: + minItems: 1 + maxItems: 8 + description: + Inputs encoding the fitted speaker assembly variant, least significant + bit first. The machine driver exposes their logical levels through the + read-only Speaker Variant ID control. GPIO polarity flags define each + bit's logical level. + patternProperties: "^dai-link(@[0-9a-f]+)?$": description: | diff --git a/arch/arm64/boot/dts/apple/t6040-j616s.dts b/arch/arm64/boot/dts/apple/t6040-j616s.dts index 933edf6ff2d63..3995f5d4db16a 100644 --- a/arch/arm64/boot/dts/apple/t6040-j616s.dts +++ b/arch/arm64/boot/dts/apple/t6040-j616s.dts @@ -1268,6 +1268,8 @@ sound: sound { compatible = "apple,j616-macaudio", "apple,macaudio"; model = "MacBook Pro J616"; + speaker-id-gpios = <&pinctrl 52 GPIO_ACTIVE_HIGH>, + <&pinctrl 53 GPIO_ACTIVE_HIGH>; #address-cells = <1>; #size-cells = <0>; diff --git a/sound/soc/apple/macaudio.c b/sound/soc/apple/macaudio.c index cc1123e10f398..b5511ed056887 100644 --- a/sound/soc/apple/macaudio.c +++ b/sound/soc/apple/macaudio.c @@ -24,6 +24,7 @@ #include #include +#include #include #include #include @@ -62,6 +63,7 @@ #define MACAUDIO_NUM_FE_LINKS 3 /* milliseconds */ #define SPEAKER_LOCK_TIMEOUT 250 +#define MACAUDIO_MAX_SPEAKER_ID_GPIOS 8 enum macaudio_amp_type { AMP_NONE, @@ -166,6 +168,7 @@ struct macaudio_snd_data { bool has_sense; bool has_safety; unsigned int max_channels; + struct gpio_descs *speaker_id_gpios; struct macaudio_link_props { /* frontend props */ @@ -1430,6 +1433,50 @@ static const struct snd_soc_jack_pin macaudio_jack_pins[] = { }, }; +static int macaudio_speaker_id_info(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_info *uinfo) +{ + struct snd_soc_card *card = snd_kcontrol_chip(kcontrol); + struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(card); + + uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER; + uinfo->count = 1; + uinfo->value.integer.min = 0; + uinfo->value.integer.max = BIT(ma->speaker_id_gpios->ndescs) - 1; + uinfo->value.integer.step = 1; + + return 0; +} + +static int macaudio_speaker_id_get(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *uvalue) +{ + struct snd_soc_card *card = snd_kcontrol_chip(kcontrol); + struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(card); + unsigned int value = 0; + unsigned int i; + int ret; + + for (i = 0; i < ma->speaker_id_gpios->ndescs; i++) { + ret = gpiod_get_value_cansleep(ma->speaker_id_gpios->desc[i]); + if (ret < 0) + return ret; + if (ret) + value |= BIT(i); + } + uvalue->value.integer.value[0] = value; + + return 0; +} + +static const struct snd_kcontrol_new macaudio_speaker_id_control = { + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .name = "Speaker Variant ID", + .access = SNDRV_CTL_ELEM_ACCESS_READ | SNDRV_CTL_ELEM_ACCESS_VOLATILE, + .info = macaudio_speaker_id_info, + .get = macaudio_speaker_id_get, +}; + static int macaudio_probe(struct snd_soc_card *card) { struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(card); @@ -1460,6 +1507,9 @@ static int macaudio_probe(struct snd_soc_card *card) snd_jack_set_key(ma->jack.jack, SND_JACK_BTN_2, KEY_VOLUMEDOWN); snd_jack_set_key(ma->jack.jack, SND_JACK_BTN_3, KEY_VOICECOMMAND); + if (ma->speaker_id_gpios) + return snd_soc_add_card_controls(card, &macaudio_speaker_id_control, 1); + return ret; } @@ -2183,6 +2233,16 @@ static int macaudio_snd_platform_probe(struct platform_device *pdev) if (ret) return ret; + data->speaker_id_gpios = devm_gpiod_get_array_optional(dev, "speaker-id", + GPIOD_IN); + if (IS_ERR(data->speaker_id_gpios)) + return dev_err_probe(dev, PTR_ERR(data->speaker_id_gpios), + "Failed to get speaker ID inputs\n"); + if (data->speaker_id_gpios && + (!data->speaker_id_gpios->ndescs || + data->speaker_id_gpios->ndescs > MACAUDIO_MAX_SPEAKER_ID_GPIOS)) + return dev_err_probe(dev, -EINVAL, "Invalid speaker ID input count\n"); + /* * Without sense the PCM2 link is gone, and a card whose front-ends * are not the MCA's (the J616 jack) has no PCM2 RX widget at all. From 6e7b99dc0bbac333b8fd640568e0715ad3bcb2a8 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 18:12:58 +1100 Subject: [PATCH 341/388] ASoC: apple: macaudio: append variant identity after existing controls Register the optional read-only identity control in late_probe, after the existing card and codec controls have been created. Their numeric identifiers remain stable when a board gains speaker-id-gpios. Assisted-by: LLM Signed-off-by: Ryan Murray --- sound/soc/apple/macaudio.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/sound/soc/apple/macaudio.c b/sound/soc/apple/macaudio.c index b5511ed056887..96cd57aae29a8 100644 --- a/sound/soc/apple/macaudio.c +++ b/sound/soc/apple/macaudio.c @@ -1507,9 +1507,6 @@ static int macaudio_probe(struct snd_soc_card *card) snd_jack_set_key(ma->jack.jack, SND_JACK_BTN_2, KEY_VOLUMEDOWN); snd_jack_set_key(ma->jack.jack, SND_JACK_BTN_3, KEY_VOICECOMMAND); - if (ma->speaker_id_gpios) - return snd_soc_add_card_controls(card, &macaudio_speaker_id_control, 1); - return ret; } @@ -1634,6 +1631,13 @@ static int macaudio_late_probe(struct snd_soc_card *card) mutex_unlock(&ma->volume_lock_mutex); } + /* Append identity metadata without renumbering the existing controls. */ + if (ma->speaker_id_gpios) { + ret = snd_soc_add_card_controls(card, &macaudio_speaker_id_control, 1); + if (ret) + return ret; + } + /* The notifier walks the card's controls: only while the card is bound */ return register_pm_notifier(&ma->pm_nb); } From d34a031e71dab2b5373270ef5133295a8221881b Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 18:46:20 +1100 Subject: [PATCH 342/388] arm64: dts: apple: retain the enabled T6040 SEP power suppliers The complete power-state table and the SEP series both describe SPI_P, SPI2 and SEP. Replaying their individual patches during a rebase leaves duplicate nodes whose later disabled status overrides the enabled suppliers required by the SPI2 fingerprint transport and SEP. Keep one definition of each physical power state and retain the enabled SEP-series definitions, as in the previously integrated tree. This also restores the active-provider invariant checked against the compiled DT. Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/boot/dts/apple/t6040-pmgr.dtsi | 28 ----------------------- 1 file changed, 28 deletions(-) diff --git a/arch/arm64/boot/dts/apple/t6040-pmgr.dtsi b/arch/arm64/boot/dts/apple/t6040-pmgr.dtsi index eab3825a95c26..e0af3e1903cfb 100644 --- a/arch/arm64/boot/dts/apple/t6040-pmgr.dtsi +++ b/arch/arm64/boot/dts/apple/t6040-pmgr.dtsi @@ -600,15 +600,6 @@ status = "disabled"; }; - ps_spi_p: power-controller@198 { - compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; - reg = <0x198 4>; - #power-domain-cells = <0>; - #reset-cells = <0>; - label = "spi_p"; - power-domains = <&ps_sio>; - status = "disabled"; - }; ps_uart_p: power-controller@1a0 { compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; @@ -662,15 +653,6 @@ status = "disabled"; }; - ps_spi2: power-controller@1c8 { - compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; - reg = <0x1c8 4>; - #power-domain-cells = <0>; - #reset-cells = <0>; - label = "spi2"; - power-domains = <&ps_spi_p>; - status = "disabled"; - }; ps_spi3: power-controller@1d0 { compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; @@ -1078,16 +1060,6 @@ status = "disabled"; }; - ps_sep: power-controller@c00 { - compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; - reg = <0xc00 4>; - #power-domain-cells = <0>; - #reset-cells = <0>; - label = "sep"; - power-domains = <&ps_fab3_soc>; - apple,always-on; - status = "disabled"; - }; }; &pmgr_aux { From e1a25b5e952e52ceb9343a03c88ebd5a2e980f48 Mon Sep 17 00:00:00 2001 From: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:50:57 +1100 Subject: [PATCH 343/388] selftests: apple-dcp: initialize Type-C retry fixture state The synthetic retry fixture left boolean state uninitialized, which let sanitizer runs fail before exercising the intended budget. Start it from the same zero state as the other fixture cases. Assisted-by: LLM Signed-off-by: Ryan Murray --- tools/testing/selftests/drivers/gpu/apple-dcp/pm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py b/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py index d5b3890d30861..664ba046dad35 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py @@ -547,7 +547,7 @@ def function(text, name): int main(void) { (void)cd321x_pm_notifications; struct cd321x c; - struct cd321x_pm_state budget; + struct cd321x_pm_state budget={0}; cd321x_pm_init(&budget);assert(cd321x_pm_ready(&budget)); assert(cd321x_display_taken(-ENOSPC,true)&&!cd321x_display_taken(-EBUSY,true)&&!cd321x_display_taken(0,true)&&cd321x_display_taken(-EBUSY,false)&&!cd321x_display_taken(-ENOSPC,false)&&!cd321x_display_taken(0,false)); for(unsigned int i=0;i Date: Tue, 27 Oct 2026 11:35:13 +0100 Subject: [PATCH 344/388] drm/apple: dual pipe: give the peer back with the primary's link A pair's peer stayed marked as its owner's peer after the owner's display was gone: after system sleep, or a cable change, the port could move to the peer pipe, and the peer then ran its own display with the owner recorded. dcp_peer_swap_complete_26_6() takes the swaps of a reserved peer for the pair's and discarded the display's own swaps: the picture stuttered and the CRTC retrained in a loop. The reservation is released when the primary disconnects its DPTX link (a merged pair is stopped first by the power-off, and a pair that still runs is kept); the next allocation reserves a peer again. A peer's swap is only taken for the pair's while the peer has no route of its own. A pipe that was unbound is no longer refused as a peer after it binds again. --- drivers/gpu/drm/apple/dcp-peer-v26_6.inc | 33 +++++++++++++++++++++++- drivers/gpu/drm/apple/dcp.c | 3 +++ drivers/gpu/drm/apple/dcp.h | 2 ++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/apple/dcp-peer-v26_6.inc b/drivers/gpu/drm/apple/dcp-peer-v26_6.inc index 8be2e69a03e5d..70e16c5330a2b 100644 --- a/drivers/gpu/drm/apple/dcp-peer-v26_6.inc +++ b/drivers/gpu/drm/apple/dcp-peer-v26_6.inc @@ -118,6 +118,36 @@ static int dcp_peer_call_oob(struct apple_dcp *dcp, const char *tag, /* ---- peer release / forget ---- */ +/* + * The primary's display is gone (cable removed, port moved, system sleep): + * give the reserved peer back. A merged pair is stopped first; a pair that + * still runs is kept. The next allocation reserves a peer again. Without + * this the peer stays marked as an owner's peer after its port was taken by + * another display, and swap_complete would discard that display's own swaps. + */ +void dcp_peer_release_26_6(struct apple_dcp *dcp) +{ + guard(mutex)(&dcp_peer_lock); + if (dcp->peer_reserved) { + struct apple_dcp *peer = dcp->peer_reserved; + + if (dcp->peer_tiling.vftg || dcp->peer_tiling.member || + peer->peer_tiling.vftg || peer->peer_tiling.member) + return; + memset(&dcp->peer_tiling, 0, sizeof(dcp->peer_tiling)); + memset(&peer->peer_tiling, 0, sizeof(peer->peer_tiling)); + peer->peer_owner = NULL; + dcp->peer_reserved = NULL; + dcp->peer_active = false; + } +} + +void dcp_peer_bound_26_6(struct apple_dcp *dcp) +{ + guard(mutex)(&dcp_peer_lock); + dcp->peer_unbinding = false; +} + static void dcp_peer_forget(struct apple_dcp *dcp) { guard(mutex)(&dcp_peer_lock); @@ -971,7 +1001,8 @@ void dcp_peer_swap_submit_26_6(struct apple_dcp *dcp, */ bool dcp_peer_swap_complete_26_6(struct apple_dcp *dcp, u32 swap_id) { - if (!READ_ONCE(dcp->peer_owner)) + /* Only a peer without a display of its own: its swaps are the pair's. */ + if (!READ_ONCE(dcp->peer_owner) || READ_ONCE(dcp->active_typec_route)) return false; WRITE_ONCE(dcp->last_swap_id, swap_id); WRITE_ONCE(dcp->have_swap_complete, true); diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 447d882f933f4..c472ebdb47a27 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -2631,6 +2631,8 @@ int dcp_dptx_disconnect(struct apple_dcp *dcp, u32 port) if (!ret) WRITE_ONCE(dcp->dptxport[port].tile_hint, false); mutex_unlock(&dcp->hpd_mutex); + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && !port) + dcp_peer_release_26_6(dcp); return ret; } @@ -4829,6 +4831,7 @@ static int dcp_comp_bind(struct device *dev, struct device *main, void *data) if (READ_ONCE(dcp->ext_dead)) return 0; + dcp_peer_bound_26_6(dcp); ret = dcp_comp_bind_pipe(dev, main, data); if (ret && dcp->ext_failsafe) { dcp_ext_give_up(dcp, ret, "bind"); diff --git a/drivers/gpu/drm/apple/dcp.h b/drivers/gpu/drm/apple/dcp.h index f6f1c4c5282be..1b62f3df7889a 100644 --- a/drivers/gpu/drm/apple/dcp.h +++ b/drivers/gpu/drm/apple/dcp.h @@ -159,5 +159,7 @@ void dcp_peer_swap_submit_26_6(struct apple_dcp *dcp, u32 swap_id); bool dcp_peer_swap_complete_26_6(struct apple_dcp *dcp, u32 swap_id); int dcp_peer_stop_26_6(struct apple_dcp *dcp); +void dcp_peer_release_26_6(struct apple_dcp *dcp); +void dcp_peer_bound_26_6(struct apple_dcp *dcp); #endif From 1e4e813f64d2e1684f7c4abf075ef4301ddada94 Mon Sep 17 00:00:00 2001 From: fbalazsy Date: Tue, 27 Oct 2026 11:35:13 +0100 Subject: [PATCH 345/388] drm/apple: fabric: plan a Type-C port onto the hybrid pipeline from the DT, no debounce without a sink apple,typec-hybrid-first on dcpext0 (the J616s: the only pipe that can be the primary of a display wider than one pipe) makes the fabric plan a Type-C port onto the hybrid pipeline first, as appledrm.typec_hybrid_first does, so a 7680 px sink on USB-C is paired at boot without a module parameter. The pipeline is only available while its fixed output has no sink, so an HDMI display plugged in first keeps it. The HDMI presence debounce (ten seconds) delayed a sample that finds no sink where there was none before, at start-up and after system sleep. The pipeline still counted as taken when the port was planned at start-up, or re-planned after the sink dropped its link in sleep, and the port went to the other pipe (a flattened 3840x2160, and after sleep no pair). Such a sample now stays absent; a sample after a real unplug is debounced as before. --- .../bindings/display/apple,dcp.yaml | 9 +++++ .../boot/dts/apple/t6040-j616s-extdisp.dtsi | 2 ++ drivers/gpu/drm/apple/dcp-fabric.c | 35 ++++++++++++++++--- drivers/gpu/drm/apple/dcp-internal.h | 2 ++ drivers/gpu/drm/apple/dcp.c | 2 ++ drivers/gpu/drm/apple/dcp.h | 2 ++ 6 files changed, 48 insertions(+), 4 deletions(-) diff --git a/Documentation/devicetree/bindings/display/apple,dcp.yaml b/Documentation/devicetree/bindings/display/apple,dcp.yaml index 4297fb6167300..38be82bc7d1bd 100644 --- a/Documentation/devicetree/bindings/display/apple,dcp.yaml +++ b/Documentation/devicetree/bindings/display/apple,dcp.yaml @@ -214,6 +214,15 @@ properties: $ref: /schemas/types.yaml#/definitions/uint32 description: Display coprocessor index on multi-DCP systems. + apple,typec-hybrid-first: + type: boolean + description: + A hybrid pipeline (a fixed output plus Type-C routes) is planned for a + Type-C port before the other pipelines, while its fixed output has no + sink, instead of being kept back for that output. Set where this + pipeline is the only one that can be the primary of a display wider than + one pipe. + apple,dcp-endpoint: $ref: /schemas/types.yaml#/definitions/uint32 description: diff --git a/arch/arm64/boot/dts/apple/t6040-j616s-extdisp.dtsi b/arch/arm64/boot/dts/apple/t6040-j616s-extdisp.dtsi index c38c36853b02d..b6486f293da9d 100644 --- a/arch/arm64/boot/dts/apple/t6040-j616s-extdisp.dtsi +++ b/arch/arm64/boot/dts/apple/t6040-j616s-extdisp.dtsi @@ -75,6 +75,8 @@ apple,bw-scratch = <&pmgr_dcp 0 4 0x28000>; /* firmware Disp_Index, the dual-pipe endpoint ID */ apple,dcp-endpoint = <1>; + /* the only pipe that can be the primary of a two-pipe display */ + apple,typec-hybrid-first; status = "disabled"; dispext0_piodma: piodma { diff --git a/drivers/gpu/drm/apple/dcp-fabric.c b/drivers/gpu/drm/apple/dcp-fabric.c index b49e277cd840a..6e0e049d47005 100644 --- a/drivers/gpu/drm/apple/dcp-fabric.c +++ b/drivers/gpu/drm/apple/dcp-fabric.c @@ -50,6 +50,19 @@ module_param(typec_follow_crtc, bool, 0444); static bool typec_hybrid_first; module_param(typec_hybrid_first, bool, 0644); MODULE_PARM_DESC(typec_hybrid_first, "Type-C ports take the hybrid (HDMI) pipeline first (dual-pipe primary)"); + +/* Set by a pipeline whose device tree asks for it (apple,typec-hybrid-first). */ +static bool typec_hybrid_first_dt; + +void dcp_typec_hybrid_first_dt(void) +{ + WRITE_ONCE(typec_hybrid_first_dt, true); +} + +static bool dcp_typec_hybrid_first(void) +{ + return READ_ONCE(typec_hybrid_first) || READ_ONCE(typec_hybrid_first_dt); +} MODULE_PARM_DESC(typec_follow_crtc, "Let direct DP-alt routes follow their selected CRTC (default: on)"); @@ -206,7 +219,7 @@ static bool dcp_typec_route_available(struct apple_dcp_typec_route *route) }; struct dcp_fabric_policy policy = { .dual_stream = dcp_typec_ordered_dual_stream(), - .hybrid_first = typec_hybrid_first }; + .hybrid_first = dcp_typec_hybrid_first() }; pipeline.presence = dcp_hdmi_presence(route->dcp); return dcp_fabric_available(&pipeline, &policy); @@ -740,6 +753,8 @@ static u64 dcp_hdmi_edge(struct apple_dcp *dcp) if (!dcp_hdmi_settle_enabled(dcp)) return 0; spin_lock_irqsave(&dcp->hdmi_presence_lock, flags); + if (dcp->hdmi_presence.state != DCP_FABRIC_SETTLING) + dcp->hdmi_presence_prev = dcp->hdmi_presence.state; generation = dcp_fabric_presence_edge(&dcp->hdmi_presence, jiffies, msecs_to_jiffies(DCP_HDMI_HOLD_MS)); spin_unlock_irqrestore(&dcp->hdmi_presence_lock, flags); @@ -776,6 +791,17 @@ static bool dcp_hdmi_sample(struct apple_dcp *dcp, u64 generation, int level) accepted = dcp_fabric_presence_sample(&dcp->hdmi_presence, generation, level > 0, jiffies, msecs_to_jiffies(DCP_HDMI_HOLD_MS)); + /* + * The debounce guards against a glitch after an unplug. A sample that + * finds no sink where there was none before (start-up, and the sample + * after system sleep) has nothing to debounce: the pipeline is free + * now, not ten seconds later, when a Type-C port has already been + * planned or moved onto another pipeline. + */ + if (accepted && level == 0 && dcp->hdmi_presence_prev == DCP_FABRIC_ABSENT) { + dcp->hdmi_presence.state = DCP_FABRIC_ABSENT; + dcp->hdmi_presence.deadline = 0; + } spin_unlock_irqrestore(&dcp->hdmi_presence_lock, flags); dcp_hdmi_schedule(dcp); return accepted; @@ -854,6 +880,7 @@ void dcp_fabric_init(struct apple_dcp *dcp) { spin_lock_init(&dcp->hdmi_presence_lock); dcp->hdmi_presence.state = DCP_FABRIC_ABSENT; + dcp->hdmi_presence_prev = DCP_FABRIC_ABSENT; INIT_DELAYED_WORK(&dcp->hdmi_settle_wq, dcp_hdmi_settle_work); INIT_DELAYED_WORK(&dcp->hdmi_recheck_wq, dcp_hdmi_recheck_work); disable_delayed_work(&dcp->hdmi_settle_wq); @@ -1082,7 +1109,7 @@ dcp_typec_free_route(struct apple_dcp_typec_port *port) struct dcp_fabric_policy policy = { .dual_stream = dcp_typec_ordered_dual_stream(), .follow = dcp_typec_port_follows(port), - .hybrid_first = typec_hybrid_first, + .hybrid_first = dcp_typec_hybrid_first(), }; dcp_fabric_snapshot_port(port, false); @@ -2059,7 +2086,7 @@ static int dcp_tb_candidate(void *data) struct dcp_fabric_policy policy = { .dual_stream = dcp_typec_ordered_dual_stream(), .follow = dcp_typec_port_follows(port), - .hybrid_first = typec_hybrid_first, + .hybrid_first = dcp_typec_hybrid_first(), }; struct dcp_fabric_route *chosen; bool connector_present = dpin ? !!port->secondary_connector : !!port->connector; @@ -2792,7 +2819,7 @@ static int dcp_typec_follow_decide(struct apple_dcp *dcp, struct drm_crtc *crtc, struct dcp_typec_follow *follow) { struct dcp_fabric_policy policy = { .dual_stream = dcp_typec_ordered_dual_stream(), - .hybrid_first = typec_hybrid_first }; + .hybrid_first = dcp_typec_hybrid_first() }; struct apple_dcp_typec_route *from, *to, *holder, *back = NULL; struct apple_dcp_typec_port *port, *other = NULL; bool secondary, off = false; diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index 545d688b2ea53..f233e9486f787 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -713,6 +713,8 @@ struct apple_dcp { /* Hardirq, resume and expiry share one generation under this lock. */ spinlock_t hdmi_presence_lock; struct dcp_fabric_presence hdmi_presence; + /* The state before the current series of HPD edges began. */ + enum dcp_fabric_presence_state hdmi_presence_prev; unsigned long hdmi_edge_jiffies; bool hdmi_edge_seen; struct delayed_work hdmi_settle_wq; diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index c472ebdb47a27..786f505dd9eac 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -5247,6 +5247,8 @@ static int dcp_platform_probe_pipe(struct platform_device *pdev) dcp->connector_type = dcp->fixed_connector_type; of_property_read_u32(dev->of_node, "apple,dcp-index", &dcp->index); of_property_read_u32(dev->of_node, "apple,dcp-endpoint", &dcp->peer_endpoint); + if (of_property_read_bool(dev->of_node, "apple,typec-hybrid-first")) + dcp_typec_hybrid_first_dt(); of_property_read_u32(dev->of_node, "apple,dptx-phy", &dcp->dptx_phy); of_property_read_u32(dev->of_node, "apple,dptx-die", &dcp->dptx_die); dcp->fixed_dptx_phy = dcp->dptx_phy; diff --git a/drivers/gpu/drm/apple/dcp.h b/drivers/gpu/drm/apple/dcp.h index 1b62f3df7889a..86007f00c9a4f 100644 --- a/drivers/gpu/drm/apple/dcp.h +++ b/drivers/gpu/drm/apple/dcp.h @@ -143,6 +143,8 @@ unsigned int apple_j613_25g83_clock_hz(void); /* A J613, or a J615 whose boot loader carries the experimental opt-in. */ bool apple_t8122_25g83_board(void); +void dcp_typec_hybrid_first_dt(void); + /* Desktop 26.6 dual-pipe peer (dcp-peer-v26_6.inc, included from dcp.c) */ struct dptx_port; int dcp_peer_hints_26_6(struct apple_dcp *dcp, struct dptx_port *port, From df9a7a251316365b1cc7d88abeefd68536356d57 Mon Sep 17 00:00:00 2001 From: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com> Date: Tue, 27 Oct 2026 23:06:12 +1100 Subject: [PATCH 346/388] phy: apple: t6040: support two-lane DP pin D beside USB3 --- drivers/phy/apple/atc-t6040-dp2-data.h | 462 +++++++++++++++++++++++++ drivers/phy/apple/t6040-usb2.c | 104 ++++-- 2 files changed, 541 insertions(+), 25 deletions(-) create mode 100644 drivers/phy/apple/atc-t6040-dp2-data.h diff --git a/drivers/phy/apple/atc-t6040-dp2-data.h b/drivers/phy/apple/atc-t6040-dp2-data.h new file mode 100644 index 0000000000000..c76fbe14d5ac3 --- /dev/null +++ b/drivers/phy/apple/atc-t6040-dp2-data.h @@ -0,0 +1,462 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Two-lane DisplayPort link sequences for the T6040 ATC core. + * From Dj's public T6050 combo PHY tables, licensed GPL-2.0-only OR MIT: + * https://github.com/aurora-silicon/linux/pull/480 + * The common ATC banks and two USB3/DP lane groups match the T6040 core. + */ +static const struct atc_op dp_group0_off[] = { + { ATC_MASK, 39, 0x14, 0x1007, 0x7, 0 }, + { ATC_MASK, 39, 0x18, 0x1, 0x0, 0 }, + { ATC_POLL, 39, 0x1c, 0x1, 0x0, 5000 }, + { ATC_MASK, 14, 0x0, 0x1, 0x1, 0 }, + { ATC_MASK, 22, 0x1c, 0xc00000, 0xc00000, 0 }, + { ATC_MASK, 22, 0x10, 0x600, 0x400, 0 }, + { ATC_MASK, 22, 0x4, 0x30000000, 0x20000000, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 22, 0x44, 0x100000, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 22, 0x44, 0x400000, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 22, 0x44, 0x4000000, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 22, 0x44, 0x1000000, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 22, 0x44, 0x1000, 0x0, 0 }, + { ATC_MASK, 2, 0x120, 0x100, 0x100, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0x120, 0x1, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0x120, 0x10, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 19, 0x44, 0x6000000, 0x4000000, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 19, 0x44, 0x18000000, 0x10000000, 0 }, + { ATC_MASK, 19, 0x24, 0xc0000000, 0x80000000, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 19, 0x24, 0x30000000, 0x20000000, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 19, 0x28, 0x10, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 19, 0x40, 0xc000, 0xc000, 0 }, + { ATC_MASK, 16, 0x1d0, 0x3, 0x1, 0 }, + { ATC_MASK, 19, 0x40, 0x10000, 0x0, 0 }, + { ATC_MASK, 2, 0xc, 0x4000, 0x4000, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0xc, 0x40, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0xc, 0x400, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0x10, 0x1, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0x10, 0x10, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 14, 0x0, 0xc, 0x8, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0x160, 0x40000, 0x40000, 0 }, + { ATC_MASK, 3, 0x8, 0x1, 0x1, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0x160, 0x80000, 0x0, 0 }, + { ATC_MASK, 3, 0x8, 0x2, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0x160, 0x80000, 0x80000, 0 }, + { ATC_MASK, 3, 0x8, 0x2, 0x2, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0x160, 0x40000, 0x0, 0 }, + { ATC_MASK, 3, 0x8, 0x1, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 6, 0x1c, 0x7, 0x1, 0 }, + { ATC_MASK, 6, 0x1c, 0x8, 0x8, 0 }, + { ATC_POLL, 6, 0x2c, 0xff00, 0x100, 500000 }, + { ATC_MASK, 6, 0x1c, 0x8, 0x0, 0 }, + { ATC_POLL, 6, 0x2c, 0xff00, 0x0, 500000 }, + { ATC_POLL, 14, 0x34, 0x8, 0x0, 500000 }, + { ATC_DELAY, 0, 0x0, 0x0, 0xa, 0 }, + { ATC_MASK, 6, 0x34, 0xf, 0x1, 0 }, + { ATC_MASK, 6, 0x1c, 0x7, 0x0, 0 }, + { ATC_MASK, 6, 0x1c, 0x8, 0x8, 0 }, + { ATC_POLL, 6, 0x2c, 0xff00, 0x100, 500000 }, + { ATC_MASK, 6, 0x1c, 0x8, 0x0, 0 }, + { ATC_POLL, 6, 0x2c, 0xff00, 0x0, 500000 }, + { ATC_MASK, 8, 0x0, 0xaaa, 0x0, 0 }, +}; + +static const struct atc_rate_op dp_group0_on[] = { + { { ATC_MASK, 39, 0x14, 0x7, 0, 0 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 39, 0x18, 0x1, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 2, 0x6c, 0x1c, 0, 0 }, 0xf, { 0x8, 0x8, 0x8, 0x8 } }, + { { ATC_MASK, 14, 0x0, 0x40fff0, 0, 0 }, 0xf, { 0x1110, 0x1110, 0x1110, 0x1110 } }, + { { ATC_MASK, 8, 0x34, 0x3, 0, 0 }, 0x1, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 7, 0x80, 0xffffffff, 0, 0 }, 0x1, { 0x1e0e021c, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 7, 0x84, 0xfffffff, 0, 0 }, 0x1, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 7, 0x88, 0x7fffff, 0, 0 }, 0x1, { 0x156600, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 8, 0x8, 0x1f0000, 0, 0 }, 0x1, { 0x70000, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 8, 0x18, 0x1, 0, 0 }, 0x1, { 0x1, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 8, 0x34, 0x3, 0, 0 }, 0x2, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 7, 0x80, 0xffffffff, 0, 0 }, 0x2, { 0x0, 0x1e0e01c2, 0x0, 0x0 } }, + { { ATC_MASK, 7, 0x84, 0xfffffff, 0, 0 }, 0x2, { 0x0, 0x7fffffe, 0x0, 0x0 } }, + { { ATC_MASK, 7, 0x88, 0x7fffff, 0, 0 }, 0x2, { 0x0, 0x155200, 0x0, 0x0 } }, + { { ATC_MASK, 8, 0x8, 0x1f0000, 0, 0 }, 0x2, { 0x0, 0x70000, 0x0, 0x0 } }, + { { ATC_MASK, 8, 0x18, 0x1, 0, 0 }, 0x2, { 0x0, 0x1, 0x0, 0x0 } }, + { { ATC_MASK, 8, 0x34, 0x3, 0, 0 }, 0x4, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 7, 0x80, 0xffffffff, 0, 0 }, 0x4, { 0x0, 0x0, 0x1e0e01c2, 0x0 } }, + { { ATC_MASK, 7, 0x84, 0xfffffff, 0, 0 }, 0x4, { 0x0, 0x0, 0x7fffffe, 0x0 } }, + { { ATC_MASK, 7, 0x88, 0x7fffff, 0, 0 }, 0x4, { 0x0, 0x0, 0x554800, 0x0 } }, + { { ATC_MASK, 8, 0x8, 0x1f0000, 0, 0 }, 0x4, { 0x0, 0x0, 0x70000, 0x0 } }, + { { ATC_MASK, 8, 0x18, 0x1, 0, 0 }, 0x4, { 0x0, 0x0, 0x1, 0x0 } }, + { { ATC_MASK, 8, 0x34, 0x3, 0, 0 }, 0x8, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 7, 0x80, 0xffffffff, 0, 0 }, 0x8, { 0x0, 0x0, 0x0, 0x1e0e02a3 } }, + { { ATC_MASK, 7, 0x84, 0xfffffff, 0, 0 }, 0x8, { 0x0, 0x0, 0x0, 0xbff7ffc } }, + { { ATC_MASK, 7, 0x88, 0x7fffff, 0, 0 }, 0x8, { 0x0, 0x0, 0x0, 0x564800 } }, + { { ATC_MASK, 8, 0x8, 0x1f0000, 0, 0 }, 0x8, { 0x0, 0x0, 0x0, 0x70000 } }, + { { ATC_MASK, 8, 0x18, 0x1, 0, 0 }, 0x8, { 0x0, 0x0, 0x0, 0x1 } }, + { { ATC_MASK, 8, 0x0, 0xaaa, 0, 0 }, 0xf, { 0xaaa, 0xaaa, 0xaaa, 0xaaa } }, + { { ATC_MASK, 6, 0x30, 0x4000, 0, 0 }, 0xf, { 0x4000, 0x4000, 0x4000, 0x4000 } }, + { { ATC_MASK, 6, 0x34, 0xf, 0, 0 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 6, 0x18, 0x1fe000, 0, 0 }, 0xf, { 0x2000, 0x2000, 0x2000, 0x2000 } }, + { { ATC_MASK, 6, 0x20, 0x1, 0, 0 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 6, 0x1c, 0x7, 0, 0 }, 0xf, { 0x7, 0x7, 0x7, 0x7 } }, + { { ATC_MASK, 6, 0x1c, 0x8, 0, 0 }, 0xf, { 0x8, 0x8, 0x8, 0x8 } }, + { { ATC_POLL, 6, 0x2c, 0xff00, 0, 500000 }, 0xf, { 0x100, 0x100, 0x100, 0x100 } }, + { { ATC_MASK, 6, 0x1c, 0x8, 0, 0 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_POLL, 6, 0x2c, 0xff00, 0, 500000 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_POLL, 14, 0x34, 0x8, 0, 500000 }, 0xf, { 0x8, 0x8, 0x8, 0x8 } }, + { { ATC_MASK, 19, 0x3c, 0x18000, 0, 0 }, 0xf, { 0x10000, 0x10000, 0x10000, 0x10000 } }, + { { ATC_MASK, 14, 0x0, 0xc, 0, 0 }, 0xf, { 0xc, 0xc, 0xc, 0xc } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 2, 0x120, 0x10, 0, 0 }, 0xf, { 0x10, 0x10, 0x10, 0x10 } }, + { { ATC_MASK, 2, 0x120, 0x40, 0, 0 }, 0xf, { 0x40, 0x40, 0x40, 0x40 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 2, 0x120, 0x1, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 2, 0x120, 0x4, 0, 0 }, 0xf, { 0x4, 0x4, 0x4, 0x4 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 2, 0x120, 0x100, 0, 0 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 2, 0x120, 0x400, 0, 0 }, 0xf, { 0x400, 0x400, 0x400, 0x400 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 22, 0x44, 0xfffffc0, 0, 0 }, 0xf, { 0xaafb800, 0xaafb800, 0xaafb800, 0xaafb800 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 22, 0x44, 0x1000000, 0, 0 }, 0xf, { 0x1000000, 0x1000000, 0x1000000, 0x1000000 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 22, 0x44, 0x4000000, 0, 0 }, 0xf, { 0x4000000, 0x4000000, 0x4000000, 0x4000000 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 22, 0x44, 0x7c0, 0, 0 }, 0xf, { 0x300, 0x300, 0x300, 0x300 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 22, 0x44, 0x6c000, 0, 0 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 21, 0x100, 0x100000, 0, 0 }, 0xf, { 0x100000, 0x100000, 0x100000, 0x100000 } }, + { { ATC_MASK, 21, 0x58, 0xc00f, 0, 0 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 21, 0x104, 0x20000000, 0, 0 }, 0xf, { 0x20000000, 0x20000000, 0x20000000, 0x20000000 } }, + { { ATC_MASK, 21, 0x100, 0x7ffff, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 22, 0x44, 0x100000, 0, 0 }, 0x7, { 0x100000, 0x100000, 0x100000, 0x0 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0x7, { 0x1, 0x1, 0x1, 0x0 } }, + { { ATC_MASK, 22, 0x44, 0x400000, 0, 0 }, 0x7, { 0x400000, 0x400000, 0x400000, 0x0 } }, + { { ATC_MASK, 22, 0x4, 0x3000000, 0, 0 }, 0xf, { 0x2000000, 0x2000000, 0x2000000, 0x2000000 } }, + { { ATC_MASK, 22, 0x4, 0x30000000, 0, 0 }, 0xf, { 0x30000000, 0x30000000, 0x30000000, 0x30000000 } }, + { { ATC_MASK, 22, 0xc, 0x7000000, 0, 0 }, 0x1, { 0x5000000, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 22, 0xc, 0x7000000, 0, 0 }, 0xe, { 0x0, 0x4000000, 0x4000000, 0x4000000 } }, + { { ATC_MASK, 22, 0x8, 0x3, 0, 0 }, 0xf, { 0x2, 0x2, 0x2, 0x2 } }, + { { ATC_MASK, 22, 0x10, 0x600, 0, 0 }, 0xf, { 0x600, 0x600, 0x600, 0x600 } }, + { { ATC_MASK, 22, 0x1c, 0xc00000, 0, 0 }, 0xf, { 0x800000, 0x800000, 0x800000, 0x800000 } }, + { { ATC_MASK, 19, 0xf4, 0x3000, 0, 0 }, 0xf, { 0x3000, 0x3000, 0x3000, 0x3000 } }, + { { ATC_MASK, 16, 0x188, 0x1, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 16, 0x1d4, 0x700, 0, 0 }, 0xf, { 0x300, 0x300, 0x300, 0x300 } }, + { { ATC_MASK, 19, 0x24, 0x3f0000, 0, 0 }, 0xf, { 0x2a0000, 0x2a0000, 0x2a0000, 0x2a0000 } }, + { { ATC_MASK, 19, 0x3c, 0x18000, 0, 0 }, 0xf, { 0x10000, 0x10000, 0x10000, 0x10000 } }, + { { ATC_MASK, 19, 0x78, 0xc0000, 0, 0 }, 0xf, { 0x80000, 0x80000, 0x80000, 0x80000 } }, + { { ATC_MASK, 19, 0x7c, 0xc0, 0, 0 }, 0xf, { 0x80, 0x80, 0x80, 0x80 } }, + { { ATC_MASK, 19, 0x84, 0xc000, 0, 0 }, 0xf, { 0x8000, 0x8000, 0x8000, 0x8000 } }, + { { ATC_MASK, 19, 0xd4, 0x600000, 0, 0 }, 0xf, { 0x400000, 0x400000, 0x400000, 0x400000 } }, + { { ATC_MASK, 19, 0xd8, 0x300, 0, 0 }, 0xf, { 0x200, 0x200, 0x200, 0x200 } }, + { { ATC_MASK, 19, 0x40, 0xc00, 0, 0 }, 0xf, { 0x800, 0x800, 0x800, 0x800 } }, + { { ATC_MASK, 19, 0x48, 0x600, 0, 0 }, 0xf, { 0x400, 0x400, 0x400, 0x400 } }, + { { ATC_MASK, 19, 0x28, 0x1ff030, 0, 0 }, 0xf, { 0x11c020, 0x11c020, 0x11c020, 0x11c020 } }, + { { ATC_MASK, 19, 0x2c, 0xfc, 0, 0 }, 0xf, { 0xb0, 0xb0, 0xb0, 0xb0 } }, + { { ATC_MASK, 19, 0x28, 0xe00, 0, 0 }, 0xf, { 0x800, 0x800, 0x800, 0x800 } }, + { { ATC_MASK, 19, 0x28, 0x1c0, 0, 0 }, 0xf, { 0x180, 0x180, 0x180, 0x180 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 2, 0x10, 0x10, 0, 0 }, 0xf, { 0x10, 0x10, 0x10, 0x10 } }, + { { ATC_MASK, 2, 0x10, 0x40, 0, 0 }, 0xf, { 0x40, 0x40, 0x40, 0x40 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 2, 0x10, 0x1, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 2, 0x10, 0x4, 0, 0 }, 0xf, { 0x4, 0x4, 0x4, 0x4 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 2, 0xc, 0x400, 0, 0 }, 0xf, { 0x400, 0x400, 0x400, 0x400 } }, + { { ATC_MASK, 2, 0xc, 0x1000, 0, 0 }, 0xf, { 0x1000, 0x1000, 0x1000, 0x1000 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 2, 0xc, 0x40, 0, 0 }, 0xf, { 0x40, 0x40, 0x40, 0x40 } }, + { { ATC_MASK, 2, 0xc, 0x100, 0, 0 }, 0xf, { 0x100, 0x100, 0x100, 0x100 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 2, 0xc, 0x4000, 0, 0 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 2, 0xc, 0x10000, 0, 0 }, 0xf, { 0x10000, 0x10000, 0x10000, 0x10000 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 16, 0x1d0, 0x3, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 16, 0x1d0, 0x2, 0, 0 }, 0xf, { 0x2, 0x2, 0x2, 0x2 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 19, 0x40, 0x30000, 0, 0 }, 0xf, { 0x30000, 0x30000, 0x30000, 0x30000 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 19, 0x40, 0xc000, 0, 0 }, 0xf, { 0xc000, 0xc000, 0xc000, 0xc000 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 19, 0x28, 0x3010, 0, 0 }, 0xf, { 0x1010, 0x1010, 0x1010, 0x1010 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 19, 0x28, 0x600, 0, 0 }, 0xf, { 0x200, 0x200, 0x200, 0x200 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 19, 0x28, 0x3000, 0, 0 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 19, 0x24, 0x30000000, 0, 0 }, 0xf, { 0x30000000, 0x30000000, 0x30000000, 0x30000000 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 19, 0x24, 0xc0000000, 0, 0 }, 0xf, { 0xc0000000, 0xc0000000, 0xc0000000, 0xc0000000 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 19, 0x28, 0x600, 0, 0 }, 0xf, { 0x600, 0x600, 0x600, 0x600 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 19, 0x28, 0x600, 0, 0 }, 0xf, { 0x400, 0x400, 0x400, 0x400 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 19, 0x28, 0x600, 0, 0 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x2, 0x2, 0x2, 0x2 } }, + { { ATC_MASK, 19, 0x44, 0x18000000, 0, 0 }, 0xf, { 0x18000000, 0x18000000, 0x18000000, 0x18000000 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x2, 0x2, 0x2, 0x2 } }, + { { ATC_MASK, 19, 0x40, 0x38, 0, 0 }, 0x1, { 0x28, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 19, 0x40, 0x38, 0, 0 }, 0xe, { 0x0, 0x20, 0x20, 0x20 } }, + { { ATC_MASK, 19, 0x44, 0x60000000, 0, 0 }, 0xf, { 0x40000000, 0x40000000, 0x40000000, 0x40000000 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 19, 0x44, 0x6000000, 0, 0 }, 0xf, { 0x6000000, 0x6000000, 0x6000000, 0x6000000 } }, + { { ATC_MASK, 19, 0x40, 0xc000, 0, 0 }, 0xf, { 0x8000, 0x8000, 0x8000, 0x8000 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 20, 0x68, 0xc00f, 0, 0 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 20, 0xac, 0x20000000, 0, 0 }, 0xf, { 0x20000000, 0x20000000, 0x20000000, 0x20000000 } }, + { { ATC_MASK, 20, 0xa8, 0x7ffff, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 20, 0x18, 0x38, 0, 0 }, 0xf, { 0x20, 0x20, 0x20, 0x20 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 14, 0x0, 0x1, 0, 0 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_POLL, 39, 0x1c, 0x1, 0, 5000 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, +}; + +static const struct atc_op dp_group1_off[] = { + { ATC_MASK, 39, 0x14, 0x1007, 0x7, 0 }, + { ATC_MASK, 39, 0x18, 0x1, 0x0, 0 }, + { ATC_POLL, 39, 0x1c, 0x1, 0x0, 5000 }, + { ATC_MASK, 14, 0x0, 0x1, 0x1, 0 }, + { ATC_MASK, 31, 0x1c, 0xc00000, 0xc00000, 0 }, + { ATC_MASK, 31, 0x10, 0x600, 0x400, 0 }, + { ATC_MASK, 31, 0x4, 0x30000000, 0x20000000, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 31, 0x44, 0x100000, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 31, 0x44, 0x400000, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 31, 0x44, 0x4000000, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 31, 0x44, 0x1000000, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 31, 0x44, 0x1000, 0x0, 0 }, + { ATC_MASK, 2, 0x120, 0x200, 0x200, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0x120, 0x2, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0x120, 0x20, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 28, 0x44, 0x6000000, 0x4000000, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 28, 0x44, 0x18000000, 0x10000000, 0 }, + { ATC_MASK, 28, 0x24, 0xc0000000, 0x80000000, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 28, 0x24, 0x30000000, 0x20000000, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 28, 0x28, 0x10, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 28, 0x40, 0xc000, 0xc000, 0 }, + { ATC_MASK, 25, 0x1d0, 0x3, 0x1, 0 }, + { ATC_MASK, 28, 0x40, 0x10000, 0x0, 0 }, + { ATC_MASK, 2, 0xc, 0x8000, 0x8000, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0xc, 0x80, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0xc, 0x800, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0x10, 0x2, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0x10, 0x20, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 14, 0x0, 0xc, 0x8, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0x160, 0x400000, 0x400000, 0 }, + { ATC_MASK, 3, 0x8, 0x4, 0x4, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0x160, 0x800000, 0x0, 0 }, + { ATC_MASK, 3, 0x8, 0x8, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0x160, 0x800000, 0x800000, 0 }, + { ATC_MASK, 3, 0x8, 0x8, 0x8, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 2, 0x160, 0x400000, 0x0, 0 }, + { ATC_MASK, 3, 0x8, 0x4, 0x0, 0 }, + { ATC_DELAY, 0, 0x0, 0x0, 0x1, 0 }, + { ATC_MASK, 6, 0x1c, 0x7, 0x1, 0 }, + { ATC_MASK, 6, 0x1c, 0x8, 0x8, 0 }, + { ATC_POLL, 6, 0x2c, 0xff00, 0x100, 500000 }, + { ATC_MASK, 6, 0x1c, 0x8, 0x0, 0 }, + { ATC_POLL, 6, 0x2c, 0xff00, 0x0, 500000 }, + { ATC_POLL, 14, 0x34, 0x8, 0x0, 500000 }, + { ATC_DELAY, 0, 0x0, 0x0, 0xa, 0 }, + { ATC_MASK, 6, 0x34, 0xf, 0x1, 0 }, + { ATC_MASK, 6, 0x1c, 0x7, 0x0, 0 }, + { ATC_MASK, 6, 0x1c, 0x8, 0x8, 0 }, + { ATC_POLL, 6, 0x2c, 0xff00, 0x100, 500000 }, + { ATC_MASK, 6, 0x1c, 0x8, 0x0, 0 }, + { ATC_POLL, 6, 0x2c, 0xff00, 0x0, 500000 }, + { ATC_MASK, 8, 0x0, 0xaaa, 0x0, 0 }, +}; + +static const struct atc_rate_op dp_group1_on[] = { + { { ATC_MASK, 39, 0x14, 0x7, 0, 0 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 39, 0x18, 0x1, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 2, 0x6c, 0xe0, 0, 0 }, 0xf, { 0x40, 0x40, 0x40, 0x40 } }, + { { ATC_MASK, 14, 0x0, 0x40fff0, 0, 0 }, 0xf, { 0x1110, 0x1110, 0x1110, 0x1110 } }, + { { ATC_MASK, 8, 0x34, 0x3, 0, 0 }, 0x1, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 7, 0x80, 0xffffffff, 0, 0 }, 0x1, { 0x1e0e021c, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 7, 0x84, 0xfffffff, 0, 0 }, 0x1, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 7, 0x88, 0x7fffff, 0, 0 }, 0x1, { 0x156600, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 8, 0x8, 0x1f0000, 0, 0 }, 0x1, { 0x70000, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 8, 0x18, 0x1, 0, 0 }, 0x1, { 0x1, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 8, 0x34, 0x3, 0, 0 }, 0x2, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 7, 0x80, 0xffffffff, 0, 0 }, 0x2, { 0x0, 0x1e0e01c2, 0x0, 0x0 } }, + { { ATC_MASK, 7, 0x84, 0xfffffff, 0, 0 }, 0x2, { 0x0, 0x7fffffe, 0x0, 0x0 } }, + { { ATC_MASK, 7, 0x88, 0x7fffff, 0, 0 }, 0x2, { 0x0, 0x155200, 0x0, 0x0 } }, + { { ATC_MASK, 8, 0x8, 0x1f0000, 0, 0 }, 0x2, { 0x0, 0x70000, 0x0, 0x0 } }, + { { ATC_MASK, 8, 0x18, 0x1, 0, 0 }, 0x2, { 0x0, 0x1, 0x0, 0x0 } }, + { { ATC_MASK, 8, 0x34, 0x3, 0, 0 }, 0x4, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 7, 0x80, 0xffffffff, 0, 0 }, 0x4, { 0x0, 0x0, 0x1e0e01c2, 0x0 } }, + { { ATC_MASK, 7, 0x84, 0xfffffff, 0, 0 }, 0x4, { 0x0, 0x0, 0x7fffffe, 0x0 } }, + { { ATC_MASK, 7, 0x88, 0x7fffff, 0, 0 }, 0x4, { 0x0, 0x0, 0x554800, 0x0 } }, + { { ATC_MASK, 8, 0x8, 0x1f0000, 0, 0 }, 0x4, { 0x0, 0x0, 0x70000, 0x0 } }, + { { ATC_MASK, 8, 0x18, 0x1, 0, 0 }, 0x4, { 0x0, 0x0, 0x1, 0x0 } }, + { { ATC_MASK, 8, 0x34, 0x3, 0, 0 }, 0x8, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 7, 0x80, 0xffffffff, 0, 0 }, 0x8, { 0x0, 0x0, 0x0, 0x1e0e02a3 } }, + { { ATC_MASK, 7, 0x84, 0xfffffff, 0, 0 }, 0x8, { 0x0, 0x0, 0x0, 0xbff7ffc } }, + { { ATC_MASK, 7, 0x88, 0x7fffff, 0, 0 }, 0x8, { 0x0, 0x0, 0x0, 0x564800 } }, + { { ATC_MASK, 8, 0x8, 0x1f0000, 0, 0 }, 0x8, { 0x0, 0x0, 0x0, 0x70000 } }, + { { ATC_MASK, 8, 0x18, 0x1, 0, 0 }, 0x8, { 0x0, 0x0, 0x0, 0x1 } }, + { { ATC_MASK, 8, 0x0, 0xaaa, 0, 0 }, 0xf, { 0xaaa, 0xaaa, 0xaaa, 0xaaa } }, + { { ATC_MASK, 6, 0x30, 0x4000, 0, 0 }, 0xf, { 0x4000, 0x4000, 0x4000, 0x4000 } }, + { { ATC_MASK, 6, 0x34, 0xf, 0, 0 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 6, 0x18, 0x1fe000, 0, 0 }, 0xf, { 0x2000, 0x2000, 0x2000, 0x2000 } }, + { { ATC_MASK, 6, 0x20, 0x1, 0, 0 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 6, 0x1c, 0x7, 0, 0 }, 0xf, { 0x7, 0x7, 0x7, 0x7 } }, + { { ATC_MASK, 6, 0x1c, 0x8, 0, 0 }, 0xf, { 0x8, 0x8, 0x8, 0x8 } }, + { { ATC_POLL, 6, 0x2c, 0xff00, 0, 500000 }, 0xf, { 0x100, 0x100, 0x100, 0x100 } }, + { { ATC_MASK, 6, 0x1c, 0x8, 0, 0 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_POLL, 6, 0x2c, 0xff00, 0, 500000 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_POLL, 14, 0x34, 0x8, 0, 500000 }, 0xf, { 0x8, 0x8, 0x8, 0x8 } }, + { { ATC_MASK, 28, 0x3c, 0x18000, 0, 0 }, 0xf, { 0x10000, 0x10000, 0x10000, 0x10000 } }, + { { ATC_MASK, 14, 0x0, 0xc, 0, 0 }, 0xf, { 0xc, 0xc, 0xc, 0xc } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 2, 0x120, 0x20, 0, 0 }, 0xf, { 0x20, 0x20, 0x20, 0x20 } }, + { { ATC_MASK, 2, 0x120, 0x80, 0, 0 }, 0xf, { 0x80, 0x80, 0x80, 0x80 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 2, 0x120, 0x2, 0, 0 }, 0xf, { 0x2, 0x2, 0x2, 0x2 } }, + { { ATC_MASK, 2, 0x120, 0x8, 0, 0 }, 0xf, { 0x8, 0x8, 0x8, 0x8 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 2, 0x120, 0x200, 0, 0 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 2, 0x120, 0x800, 0, 0 }, 0xf, { 0x800, 0x800, 0x800, 0x800 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 31, 0x44, 0xfffffc0, 0, 0 }, 0xf, { 0xaafb800, 0xaafb800, 0xaafb800, 0xaafb800 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 31, 0x44, 0x1000000, 0, 0 }, 0xf, { 0x1000000, 0x1000000, 0x1000000, 0x1000000 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 31, 0x44, 0x4000000, 0, 0 }, 0xf, { 0x4000000, 0x4000000, 0x4000000, 0x4000000 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 31, 0x44, 0x7c0, 0, 0 }, 0xf, { 0x300, 0x300, 0x300, 0x300 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 31, 0x44, 0x6c000, 0, 0 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 30, 0x100, 0x100000, 0, 0 }, 0xf, { 0x100000, 0x100000, 0x100000, 0x100000 } }, + { { ATC_MASK, 30, 0x58, 0xc00f, 0, 0 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 30, 0x104, 0x20000000, 0, 0 }, 0xf, { 0x20000000, 0x20000000, 0x20000000, 0x20000000 } }, + { { ATC_MASK, 30, 0x100, 0x7ffff, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 31, 0x44, 0x100000, 0, 0 }, 0x7, { 0x100000, 0x100000, 0x100000, 0x0 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0x7, { 0x1, 0x1, 0x1, 0x0 } }, + { { ATC_MASK, 31, 0x44, 0x400000, 0, 0 }, 0x7, { 0x400000, 0x400000, 0x400000, 0x0 } }, + { { ATC_MASK, 31, 0x4, 0x3000000, 0, 0 }, 0xf, { 0x2000000, 0x2000000, 0x2000000, 0x2000000 } }, + { { ATC_MASK, 31, 0x4, 0x30000000, 0, 0 }, 0xf, { 0x30000000, 0x30000000, 0x30000000, 0x30000000 } }, + { { ATC_MASK, 31, 0xc, 0x7000000, 0, 0 }, 0x1, { 0x5000000, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 31, 0xc, 0x7000000, 0, 0 }, 0xe, { 0x0, 0x4000000, 0x4000000, 0x4000000 } }, + { { ATC_MASK, 31, 0x8, 0x3, 0, 0 }, 0xf, { 0x2, 0x2, 0x2, 0x2 } }, + { { ATC_MASK, 31, 0x10, 0x600, 0, 0 }, 0xf, { 0x600, 0x600, 0x600, 0x600 } }, + { { ATC_MASK, 31, 0x1c, 0xc00000, 0, 0 }, 0xf, { 0x800000, 0x800000, 0x800000, 0x800000 } }, + { { ATC_MASK, 28, 0xf4, 0x3000, 0, 0 }, 0xf, { 0x3000, 0x3000, 0x3000, 0x3000 } }, + { { ATC_MASK, 25, 0x188, 0x1, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 25, 0x1d4, 0x700, 0, 0 }, 0xf, { 0x300, 0x300, 0x300, 0x300 } }, + { { ATC_MASK, 28, 0x24, 0x3f0000, 0, 0 }, 0xf, { 0x2a0000, 0x2a0000, 0x2a0000, 0x2a0000 } }, + { { ATC_MASK, 28, 0x3c, 0x18000, 0, 0 }, 0xf, { 0x10000, 0x10000, 0x10000, 0x10000 } }, + { { ATC_MASK, 28, 0x78, 0xc0000, 0, 0 }, 0xf, { 0x80000, 0x80000, 0x80000, 0x80000 } }, + { { ATC_MASK, 28, 0x7c, 0xc0, 0, 0 }, 0xf, { 0x80, 0x80, 0x80, 0x80 } }, + { { ATC_MASK, 28, 0x84, 0xc000, 0, 0 }, 0xf, { 0x8000, 0x8000, 0x8000, 0x8000 } }, + { { ATC_MASK, 28, 0xd4, 0x600000, 0, 0 }, 0xf, { 0x400000, 0x400000, 0x400000, 0x400000 } }, + { { ATC_MASK, 28, 0xd8, 0x300, 0, 0 }, 0xf, { 0x200, 0x200, 0x200, 0x200 } }, + { { ATC_MASK, 28, 0x40, 0xc00, 0, 0 }, 0xf, { 0x800, 0x800, 0x800, 0x800 } }, + { { ATC_MASK, 28, 0x48, 0x600, 0, 0 }, 0xf, { 0x400, 0x400, 0x400, 0x400 } }, + { { ATC_MASK, 28, 0x28, 0x1ff030, 0, 0 }, 0xf, { 0x11c020, 0x11c020, 0x11c020, 0x11c020 } }, + { { ATC_MASK, 28, 0x2c, 0xfc, 0, 0 }, 0xf, { 0xb0, 0xb0, 0xb0, 0xb0 } }, + { { ATC_MASK, 28, 0x28, 0xe00, 0, 0 }, 0xf, { 0x800, 0x800, 0x800, 0x800 } }, + { { ATC_MASK, 28, 0x28, 0x1c0, 0, 0 }, 0xf, { 0x180, 0x180, 0x180, 0x180 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 2, 0x10, 0x20, 0, 0 }, 0xf, { 0x20, 0x20, 0x20, 0x20 } }, + { { ATC_MASK, 2, 0x10, 0x80, 0, 0 }, 0xf, { 0x80, 0x80, 0x80, 0x80 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 2, 0x10, 0x2, 0, 0 }, 0xf, { 0x2, 0x2, 0x2, 0x2 } }, + { { ATC_MASK, 2, 0x10, 0x8, 0, 0 }, 0xf, { 0x8, 0x8, 0x8, 0x8 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 2, 0xc, 0x800, 0, 0 }, 0xf, { 0x800, 0x800, 0x800, 0x800 } }, + { { ATC_MASK, 2, 0xc, 0x2000, 0, 0 }, 0xf, { 0x2000, 0x2000, 0x2000, 0x2000 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 2, 0xc, 0x80, 0, 0 }, 0xf, { 0x80, 0x80, 0x80, 0x80 } }, + { { ATC_MASK, 2, 0xc, 0x200, 0, 0 }, 0xf, { 0x200, 0x200, 0x200, 0x200 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 2, 0xc, 0x8000, 0, 0 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 2, 0xc, 0x20000, 0, 0 }, 0xf, { 0x20000, 0x20000, 0x20000, 0x20000 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 25, 0x1d0, 0x3, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 25, 0x1d0, 0x2, 0, 0 }, 0xf, { 0x2, 0x2, 0x2, 0x2 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 28, 0x40, 0x30000, 0, 0 }, 0xf, { 0x30000, 0x30000, 0x30000, 0x30000 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 28, 0x40, 0xc000, 0, 0 }, 0xf, { 0xc000, 0xc000, 0xc000, 0xc000 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 28, 0x28, 0x3010, 0, 0 }, 0xf, { 0x1010, 0x1010, 0x1010, 0x1010 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 28, 0x28, 0x600, 0, 0 }, 0xf, { 0x200, 0x200, 0x200, 0x200 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 28, 0x28, 0x3000, 0, 0 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 28, 0x24, 0x30000000, 0, 0 }, 0xf, { 0x30000000, 0x30000000, 0x30000000, 0x30000000 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 28, 0x24, 0xc0000000, 0, 0 }, 0xf, { 0xc0000000, 0xc0000000, 0xc0000000, 0xc0000000 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 28, 0x28, 0x600, 0, 0 }, 0xf, { 0x600, 0x600, 0x600, 0x600 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 28, 0x28, 0x600, 0, 0 }, 0xf, { 0x400, 0x400, 0x400, 0x400 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 28, 0x28, 0x600, 0, 0 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x2, 0x2, 0x2, 0x2 } }, + { { ATC_MASK, 28, 0x44, 0x18000000, 0, 0 }, 0xf, { 0x18000000, 0x18000000, 0x18000000, 0x18000000 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x2, 0x2, 0x2, 0x2 } }, + { { ATC_MASK, 28, 0x40, 0x38, 0, 0 }, 0x1, { 0x28, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 28, 0x40, 0x38, 0, 0 }, 0xe, { 0x0, 0x20, 0x20, 0x20 } }, + { { ATC_MASK, 28, 0x44, 0x60000000, 0, 0 }, 0xf, { 0x40000000, 0x40000000, 0x40000000, 0x40000000 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 28, 0x44, 0x6000000, 0, 0 }, 0xf, { 0x6000000, 0x6000000, 0x6000000, 0x6000000 } }, + { { ATC_MASK, 28, 0x40, 0xc000, 0, 0 }, 0xf, { 0x8000, 0x8000, 0x8000, 0x8000 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 29, 0x68, 0xc00f, 0, 0 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_MASK, 29, 0xac, 0x20000000, 0, 0 }, 0xf, { 0x20000000, 0x20000000, 0x20000000, 0x20000000 } }, + { { ATC_MASK, 29, 0xa8, 0x7ffff, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 29, 0x18, 0x38, 0, 0 }, 0xf, { 0x20, 0x20, 0x20, 0x20 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_DELAY, 0, 0x0, 0x0, 0, 0 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, + { { ATC_MASK, 14, 0x0, 0x1, 0, 0 }, 0xf, { 0x0, 0x0, 0x0, 0x0 } }, + { { ATC_POLL, 39, 0x1c, 0x1, 0, 5000 }, 0xf, { 0x1, 0x1, 0x1, 0x1 } }, +}; diff --git a/drivers/phy/apple/t6040-usb2.c b/drivers/phy/apple/t6040-usb2.c index 20fb5e9da5cb0..5425cdfa25d38 100644 --- a/drivers/phy/apple/t6040-usb2.c +++ b/drivers/phy/apple/t6040-usb2.c @@ -106,7 +106,7 @@ #define T6040_PIPEHANDLER_DUMMY_PHY_READY BIT(15) /* - * DisplayPort (PHY mode 3). Offsets are into the "core" and "cfg" windows + * DisplayPort (four-lane mode 3 and two-lane mode 4). Offsets are into the "core" and "cfg" windows * the USB3 path uses; names follow the paired AppleT6040TypeCPhy driver. */ #define T6040_CORE_AUX_CTRL 0x28000 /* ACIOPHY_AUX_TOP_BLK_AUX_CTRL */ @@ -242,12 +242,15 @@ struct t6040_usb2_phy { bool usb3_lent; /* DP mode took the lanes of this Type-C port */ bool pipe_switched; /* the controller's PIPE runs on this PHY */ - /* DisplayPort (PHY mode 3); dp_ok is false when the DT lacks a DP route */ + /* DisplayPort; dp_ok is false when the DT lacks a DP route */ struct phy *dp; struct typec_mux_dev *mux; bool dp_ok; bool dp_up; /* DP mode is programmed on the PHY */ - bool dp_alt; /* the Type-C port is in DP alt mode, pin assignment C/E */ + bool dp_alt; /* the Type-C port is in DP alt mode */ + bool dp_two_lanes; /* pin D keeps USB3 on the other pair */ + bool dp_mode_two_lanes; /* latched until DP power_off, even after mux exit */ + bool dp_mode_flip; bool dp_flip; /* plug orientation reversed for the DP lanes */ int dp_rate; /* link rate index of the running link sequence, -1 = off */ }; @@ -952,6 +955,9 @@ static void t6040_usb3_down_locked(struct t6040_usb2_phy *tphy) t6040_usb3_pipe_park(tphy); tphy->pipe_switched = false; } + /* DWC3 can drop its role before the DP fabric has released pin D. */ + if (tphy->dp_up && tphy->dp_mode_two_lanes) + return; if (tphy->usb3_up) { u32 uc = readl(t6040_core(tphy) + T6040_CORE_UC_CTRL); @@ -1272,6 +1278,7 @@ struct atc_rate_op { }; #include "atc-t6050-dp-data.h" +#include "atc-t6040-dp2-data.h" static const struct { u8 bank; u32 offset; } t6040_dp_core_banks[] = { {2, 0}, {3, 0x400}, {4, 0x800}, {5, 0xa00}, {6, 0x1000}, @@ -1325,12 +1332,29 @@ static int t6040_dp_seq_step(struct t6040_usb2_phy *tphy, const struct atc_op *s /* Take the link down if it is up, then bring it up at rate index @idx (-1: off). */ static int t6040_dp_set_rate(struct t6040_usb2_phy *tphy, int idx) { + const struct atc_op *off = dp_link_off; + const struct atc_rate_op *on = dp_link_on; + size_t nr_off = ARRAY_SIZE(dp_link_off); + size_t nr_on = ARRAY_SIZE(dp_link_on); size_t i; int ret; + if (tphy->dp_mode_two_lanes) { + if (tphy->dp_mode_flip) { + off = dp_group0_off; + on = dp_group0_on; + nr_off = ARRAY_SIZE(dp_group0_off); + nr_on = ARRAY_SIZE(dp_group0_on); + } else { + off = dp_group1_off; + on = dp_group1_on; + nr_off = ARRAY_SIZE(dp_group1_off); + nr_on = ARRAY_SIZE(dp_group1_on); + } + } if (tphy->dp_rate >= 0) { - for (i = 0; i < ARRAY_SIZE(dp_link_off); i++) { - ret = t6040_dp_seq_step(tphy, &dp_link_off[i]); + for (i = 0; i < nr_off; i++) { + ret = t6040_dp_seq_step(tphy, &off[i]); if (ret) return ret; } @@ -1338,12 +1362,12 @@ static int t6040_dp_set_rate(struct t6040_usb2_phy *tphy, int idx) } if (idx < 0) return 0; - for (i = 0; i < ARRAY_SIZE(dp_link_on); i++) { - struct atc_op op = dp_link_on[i].op; + for (i = 0; i < nr_on; i++) { + struct atc_op op = on[i].op; - if (!(dp_link_on[i].rates & BIT(idx))) + if (!(on[i].rates & BIT(idx))) continue; - op.value = dp_link_on[i].values[idx]; + op.value = on[i].values[idx]; ret = t6040_dp_seq_step(tphy, &op); if (ret) return ret; @@ -1359,7 +1383,8 @@ static int t6040_dp_set_rate(struct t6040_usb2_phy *tphy, int idx) /* Drive settings of the T6050 sequence: lanes 0/1 on PMA group 1, 2/3 on group 0. */ static void t6040_dp_drive(struct t6040_usb2_phy *tphy, unsigned int lane, u32 preset) { - unsigned int group = lane < 2 ? 1 : 0; + unsigned int group = (lane < 2 ? 1 : 0) ^ + (tphy->dp_mode_two_lanes && tphy->dp_mode_flip); bool tx = lane & 1; void __iomem *base = t6040_dp_bank(tphy, (tx ? 21 : 20) + 9 * group); u32 ctrl = ((preset >> 2) & 0xf) | ((preset & 3) << 14); @@ -1407,11 +1432,25 @@ static int t6040_dp_power_on(struct phy *phy) guard(mutex)(&tphy->lock); if (!tphy->dp_ok || tphy->dp_up) return 0; - /* - * A Type-C PHY in DP alt mode (pin assignment C/E) hands all four - * lanes to DP: aciophy_phy_shutdown of the USB3 side first, then the - * same mode-3 init as the fixed DP PHY. - */ + /* Pin D shares the running PHY with USB3. Keep its PIPE and firmware. */ + if (tphy->dp_two_lanes) { + if (!tphy->usb3_ok || !tphy->dp_alt) + return -ENOLINK; + if (!tphy->usb3_up) + t6040_usb3_up_locked(tphy); + if (!tphy->usb3_up) + return -ENOLINK; + t6040_dp_fixed_tunables(t6040_core(tphy)); + t6040_dp_aux_init(tphy); + tphy->dp_rate = -1; + tphy->dp_mode_two_lanes = true; + tphy->dp_mode_flip = tphy->dp_flip; + tphy->dp_up = true; + dev_info(tphy->dev, "DP PHY pin D: two lanes, USB3 retained, %s orientation\n", + tphy->dp_flip ? "reverse" : "normal"); + return 0; + } + /* C/E and the fixed HDMI PHY hand all four lanes to DP. */ if (!t6040_dp_only(tphy)) { t6040_usb3_down_locked(tphy); tphy->usb3_lent = true; @@ -1423,6 +1462,7 @@ static int t6040_dp_power_on(struct phy *phy) } t6040_dp_mode_setup(tphy, tphy->dp_flip); tphy->dp_rate = -1; + tphy->dp_mode_two_lanes = false; tphy->dp_up = true; dev_dbg(tphy->dev, "DP PHY mode set, lane map %s\n", tphy->dp_flip ? "reversed" : "normal"); @@ -1432,22 +1472,33 @@ static int t6040_dp_power_on(struct phy *phy) static int t6040_dp_power_off(struct phy *phy) { struct t6040_usb2_phy *tphy = phy_get_drvdata(phy); + int ret = 0; guard(mutex)(&tphy->lock); + if (tphy->dp_up && tphy->dp_mode_two_lanes) { + ret = t6040_dp_set_rate(tphy, -1); + if (ret) + return ret; + } tphy->dp_up = false; + if (tphy->dp_mode_two_lanes && !tphy->usb3_on) + t6040_usb3_down_locked(tphy); + tphy->dp_mode_two_lanes = false; t6040_usb3_reclaim_locked(tphy); - return 0; + return ret; } -/* GET_MAX_LANE_COUNT: DP on all four lanes in mode 3. */ +/* GET_MAX_LANE_COUNT follows the negotiated pin assignment. */ static int t6040_dp_validate(struct phy *phy, enum phy_mode mode, int submode, union phy_configure_opts *opts_) { + struct t6040_usb2_phy *tphy = phy_get_drvdata(phy); struct phy_configure_opts_dp *opts = &opts_->dp; if (mode != PHY_MODE_DP) return -EINVAL; - opts->lanes = 4; + guard(mutex)(&tphy->lock); + opts->lanes = tphy->dp_two_lanes ? 2 : 4; return 0; } @@ -1461,6 +1512,9 @@ static int t6040_dp_configure(struct phy *phy, union phy_configure_opts *opts_) guard(mutex)(&tphy->lock); if (!tphy->dp_up) return -EINVAL; + if ((opts->set_lanes || opts->set_voltages) && + opts->lanes > (tphy->dp_mode_two_lanes ? 2 : 4)) + return -EINVAL; if (opts->set_rate) { switch (opts->link_rate) { @@ -1491,7 +1545,7 @@ static int t6040_dp_configure(struct phy *phy, union phy_configure_opts *opts_) if (opts->set_voltages && tphy->dp_rate >= 0) { unsigned int i; - for (i = 0; i < opts->lanes && i < 4; i++) { + for (i = 0; i < opts->lanes; i++) { unsigned int v = min_t(unsigned int, opts->voltage[i], 3); unsigned int pe = min_t(unsigned int, opts->pre[i], 3); @@ -1530,10 +1584,9 @@ static int t6040_dp_mux_set(struct typec_mux_dev *mux, struct typec_mux_state *s tphy->dp_flip = (tphy->orientation == TYPEC_ORIENTATION_REVERSE); /* * Latch the DP alt mode state for the DCP's Type-C route. Pin - * assignments C and E put DP on all four lanes (PHY mode 3, as the - * fixed DP route); D (two DP lanes next to USB3, PHY mode 4 with the - * coprocessor) is not supported. The DP lanes themselves are brought - * up by the DCP through phy_power_on()/phy_configure() on the DP phy. + * assignments C and E put DP on all four lanes. D leaves the USB3 + * pair with its running coprocessor and gives DP the other pair. + * The DCP brings the DP lanes up through phy_power_on()/phy_configure(). */ if (dp) { switch (state->mode) { @@ -1542,16 +1595,17 @@ static int t6040_dp_mux_set(struct typec_mux_dev *mux, struct typec_mux_state *s alt = true; break; case TYPEC_DP_STATE_D: - dev_warn_once(tphy->dev, "Type-C mux: DP pin assignment D (two lanes next to USB3) not supported\n"); + alt = true; break; default: break; } } tphy->dp_alt = alt; + tphy->dp_two_lanes = alt && state->mode == TYPEC_DP_STATE_D; dev_dbg(tphy->dev, "Type-C mux: %s state %lu -> DP alt mode %d, lane map %s\n", dp ? "DP" : "non-DP", state->mode, alt, tphy->dp_flip ? "reversed" : "normal"); - if (alt && tphy->dp_up) + if (alt && tphy->dp_up && !tphy->dp_mode_two_lanes && !tphy->dp_two_lanes) t6040_dp_xbar_config(tphy, tphy->dp_flip); t6040_usb3_reclaim_locked(tphy); return 0; From 31340abccfcc3f7f9669fa327d8771a3cadd4f3f Mon Sep 17 00:00:00 2001 From: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com> Date: Tue, 27 Oct 2026 23:31:51 +1100 Subject: [PATCH 347/388] phy: apple: t6040: avoid rewriting live USB3 tunables for DP pin D --- drivers/phy/apple/t6040-usb2.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/drivers/phy/apple/t6040-usb2.c b/drivers/phy/apple/t6040-usb2.c index 5425cdfa25d38..cb4146d91faf1 100644 --- a/drivers/phy/apple/t6040-usb2.c +++ b/drivers/phy/apple/t6040-usb2.c @@ -1039,8 +1039,8 @@ static void t6040_usb3_switch_unregister(void *sw) * link rate through phy_configure(): the per-rate link sequence is the T6050 * fixed-DP table (atc-t6050-dp-data.h; the T6050 ADT names its PHY * "atc-phy,t6050", "atc-phy,t6040"), followed by the T6040 DP clock switch in - * the "cfg" window. Pin assignment D (two DP lanes next to USB3) is not - * supported. + * the "cfg" window. Pin assignment D shares USB3's running PHY and uses + * the two-lane group link sequences below. */ /* The fixed DPTX tunable writes (apply_dptx_tunables, shared with USB3). */ @@ -1440,7 +1440,7 @@ static int t6040_dp_power_on(struct phy *phy) t6040_usb3_up_locked(tphy); if (!tphy->usb3_up) return -ENOLINK; - t6040_dp_fixed_tunables(t6040_core(tphy)); + /* USB3 startup already applied these DPTX tunables before firmware. */ t6040_dp_aux_init(tphy); tphy->dp_rate = -1; tphy->dp_mode_two_lanes = true; From f70f1acd35233683c65b64a7cfee79418817b9c6 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 23:12:35 +1100 Subject: [PATCH 348/388] soc: apple: guard SEP enrolment and recover exact identities Validate enrolment metadata and durable Linux bindings before capture. Prepare an empty owner context only after userspace has validated the account. Expose Linux user-context identities and an exact-ID recovery delete, saving owner, user and master Catacombs before reporting success. Report the identity returned by the enclave at match time. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/soc/apple/bio.rs | 132 ++++++++++-- drivers/soc/apple/catacomb.rs | 6 + drivers/soc/apple/sbio.rs | 374 +++++++++++++++++++++++++++++++-- drivers/soc/apple/sep.rs | 4 + drivers/soc/apple/shim.h | 1 + drivers/soc/apple/shim.rs | 10 + drivers/soc/apple/store.rs | 6 + drivers/soc/apple/store_shim.c | 8 + include/uapi/linux/apple-sep.h | 20 +- 9 files changed, 527 insertions(+), 34 deletions(-) diff --git a/drivers/soc/apple/bio.rs b/drivers/soc/apple/bio.rs index 6b4e1edbe2966..573830b7973d0 100644 --- a/drivers/soc/apple/bio.rs +++ b/drivers/soc/apple/bio.rs @@ -6,7 +6,7 @@ use kernel::ioctl::{_IO, _IOR, _IOW, _IOWR}; use kernel::prelude::*; use kernel::uaccess::{UserPtr, UserSlice}; -pub(crate) const IFACE_VERSION: u32 = 4; +pub(crate) const IFACE_VERSION: u32 = 5; pub(crate) const ENROL_USER_ID: i32 = 1000; @@ -100,10 +100,35 @@ struct List { #[repr(C)] struct EnrolStart { flags: u32, + account_uid: u32, + finger: u32, reserved: u32, label: [u8; LABEL_LEN], } +#[repr(C)] +pub(crate) struct EnclaveList { + pub(crate) count: u32, + pub(crate) reserved: u32, + pub(crate) uuid: [[u8; UUID_LEN]; MAX_IDENTITIES], +} + +impl EnclaveList { + pub(crate) fn new() -> Self { + Self { count: 0, reserved: 0, uuid: [[0; UUID_LEN]; MAX_IDENTITIES] } + } + + pub(crate) fn push(&mut self, uuid: [u8; UUID_LEN]) -> Result<()> { + let at = self.count as usize; + if at >= MAX_IDENTITIES { + return Err(EOVERFLOW); + } + self.uuid[at] = uuid; + self.count += 1; + Ok(()) + } +} + #[repr(C)] #[derive(Default)] struct EnrolPoll { @@ -136,8 +161,8 @@ struct VerifyPoll { } #[repr(C)] -struct Delete { - uuid: [u8; UUID_LEN], +pub(crate) struct Delete { + pub(crate) uuid: [u8; UUID_LEN], } pub(crate) const ATTEST_CHALLENGE_LEN: usize = 32; @@ -162,7 +187,8 @@ pub(crate) struct Attest { static_assert!(core::mem::size_of::() == 144); static_assert!(core::mem::size_of::() == 32); static_assert!(core::mem::size_of::() == 4616); -static_assert!(core::mem::size_of::() == 136); +static_assert!(core::mem::size_of::() == 144); +static_assert!(core::mem::size_of::() == 520); static_assert!(core::mem::size_of::() == 40); static_assert!(core::mem::size_of::() == 40); static_assert!(core::mem::size_of::() == 72); @@ -182,7 +208,7 @@ macro_rules! ioctl_pod { }; } ioctl_pod! { - to_user: Info, List, EnrolPoll, VerifyPoll, Attest; + to_user: Info, List, EnclaveList, EnrolPoll, VerifyPoll, Attest; from_user: EnrolStart, VerifyStart, Delete, Attest } @@ -198,6 +224,9 @@ const IOC_CANCEL: u32 = _IO(MAGIC, 0x07); const IOC_DELETE: u32 = _IOW::(MAGIC, 0x08); const IOC_DELETE_ALL: u32 = _IO(MAGIC, 0x09); pub(crate) const IOC_ATTEST: u32 = _IOWR::(MAGIC, 0x0a); +pub(crate) const IOC_LIST_ENCLAVE: u32 = _IOR::(MAGIC, 0x0b); +pub(crate) const IOC_DELETE_ORPHAN: u32 = _IOW::(MAGIC, 0x0c); +pub(crate) const IOC_PREPARE_ENROL: u32 = _IOW::(MAGIC, 0x0d); pub(crate) struct MatchEvidence { identity: [u8; UUID_LEN], @@ -338,6 +367,10 @@ impl IdentityIndex { self.save(store) } + pub(crate) fn can_persist_next(&self, store: &Store) -> bool { + store.can_write(&identity_key(), 8 + (self.entries.len() + 1) * (UUID_LEN + LABEL_LEN)) + } + pub(crate) fn identity_v1_for( &self, uuid: &[u8; UUID_LEN], @@ -481,7 +514,7 @@ pub(crate) struct Handled { pub(crate) delete_identities: KVec, } -fn ok() -> Result { +pub(crate) fn ok() -> Result { Ok(Handled { ret: 0, wake: false, @@ -492,7 +525,7 @@ fn ok() -> Result { }) } -fn require_admin() -> Result<()> { +pub(crate) fn require_admin() -> Result<()> { if crate::shim::capable_admin() { Ok(()) } else { @@ -500,6 +533,12 @@ fn require_admin() -> Result<()> { } } +pub(crate) fn orphan_delete(identity: crate::sbio::IdentityV1) -> Result { + let mut handled = ok()?; + handled.delete_identity = Some(identity); + Ok(handled) +} + fn require_sensor(present: bool) -> Result<()> { if present { Ok(()) @@ -567,22 +606,85 @@ fn list(ctx: &mut Context<'_>, user: UserPtr) -> Result { ok() } +fn validate_enrol_request(request: &EnrolStart) -> Result<()> { + // NSS account resolution occurs in libfprint. This ABI requires its + // resolved uid, the named finger and the generated user label. + if request.flags != 0 || request.reserved != 0 + || request.account_uid == u32::MAX || !(1..=10).contains(&request.finger) + || !request.label.starts_with(b"FP1-") + || request.label[12] != b'-' || request.label[14] != b'-' + || request.label[23] != b'-' || request.label[24] == 0 + || request.label[13] != b"123456789A"[request.finger as usize - 1] + || !request.label.contains(&0) + { + return Err(EINVAL); + } + Ok(()) +} + +pub(crate) fn validate_enrol_start_arg(arg: usize) -> Result<()> { + let user = UserPtr::from_addr(arg); + let request: EnrolStart = UserSlice::new(user, core::mem::size_of::()) + .reader().read()?; + validate_enrol_request(&request) +} + +#[cfg(CONFIG_APPLE_SEP_KUNIT_TEST)] +#[::kernel::macros::kunit_tests(apple_sep_enrol_request)] +mod enrol_request_tests { + use super::*; + + fn valid() -> EnrolStart { + let mut request = EnrolStart { + flags: 0, account_uid: 1000, finger: 7, reserved: 0, + label: [0; LABEL_LEN], + }; + request.label[..28].copy_from_slice(b"FP1-20261027-7-12345678-user"); + request + } + + #[test] + fn resolved_account_and_known_finger() { + assert!(validate_enrol_request(&valid()).is_ok()); + } + + #[test] + fn missing_uid_is_refused() { + let mut request = valid(); + request.account_uid = u32::MAX; + assert!(validate_enrol_request(&request).is_err()); + } + + #[test] + fn unknown_or_mismatched_finger_is_refused() { + let mut request = valid(); + request.finger = 0; + assert!(validate_enrol_request(&request).is_err()); + request.finger = 6; + assert!(validate_enrol_request(&request).is_err()); + } + + #[test] + fn missing_username_is_refused() { + let mut request = valid(); + request.label[24] = 0; + assert!(validate_enrol_request(&request).is_err()); + } +} + fn enrol_start(ctx: &mut Context<'_>, user: UserPtr) -> Result { require_admin()?; let request: EnrolStart = UserSlice::new(user, core::mem::size_of::()) .reader() .read()?; - if request.flags != 0 || request.reserved != 0 { - return Err(EINVAL); - } + validate_enrol_request(&request)?; require_sensor(ctx.sensor_present)?; if ctx.session.worker_epoch.is_some() || !ctx.session.op.may_start() { return Err(EBUSY); } - if matches!(ctx.live_identity_count, Some(count) if count >= SEP_USER_CAPACITY) - || ctx.index.total() >= MAX_IDENTITIES - { + let count = ctx.live_identity_count.ok_or(EIO)?; + if count >= SEP_USER_CAPACITY || ctx.index.total() >= MAX_IDENTITIES { return Err(ENOSPC); } @@ -960,6 +1062,10 @@ pub(crate) fn starts_capture(cmd: u32) -> bool { cmd == IOC_ENROL_START || cmd == IOC_VERIFY_START } +pub(crate) fn idle_for_mutation(session: &Session) -> bool { + session.worker_epoch.is_none() && session.op.may_start() +} + fn delete(ctx: &mut Context<'_>, user: UserPtr) -> Result { require_admin()?; if !ctx.session.op.may_start() { diff --git a/drivers/soc/apple/catacomb.rs b/drivers/soc/apple/catacomb.rs index 8b18efb4ead77..9c60e19b5861d 100644 --- a/drivers/soc/apple/catacomb.rs +++ b/drivers/soc/apple/catacomb.rs @@ -28,6 +28,12 @@ pub(crate) fn is_kind(kind: u8) -> bool { path_for(kind).is_some() } +pub(crate) fn writable(kind: u8) -> bool { + path_for(kind) + .and_then(|path| shim::StoreFile::open_writable_existing(path).ok()) + .is_some() +} + pub(crate) fn write(kind: u8, blob: &[u8]) -> Result<()> { let path = path_for(kind).ok_or(EINVAL)?; if blob.len() > MAX_CATACOMB { diff --git a/drivers/soc/apple/sbio.rs b/drivers/soc/apple/sbio.rs index 88be672817b1c..2f72bfa2ce7cc 100644 --- a/drivers/soc/apple/sbio.rs +++ b/drivers/soc/apple/sbio.rs @@ -10,6 +10,128 @@ use crate::sks::SKS_AUTH_TOKEN_LEN; use kernel::prelude::*; use kernel::soc::apple::mailbox::Message; +struct EnrolPrerequisites { + lane: bool, + live_count: Option, + bag_bound: bool, + catacomb_readable: [bool; 3], + catacomb_writable: [bool; 3], + keybag_writable: bool, + host_store_capacity: bool, +} + +fn admit_enrol(p: &EnrolPrerequisites) -> Result<()> { + if !p.lane { return Err(ENXIO); } + let count = p.live_count.ok_or(EIO)?; + if count >= 3 { return Err(ENOSPC); } + if !p.bag_bound || p.catacomb_readable.contains(&false) { + return Err(ENODATA); + } + if p.catacomb_writable.contains(&false) || !p.keybag_writable { + return Err(EROFS); + } + if !p.host_store_capacity { return Err(ENOSPC); } + Ok(()) +} + +fn catacomb_needed_for_enrol(kind: u8, live_count: Option) -> bool { + live_count != Some(0) || kind == PRIVATE_TYPE_CATACOMB_OWNER +} + +fn admissible_orphan_target( + listed_linux: &[[u8; bio::UUID_LEN]], + uuid: &[u8; bio::UUID_LEN], +) -> Result<()> { + if *uuid == [0; bio::UUID_LEN] { return Err(EINVAL); } + if !listed_linux.contains(uuid) { return Err(ENOENT); } + Ok(()) +} + +#[cfg(CONFIG_APPLE_SEP_KUNIT_TEST)] +#[::kernel::macros::kunit_tests(apple_sep_enrol_admission)] +mod admission_tests { + use super::*; + + fn ready() -> EnrolPrerequisites { + EnrolPrerequisites { + lane: true, live_count: Some(1), bag_bound: true, + catacomb_readable: [true; 3], catacomb_writable: [true; 3], + keybag_writable: true, host_store_capacity: true, + } + } + + fn refused(p: &EnrolPrerequisites, expected: Error) { + assert_eq!(admit_enrol(p).unwrap_err().to_errno(), expected.to_errno()); + } + + #[test] + fn missing_os_identity() { + let mut p = ready(); p.lane = false; refused(&p, ENXIO); + } + + #[test] + fn unavailable_enclave_list() { + let mut p = ready(); p.live_count = None; refused(&p, EIO); + } + + #[test] + fn enclave_full() { + let mut p = ready(); p.live_count = Some(3); refused(&p, ENOSPC); + } + + #[test] + fn missing_identity_bag_binding() { + let mut p = ready(); p.bag_bound = false; refused(&p, ENODATA); + } + + #[test] + fn missing_catacomb() { + for i in 0..3 { + let mut p = ready(); p.catacomb_readable[i] = false; refused(&p, ENODATA); + } + } + + #[test] + fn unwritable_catacomb_or_keybag() { + for i in 0..3 { + let mut p = ready(); p.catacomb_writable[i] = false; refused(&p, EROFS); + } + let mut p = ready(); p.keybag_writable = false; refused(&p, EROFS); + } + + #[test] + fn host_store_full() { + let mut p = ready(); p.host_store_capacity = false; refused(&p, ENOSPC); + } + + #[test] + fn ready_binding_is_admitted() { + assert!(admit_enrol(&ready()).is_ok()); + } + + #[test] + fn clean_lane_requires_prepared_owner_before_first_capture() { + assert!(catacomb_needed_for_enrol(PRIVATE_TYPE_CATACOMB_OWNER, Some(0))); + assert!(!catacomb_needed_for_enrol(PRIVATE_TYPE_CATACOMB_USER, Some(0))); + assert!(!catacomb_needed_for_enrol(PRIVATE_TYPE_CATACOMB_MASTER, Some(0))); + assert!(catacomb_needed_for_enrol(PRIVATE_TYPE_CATACOMB_USER, Some(1))); + } + + #[test] + fn orphan_delete_requires_exact_linux_identity() { + let linux = [1u8; bio::UUID_LEN]; + let other_lane = [2u8; bio::UUID_LEN]; + assert!(admissible_orphan_target(&[linux], &linux).is_ok()); + refused_orphan(&[linux], &other_lane, ENOENT); + refused_orphan(&[linux], &[0; bio::UUID_LEN], EINVAL); + } + + fn refused_orphan(list: &[[u8; bio::UUID_LEN]], uuid: &[u8; bio::UUID_LEN], expected: Error) { + assert_eq!(admissible_orphan_target(list, uuid).unwrap_err().to_errno(), + expected.to_errno()); + } +} + impl SepData { fn sbio_enrol_step(&self, op: &crate::sbio::SbioOp) -> Option> { match self.sbio_call(op) { @@ -349,6 +471,12 @@ impl SepData { return self.enrol_into_existing_context(user); }; + // PREPARE_ENROL may already have selected the empty Linux context in + // this boot. A file left from an earlier boot is not that proof. + if self.empty_context_prepared.xchg(false, Relaxed) { + return self.enrol_into_existing_context(user); + } + self.open_fresh_context(user, &proof) } @@ -1777,6 +1905,7 @@ impl SepData { }; if !parsed.matches(user) { + dev_info!(self.dev, "verify: enclave found no identity in the Linux user context\n"); return bio::VerifyOutcome::NoMatch; } @@ -1784,6 +1913,12 @@ impl SepData { let identity = *parsed.identity_uuid(); let known = self.bio_index.lock().contains_uuid(&identity); + dev_info!( + self.dev, + "verify: enclave matched identity {} (host index {})\n", + Hex(&identity), if known { "present" } else { "absent" } + ); + if !known { dev_err!( self.dev, @@ -3403,6 +3538,115 @@ impl SepData { Some(bytes) } + fn require_linux_bio_lane(&self) -> Result<()> { + if !self.touchid_started.load(Relaxed) || self.xarm.lock().os_uuid.is_none() { + return Err(ENXIO); + } + Ok(()) + } + + /// Refuse before ENROL_START schedules work or sends BEGIN_ENROL. A + /// userspace passwd lookup proves the host account; these are the durable + /// Linux-lane bindings that only the kernel can inspect. + fn preflight_enrol(&self, live_count: Option) -> Result<()> { + let lane = self.touchid_started.load(Relaxed) && self.xarm.lock().os_uuid.is_some(); + let bag_bound = self.keybag_designated.load(Relaxed) && self.enrol_material.lock() + .as_ref().is_some_and(|material| material.snapshot_identity.is_some()); + let kinds = [PRIVATE_TYPE_CATACOMB_OWNER, PRIVATE_TYPE_CATACOMB_USER, + PRIVATE_TYPE_CATACOMB_MASTER]; + let readable = kinds.map(|kind| !catacomb_needed_for_enrol(kind, live_count) + || crate::catacomb::read(kind).is_some()); + let writable = kinds.map(|kind| !catacomb_needed_for_enrol(kind, live_count) + || crate::catacomb::writable(kind)); + let index = self.bio_index.lock(); + let preconditions = EnrolPrerequisites { + lane, + live_count, + bag_bound, + catacomb_readable: readable, + catacomb_writable: writable, + keybag_writable: shim::StoreFile::open_writable_existing( + keybag::Slot::Identity.path()).is_ok(), + host_store_capacity: self.with_host_store(|store| index.can_persist_next(store)) + .unwrap_or(false), + }; + let result = admit_enrol(&preconditions); + if let Err(e) = &result { + dev_warn!(self.dev, "enrol: prerequisites failed before enclave capture: {:?}\n", e); + } + result + } + + fn bio_list_enclave(&self, arg: usize) -> Result { + bio::require_admin()?; + self.require_linux_bio_lane()?; + let ids = self.enclave_identities_for(SBIO_PROBE_USER_ID).ok_or(EIO)?; + let mut out = bio::EnclaveList::new(); + for uuid in ids.iter() { + out.push(*uuid)?; + } + kernel::uaccess::UserSlice::new( + kernel::uaccess::UserPtr::from_addr(arg), core::mem::size_of::() + ).writer().write(&out)?; + bio::ok() + } + + fn bio_delete_orphan(&self, arg: usize) -> Result { + bio::require_admin()?; + self.require_linux_bio_lane()?; + if !bio::idle_for_mutation(&self.bio_session.lock()) { return Err(EBUSY); } + let request: bio::Delete = kernel::uaccess::UserSlice::new( + kernel::uaccess::UserPtr::from_addr(arg), core::mem::size_of::() + ).reader().read()?; + // The exact SEP user and built-in selector are checked by the parser. + // The host index may still hold an orphan after fprintd lost its file, + // so that index cannot decide whether this exact-ID recovery is valid. + let ids = self.enclave_identities_for(SBIO_PROBE_USER_ID).ok_or(EIO)?; + admissible_orphan_target(&ids, &request.uuid)?; + bio::orphan_delete(crate::sbio::IdentityV1::from_index( + SBIO_PROBE_USER_ID, request.uuid)) + } + + fn bio_prepare_enrol(&self, arg: usize) -> Result { + bio::require_admin()?; + bio::validate_enrol_start_arg(arg)?; + self.require_linux_bio_lane()?; + if !bio::idle_for_mutation(&self.bio_session.lock()) { return Err(EBUSY); } + if !self.xart_writable() { return Err(EROFS); } + let ids = self.enclave_identities_for(SBIO_PROBE_USER_ID).ok_or(EIO)?; + if !ids.is_empty() { + // An existing identity must already have a durable owner file; + // selecting SYSTEM here could disturb its live context. + return if crate::catacomb::read(PRIVATE_TYPE_CATACOMB_OWNER).is_some() { + bio::ok() + } else { Err(ENODATA) }; + } + if crate::catacomb::read(PRIVATE_TYPE_CATACOMB_OWNER).is_some() { + return bio::ok(); + } + if !self.keybag_designated.load(Relaxed) + || !self.enrol_material.lock().as_ref() + .is_some_and(|material| material.snapshot_identity.is_some()) { + return Err(ENODATA); + } + let user = crate::sbio::UserId::new(SBIO_PROBE_USER_ID).ok_or(EINVAL)?; + loop { + self.wait_until_awake()?; + let _writer = self.catacomb_writer.lock(); + if !self.capture_begin() { continue; } + let prepared = self.open_enrolment_context(user); + self.capture_end(); + if !prepared { return Err(EIO); } + break; + } + if crate::catacomb::read(PRIVATE_TYPE_CATACOMB_OWNER).is_none() + || !crate::catacomb::writable(PRIVATE_TYPE_CATACOMB_OWNER) { + return Err(EROFS); + } + self.empty_context_prepared.store(true, Relaxed); + bio::ok() + } + pub(crate) fn bio_ioctl(&self, cmd: u32, arg: usize) -> Result { if self.shutting_down.load(Relaxed) { return Err(ENODEV); @@ -3410,6 +3654,12 @@ impl SepData { if cmd == bio::IOC_ATTEST { return self.bio_attest(arg); } + if cmd == bio::IOC_LIST_ENCLAVE { + return self.bio_list_enclave(arg); + } + if cmd == bio::IOC_PREPARE_ENROL { + return self.bio_prepare_enrol(arg); + } // With xART writes off the enclave cannot persist what an enrolment or // a match changes. It saves anti-replay state after every match, and a // refused save has left it unresponsive until reboot. Refuse the @@ -3434,14 +3684,21 @@ impl SepData { let live_identity_count = if (cmd == bio::IOC_GET_INFO || cmd == bio::IOC_ENROL_START) && self.touchid_started.load(Relaxed) { - self.enclave_identity_count() + self.enclave_identities_for(SBIO_PROBE_USER_ID).map(|ids| ids.len()) } else { None }; + if cmd == bio::IOC_ENROL_START { + bio::require_admin()?; + bio::validate_enrol_start_arg(arg)?; + self.preflight_enrol(live_identity_count)?; + } // The SPI probe may have deferred until its SMC power supplier bound. // A saved attach-time snapshot would incorrectly reject later ioctls. let sensor_present = sensor::is_bound(); - let handled = { + let handled = if cmd == bio::IOC_DELETE_ORPHAN { + self.bio_delete_orphan(arg)? + } else { let mut session = self.bio_session.lock(); if bio::starts_capture(cmd) { self.wait_for_worker_exit(&mut session, false)?; @@ -3478,7 +3735,7 @@ impl SepData { if !self.capture_begin() { continue; } - let deleted = self.delete_and_persist(&handled); + let deleted = self.delete_and_persist(&handled, cmd == bio::IOC_DELETE_ORPHAN); self.capture_end(); break deleted; }; @@ -3721,7 +3978,7 @@ impl SepData { /// Called with `catacomb_writer` held. A host index entry goes only once /// its deletion is saved: if the save fails, the entry stays, userspace /// still lists the fingerprint, and a retried deletion saves again. - fn delete_and_persist(&self, handled: &bio::Handled) -> Result<()> { + fn delete_and_persist(&self, handled: &bio::Handled, recovery: bool) -> Result<()> { let retry = self.deletion_unsaved.load(Relaxed); let wanted = Iterator::chain(handled.delete_identity.iter(), handled.delete_identities.iter()); @@ -3729,10 +3986,13 @@ impl SepData { let mut gone: KVec<[u8; bio::UUID_LEN]> = KVec::with_capacity(wanted.clone().count(), GFP_KERNEL)?; let mut delete_err: Option = None; + let mut orphan_removed = false; for identity in wanted { - match self.delete_identity(identity) { + let orphan = recovery || !self.bio_index.lock().contains_uuid(identity.uuid()); + match self.delete_identity(identity, orphan) { Ok(true) => { let _ = gone.push_within_capacity(*identity.uuid()); + orphan_removed |= orphan; } Ok(false) => {} Err(e) => delete_err = delete_err.or(Some(e)), @@ -3740,7 +4000,7 @@ impl SepData { } // Save what was removed even if another deletion failed. if !gone.is_empty() { - match self.persist_deletion(retry) { + match self.persist_deletion(retry, orphan_removed) { Ok(()) => { self.deletion_unsaved.store(false, Relaxed); for uuid in gone.iter() { @@ -3765,7 +4025,7 @@ impl SepData { /// Returns whether the enclave no longer holds the identity and the host /// entry must wait for [`Self::persist_deletion`]. - fn delete_identity(&self, identity: &crate::sbio::IdentityV1) -> Result { + fn delete_identity(&self, identity: &crate::sbio::IdentityV1, orphan: bool) -> Result { if self.enclave_lists(identity.uuid()) == Some(false) { // Unless an earlier deletion's save failed, which leaves the // enclave without an identity the saved Catacomb still holds. @@ -3776,18 +4036,39 @@ impl SepData { return Ok(false); } - // 0x57 cannot be undone. Check first that its deletion can be saved. + // 0x57 cannot be undone. Ensure the host can save the affected user + // and master Catacombs before sending it. For an unindexed orphan the + // owner Catacomb provides the recovery path even when the bag snapshot + // has no binding; export that owner first if this install lacks a file. + for kind in [PRIVATE_TYPE_CATACOMB_USER, PRIVATE_TYPE_CATACOMB_MASTER] { + if crate::catacomb::read(kind).is_none() || !crate::catacomb::writable(kind) { + dev_err!( + self.dev, + "bio: not deleting: user/master Catacomb is unavailable or unwritable\n" + ); + return Err(EIO); + } + } + if orphan && crate::catacomb::read(PRIVATE_TYPE_CATACOMB_OWNER).is_none() { + let Some(reply) = self.read_component_states() else { return Err(EIO); }; + let state = crate::sbio::ComponentStates::new(&reply) + .state_for(crate::sbio::CatacombUser::OWNER.value()); + if !state.is_some_and(|s| s & crate::sbio::COMPONENT_STATE_ACTIVE != 0) + || !self.save_catacomb(crate::sbio::CatacombUser::OWNER, + PRIVATE_TYPE_CATACOMB_OWNER, + c"orphan-recovery owner Catacomb") { + dev_err!(self.dev, "bio: not deleting: owner Catacomb export failed\n"); + return Err(EIO); + } + } if crate::catacomb::read(PRIVATE_TYPE_CATACOMB_OWNER).is_none() - || !self - .enrol_material - .lock() - .as_ref() - .is_some_and(|material| material.snapshot_identity.is_some()) + || !crate::catacomb::writable(PRIVATE_TYPE_CATACOMB_OWNER) { - dev_err!( - self.dev, - "bio: not deleting: there is no owner Catacomb file or identity-bag binding to save the deletion with; nothing was changed\n" - ); + return Err(EIO); + } + if !orphan && !self.enrol_material.lock().as_ref() + .is_some_and(|material| material.snapshot_identity.is_some()) { + dev_err!(self.dev, "bio: not deleting: indexed identity has no identity-bag binding\n"); return Err(EIO); } @@ -3813,7 +4094,7 @@ impl SepData { /// identity bag, which must match the Catacombs. Safe to repeat after a /// failure partway: a `retry` accepts a user Catacomb an earlier attempt /// already saved. - fn persist_deletion(&self, retry: bool) -> Result<()> { + fn persist_deletion(&self, retry: bool, orphan: bool) -> Result<()> { let Some(user) = crate::sbio::UserId::new(SBIO_PROBE_USER_ID) else { return Err(EIO); }; @@ -3828,6 +4109,11 @@ impl SepData { PRIVATE_TYPE_CATACOMB_MASTER, c"master Catacomb after deletion", ), + ( + crate::sbio::CatacombUser::OWNER, + PRIVATE_TYPE_CATACOMB_OWNER, + c"owner Catacomb after deletion", + ), ] { let Some(reply) = self.read_component_states() else { dev_err!(self.dev, "bio: cannot read component state before saving the {}\n", what); @@ -3843,7 +4129,8 @@ impl SepData { dev_err!(self.dev, "bio: the {} is not active; the deletion cannot be saved\n", what); return Err(EIO); } - if state & crate::sbio::COMPONENT_STATE_SAVE_PENDING != 0 { + if state & crate::sbio::COMPONENT_STATE_SAVE_PENDING != 0 || + (orphan && who.value() == crate::sbio::CatacombUser::OWNER.value()) { if !self.save_catacomb(who, kind, what) { dev_err!(self.dev, "bio: saving the {} failed\n", what); return Err(EIO); @@ -3864,13 +4151,19 @@ impl SepData { dev_err!(self.dev, "bio: lockout persistence failed after the deletion\n"); return Err(EIO); } - if !self.resnapshot_identity_keybag() { + if !orphan && !self.resnapshot_identity_keybag() { dev_err!( self.dev, "bio: Catacombs saved after the deletion but the identity-bag snapshot failed\n" ); return Err(EIO); } + if orphan { + dev_info!( + self.dev, + "bio: orphan deletion saved via Linux user/master/owner Catacombs; no unbound identity bag was snapshotted\n" + ); + } Ok(()) } @@ -4677,6 +4970,47 @@ impl<'a> IdentityRecords<'a> { } } +#[cfg(CONFIG_APPLE_SEP_KUNIT_TEST)] +#[::kernel::macros::kunit_tests(apple_sep_enclave_identity_scope)] +mod identity_scope_tests { + use super::*; + + #[test] + fn list_excludes_other_users_and_selectors() { + let mut reply = [0u8; 3 * IDENTITY_RECORD_LEN]; + for (i, (user, selector, byte)) in [ + (SBIO_PROBE_USER_ID, IR_SELECTOR_BUILTIN, 1u8), + (501, IR_SELECTOR_BUILTIN, 2u8), + (SBIO_PROBE_USER_ID, 2, 3u8), + ].iter().enumerate() { + let at = i * IDENTITY_RECORD_LEN; + reply[at..at + 4].copy_from_slice(&user.to_le_bytes()); + reply[at + 4..at + IDENTITY_V1_LEN].fill(*byte); + reply[at + IR_SELECTOR..at + IR_SELECTOR + 4] + .copy_from_slice(&selector.to_le_bytes()); + } + let records = IdentityRecords::new(&reply).unwrap(); + let mut seen = 0; + records.each_built_in_for(SBIO_PROBE_USER_ID, |identity| { + seen += 1; + assert!(*identity.uuid() == [1u8; IDENTITY_UUID_LEN]); + }); + assert_eq!(seen, 1); + } + + #[test] + fn match_result_carries_the_exact_enclave_identity() { + let mut reply = [0u8; SBIO_MATCH_RESULT_LEN]; + reply[MR_USER_ID..MR_USER_ID + 4] + .copy_from_slice(&SBIO_PROBE_USER_ID.to_le_bytes()); + reply[MR_IDENTITY..MR_IDENTITY + IDENTITY_UUID_LEN].fill(0xa5); + let result = MatchResult::parse(&reply).unwrap(); + let user = UserId::new(SBIO_PROBE_USER_ID).unwrap(); + assert!(result.matches(user)); + assert!(*result.identity_uuid() == [0xa5; IDENTITY_UUID_LEN]); + } +} + const FIC_DEVICE_BUILTIN: u32 = 1; pub(crate) const SBIO_FREE_COUNT_LEN: usize = 4 + 20; static_assert!(SBIO_FREE_COUNT_LEN == SBIO_SAVE_SELECTOR_LEN); diff --git a/drivers/soc/apple/sep.rs b/drivers/soc/apple/sep.rs index df71496a564fe..4e8ff6ba2caba 100644 --- a/drivers/soc/apple/sep.rs +++ b/drivers/soc/apple/sep.rs @@ -781,6 +781,9 @@ struct SepData { // only under `catacomb_writer`. deletion_unsaved: Atomic, + // PREPARE_ENROL selected the empty Linux context during this boot. + empty_context_prepared: Atomic, + #[pin] rx_work: Work, @@ -980,6 +983,7 @@ impl SepData { worker_gone <- new_condvar!("SepData::worker_gone"), catacomb_writer <- new_mutex!(()), deletion_unsaved: Atomic::new(false), + empty_context_prepared: Atomic::new(false), rx_work <- new_work!("SepData::rx_work"), enrol_work <- new_work!("SepData::enrol_work"), verify_work <- new_work!("SepData::verify_work"), diff --git a/drivers/soc/apple/shim.h b/drivers/soc/apple/shim.h index 0425bf2cdaa6e..69b18c9110c66 100644 --- a/drivers/soc/apple/shim.h +++ b/drivers/soc/apple/shim.h @@ -44,6 +44,7 @@ void sep_hwrng_unregister(void *mem); void *sep_store_open(const char *path); void *sep_store_open_host(const char *path, bool *created); void *sep_store_open_trunc(const char *path); +void *sep_store_open_writable_existing(const char *path); void *sep_store_open_ro(const char *path); void *sep_store_open_block(const char *path, int writable); void sep_store_close(void *handle); diff --git a/drivers/soc/apple/shim.rs b/drivers/soc/apple/shim.rs index 8e30c3f54b817..6d546a5603f37 100644 --- a/drivers/soc/apple/shim.rs +++ b/drivers/soc/apple/shim.rs @@ -9,6 +9,7 @@ extern "C" { fn sep_store_open(path: *const c_char) -> *mut c_void; fn sep_store_open_host(path: *const c_char, created: *mut bool) -> *mut c_void; fn sep_store_open_trunc(path: *const c_char) -> *mut c_void; + fn sep_store_open_writable_existing(path: *const c_char) -> *mut c_void; fn sep_store_open_ro(path: *const c_char) -> *mut c_void; fn sep_store_open_block(path: *const c_char, writable: c_int) -> *mut c_void; fn sep_store_close(handle: *mut c_void); @@ -80,6 +81,15 @@ impl StoreFile { Ok(StoreFile { handle }) } + pub(crate) fn open_writable_existing(path: &CStr) -> Result { + // SAFETY: `path` is NUL-terminated; this open cannot create or truncate. + let handle = unsafe { sep_store_open_writable_existing(path.as_char_ptr()) }; + if handle.is_null() { + return Err(EROFS); + } + Ok(StoreFile { handle }) + } + pub(crate) fn open_readonly(path: &CStr) -> Result { // SAFETY: `path` is NUL-terminated; the shim returns NULL on failure. let handle = unsafe { sep_store_open_ro(path.as_char_ptr()) }; diff --git a/drivers/soc/apple/store.rs b/drivers/soc/apple/store.rs index e3abe11c7d5c8..bff68e070db4b 100644 --- a/drivers/soc/apple/store.rs +++ b/drivers/soc/apple/store.rs @@ -359,6 +359,12 @@ impl Store { self.slots.iter().position(|s| !s.used) } + /// Admission check for the copy-on-write slot used by the next write. + /// Even replacing an existing key needs a free slot until commit. + pub(crate) fn can_write(&self, key: &Key, len: usize) -> bool { + valid_kind(key.kind) && len <= MAX_VALUE && self.find_free().is_some() + } + fn block_offset(slot: usize) -> u64 { ((slot + 1) * BLOCK_SIZE) as u64 } diff --git a/drivers/soc/apple/store_shim.c b/drivers/soc/apple/store_shim.c index d50270d717be5..77319a4702164 100644 --- a/drivers/soc/apple/store_shim.c +++ b/drivers/soc/apple/store_shim.c @@ -83,6 +83,14 @@ void *sep_store_open_trunc(const char *path) return IS_ERR(f) ? NULL : f; } +/* Admission check only: never create or truncate an existing record. */ +void *sep_store_open_writable_existing(const char *path) +{ + struct file *f = open_as_kernel(path, O_WRONLY | O_LARGEFILE, 0); + + return IS_ERR(f) ? NULL : f; +} + /* * Opens an existing file read-only, NULL if absent. O_RDONLY and no O_CREAT so * the seed cannot be created, truncated or written by mistake. diff --git a/include/uapi/linux/apple-sep.h b/include/uapi/linux/apple-sep.h index 394b497ff7a59..fbae982a4c0ef 100644 --- a/include/uapi/linux/apple-sep.h +++ b/include/uapi/linux/apple-sep.h @@ -14,7 +14,7 @@ #include #include -#define SEP_BIO_IFACE_VERSION 4 +#define SEP_BIO_IFACE_VERSION 5 #define SEP_BIO_UUID_LEN 16 #define SEP_BIO_LABEL_LEN 128 @@ -64,10 +64,22 @@ struct sep_bio_list { struct sep_bio_enrol_start { __u32 flags; + __u32 account_uid; /* resolved passwd uid; UINT32_MAX is invalid */ + __u32 finger; /* FP_FINGER_LEFT_THUMB..FP_FINGER_RIGHT_LITTLE (1..10) */ __u32 reserved; __u8 label[SEP_BIO_LABEL_LEN]; }; +/* Linux's built-in SEP user context only. This does not enumerate other SEP + * users or OS lanes. DELETE_ORPHAN uses an exact UUID; the kernel index may + * still hold it when fprintd has already lost its own record. + */ +struct sep_bio_enclave_list { + __u32 count; + __u32 reserved; + __u8 uuid[SEP_BIO_MAX_IDENTITIES][SEP_BIO_UUID_LEN]; +}; + #define SEP_BIO_GUIDANCE_NONE 0 #define SEP_BIO_GUIDANCE_PLACE 1 #define SEP_BIO_GUIDANCE_LIFT_AND_MOVE 2 @@ -129,5 +141,11 @@ struct sep_bio_attest { #define SEP_BIO_DELETE _IOW(SEP_BIO_IOC_MAGIC, 0x08, struct sep_bio_delete) #define SEP_BIO_DELETE_ALL _IO(SEP_BIO_IOC_MAGIC, 0x09) #define SEP_BIO_ATTEST _IOWR(SEP_BIO_IOC_MAGIC, 0x0a, struct sep_bio_attest) +#define SEP_BIO_LIST_ENCLAVE _IOR(SEP_BIO_IOC_MAGIC, 0x0b, struct sep_bio_enclave_list) +#define SEP_BIO_DELETE_ORPHAN _IOW(SEP_BIO_IOC_MAGIC, 0x0c, struct sep_bio_delete) +/* Creates the empty Linux owner context only when SEP lists zero Linux + * identities. ENROL_START itself never repairs a missing persistence binding. + */ +#define SEP_BIO_PREPARE_ENROL _IOW(SEP_BIO_IOC_MAGIC, 0x0d, struct sep_bio_enrol_start) #endif /* _UAPI_LINUX_APPLE_SEP_H */ From b42591955c08df88bbe7361f005ba768317828f3 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 23:18:14 +1100 Subject: [PATCH 349/388] tools: aurora-sep: provide exact-ID Linux identity recovery Install a utility that lists the selected Linux SEP user context and deletes one named identity while requiring the operator to name identities that must remain. Document the recovery and admission sequence and test the CLI and package sources. Assisted-by: LLM Signed-off-by: Ryan Murray --- tools/aurora-sep/README.md | 20 +++++ .../packaging/arch/aurora-sep/PKGBUILD | 11 ++- .../arch/aurora-sep/sep-bio-identities | 1 + .../packaging/fedora/aurora-sep.spec | 8 +- tools/aurora-sep/sep-bio-identities | 82 +++++++++++++++++++ .../tests/test_sep_bio_identities.py | 44 ++++++++++ 6 files changed, 161 insertions(+), 5 deletions(-) create mode 120000 tools/aurora-sep/packaging/arch/aurora-sep/sep-bio-identities create mode 100755 tools/aurora-sep/sep-bio-identities create mode 100644 tools/aurora-sep/tests/test_sep_bio_identities.py diff --git a/tools/aurora-sep/README.md b/tools/aurora-sep/README.md index a4f96039ca6d3..142594f8218f3 100644 --- a/tools/aurora-sep/README.md +++ b/tools/aurora-sep/README.md @@ -102,6 +102,26 @@ and starts a match again by itself. When the kernel ends an enrolment because too many captures in a row were unusable (`SEP_BIO_STATUS_REJECTED`), the enrolment fails with that reason in fprintd's log, and fprintd reports `enroll-unknown-error`. Kernels from before these statuses never send them. + +Interface version 5 requires a resolved Unix uid, a known finger, the selected +Linux SEP OS identity, a live identity count below three, a bound identity bag, +and readable, writable owner/user/master Catacomb files before ENROL_START can +schedule enclave capture. On an empty Linux lane, the driver prepares the owner +context after libfprint validates the account; the user/master Catacombs are +saved at the first completed enrolment. The matching aurora libfprint build +rejects a missing passwd entry before either ioctl. A missing persistence file +for an existing identity is an admission error; repair the binding rather than +retrying a capture against unbound state. + +`sep-bio-identities` is the recovery interface for a print with no fprintd +record. As root, run `sep-bio-identities` to list only the selected Linux OS +identity's built-in SEP user context. Use `sep-bio-identities --delete-orphan +UUID --keep GOOD_UUID` when another identity must remain. The kernel index may +still hold a UUID whose fprintd record was lost, so compare the IDs carefully. +The utility does not expose DELETE_ALL. Record the list and +backup of any known-good print before deletion, and confirm the deleted UUID +stays absent after a reboot. This path must be tested on the affected hardware +before treating a deletion as a successful recovery. The device-specific `apple/mesa_calibration.bin` on m2 was recovered using the bundled [`extract-mesa-calibration.py`](extract-mesa-calibration.py), copied from [Gist revision `eb079a8007985d04e75182f20994f1a1c496f12e`](https://gist.github.com/DjDeveloperr/867a1961b861c570442724f48f770158/eb079a8007985d04e75182f20994f1a1c496f12e), diff --git a/tools/aurora-sep/packaging/arch/aurora-sep/PKGBUILD b/tools/aurora-sep/packaging/arch/aurora-sep/PKGBUILD index fc44475693abb..1a57d48551d87 100644 --- a/tools/aurora-sep/packaging/arch/aurora-sep/PKGBUILD +++ b/tools/aurora-sep/packaging/arch/aurora-sep/PKGBUILD @@ -7,7 +7,7 @@ pkgname=aurora-sep pkgver=1 -pkgrel=13 +pkgrel=14 pkgdesc="Loader and desktop integration for the Apple SEP (Touch ID) driver" url="https://github.com/aurora-silicon/linux/tree/aurora-wip/tools/aurora-sep" arch=(any) @@ -31,7 +31,8 @@ source=(load-driver aurora-sep-greeter-fingerprint.service README.md XART-SAFETY.md - OS-IDENTITY.md) + OS-IDENTITY.md + sep-bio-identities) sha256sums=('abea60bb264ced349f7cf671d8d26e1609a81a60ea4c34d66ec6f7f73b847dbb' '447f99cc3c7e683134f31b993aa825fadde24e4854954a3035c96e6631d4d4d0' 'f8d0c4b9385e6716d5ac5dd9941a57459fbc551a8238383bcde36cd929d0523f' @@ -41,9 +42,10 @@ sha256sums=('abea60bb264ced349f7cf671d8d26e1609a81a60ea4c34d66ec6f7f73b847dbb' '0695bd9ce4c207241db13c3ea07819a032d8213417021378f5015ab9317cde12' '5936eb76db666496a8700dade7ac6bcb3785b517958dcea10685690f1822c86f' 'ffab97b20bfd5f0eb1ab641179a131843349cf16890fff0cf2d893f0b5681b37' - 'e5d684962c5323e285486e7504c2dd003ce67ff9c2fda49f9c3ec3b6dfc27563' + 'cf245fbdaa7b3596033d050900fcc94000f620072a25360a898330a8da141974' '4422988e5e7f2417791957d066c501741e929705724083da76cdf1cb70d7a2fc' - '52020e636e62b60f0c3e6251c3b67b8a8c409bff0486bc3463f356fd3073384b') + '52020e636e62b60f0c3e6251c3b67b8a8c409bff0486bc3463f356fd3073384b' + '6209d874a8d2950e5c6a68e2a9c19ffd4ba6e86a27bde3351f37e69290830349') # A unit and the helper it runs ($1, installed as $3), pointed at the # packaged helper. @@ -71,5 +73,6 @@ package() { >"$pkgdir/usr/lib/systemd/system/fprintd.service.d/50-aurora-sep-no-timeout.conf" install -Dm644 logind-aurora-sep.conf \ "$pkgdir/usr/lib/systemd/logind.conf.d/50-aurora-sep.conf" + install -Dm755 sep-bio-identities "$pkgdir/usr/bin/sep-bio-identities" install -Dm644 -t "$pkgdir/usr/share/doc/$pkgname" README.md XART-SAFETY.md OS-IDENTITY.md } diff --git a/tools/aurora-sep/packaging/arch/aurora-sep/sep-bio-identities b/tools/aurora-sep/packaging/arch/aurora-sep/sep-bio-identities new file mode 120000 index 0000000000000..ddd469de602a7 --- /dev/null +++ b/tools/aurora-sep/packaging/arch/aurora-sep/sep-bio-identities @@ -0,0 +1 @@ +../../../sep-bio-identities \ No newline at end of file diff --git a/tools/aurora-sep/packaging/fedora/aurora-sep.spec b/tools/aurora-sep/packaging/fedora/aurora-sep.spec index 01e39051e9ec5..b3f0fbc7f2fe7 100644 --- a/tools/aurora-sep/packaging/fedora/aurora-sep.spec +++ b/tools/aurora-sep/packaging/fedora/aurora-sep.spec @@ -8,7 +8,7 @@ Name: aurora-sep Version: 1 -Release: 8%{?dist} +Release: 9%{?dist} Summary: Loader and desktop integration for the Apple SEP fingerprint driver License: GPL-2.0-only @@ -27,6 +27,7 @@ Source10: aurora-sep-greeter-fingerprint.service Source11: aurora_sep.te Source12: aurora_sep.fc Source13: OS-IDENTITY.md +Source14: sep-bio-identities BuildArch: noarch BuildRequires: systemd-rpm-macros @@ -91,6 +92,7 @@ sed 's|@FPRINTD@|%{_libexecdir}/fprintd|' %{SOURCE7} \ >%{buildroot}%{_unitdir}/fprintd.service.d/50-aurora-sep-no-timeout.conf install -Dpm 0644 %{SOURCE8} \ %{buildroot}%{_prefix}/lib/systemd/logind.conf.d/50-aurora-sep.conf +install -Dpm 0755 %{SOURCE14} %{buildroot}%{_bindir}/sep-bio-identities # Load the driver at boot, so the enclave is up before the login screen asks. install -d %{buildroot}%{_presetdir} echo 'enable aurora-sep.service' >%{buildroot}%{_presetdir}/80-aurora-sep.preset @@ -131,6 +133,7 @@ fi %{_libexecdir}/aurora-sep-load %{_libexecdir}/aurora-sep-ready %{_libexecdir}/aurora-sep-greeter-fingerprint +%{_bindir}/sep-bio-identities %{_unitdir}/aurora-sep.service %{_unitdir}/aurora-sep-ready.service %{_unitdir}/aurora-sep-greeter-fingerprint.service @@ -145,6 +148,9 @@ fi %ghost %verify(not md5 size mode mtime) %{_sharedstatedir}/selinux/%{selinuxtype}/active/modules/200/%{modulename} %changelog +* Tue Oct 27 2026 Aurora Silicon contributors - 1-9 +- Add the Linux-lane orphan identity list and delete tool + * Mon Oct 19 2026 Ryan Murray - 1-7 - XART-SAFETY.md: what the xART guard allows for restore, use, setup and recovery, per platform, and what a rollback does to its list diff --git a/tools/aurora-sep/sep-bio-identities b/tools/aurora-sep/sep-bio-identities new file mode 100755 index 0000000000000..2231434678645 --- /dev/null +++ b/tools/aurora-sep/sep-bio-identities @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +"""List Linux SEP identities and remove one fprintd orphan by its UUID. + +The kernel limits both ioctls to the selected Linux SEP OS identity's built-in +user context. The kernel index may still hold a UUID after fprintd lost its +record, so the operator must identify the exact orphan. This utility has no +delete-all operation. +""" + +import argparse +import fcntl +import os +import struct +import sys +import uuid + +MAGIC = 0xB1 +LIST_SIZE = 8 + 32 * 16 +DELETE_SIZE = 16 + + +def ioctl_number(direction, number, size): + return (direction << 30) | (size << 16) | (MAGIC << 8) | number + + +LIST_ENCLAVE = ioctl_number(2, 0x0B, LIST_SIZE) +DELETE_ORPHAN = ioctl_number(1, 0x0C, DELETE_SIZE) + + +def list_identities(fd): + reply = bytearray(LIST_SIZE) + fcntl.ioctl(fd, LIST_ENCLAVE, reply, True) + count, reserved = struct.unpack_from("=II", reply) + if reserved != 0 or count > 32: + raise ValueError("invalid SEP identity-list reply") + return [uuid.UUID(bytes=bytes(reply[8 + 16 * i:24 + 16 * i])) + for i in range(count)] + + +def check_target(before, target, keep): + if target not in before: + raise ValueError("identity is absent from the Linux-lane list") + if len(before) > 1 and not keep: + raise ValueError("name the identity to preserve with --keep before deleting") + if target in keep or any(identity not in before for identity in keep): + raise ValueError("--keep must list a different identity present in the Linux lane") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--delete-orphan", type=uuid.UUID, metavar="UUID", + help="delete exactly this Linux-lane identity with no fprintd record") + parser.add_argument("--keep", type=uuid.UUID, action="append", default=[], metavar="UUID", + help="identity that must remain; required when more than one is listed") + args = parser.parse_args() + + try: + with os.fdopen(os.open("/dev/sep-bio", os.O_RDWR | os.O_CLOEXEC), "rb", closefd=True) as device: + fd = device.fileno() + before = list_identities(fd) + if args.delete_orphan is None: + for identity in before: + print(identity) + return 0 + check_target(before, args.delete_orphan, args.keep) + fcntl.ioctl(fd, DELETE_ORPHAN, args.delete_orphan.bytes) + after = list_identities(fd) + if args.delete_orphan in after: + raise RuntimeError("SEP still lists the identity after deletion") + if any(identity not in after for identity in args.keep): + raise RuntimeError("a protected identity is absent after deletion") + print("deleted orphan", args.delete_orphan) + print("Linux-lane identities:", len(before), "->", len(after)) + return 0 + except (OSError, ValueError, RuntimeError) as error: + print(f"sep-bio-identities: {error}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/aurora-sep/tests/test_sep_bio_identities.py b/tools/aurora-sep/tests/test_sep_bio_identities.py new file mode 100644 index 0000000000000..23505157866f5 --- /dev/null +++ b/tools/aurora-sep/tests/test_sep_bio_identities.py @@ -0,0 +1,44 @@ +# SPDX-License-Identifier: GPL-2.0-only +"""Synthetic tests for the exact-ID Linux-lane recovery utility.""" + +import runpy +import struct +import unittest +import uuid +from pathlib import Path +from unittest.mock import patch + + +CLI = runpy.run_path(str(Path(__file__).parents[1] / "sep-bio-identities"), run_name="sep_bio_test") + + +class IdentityToolTests(unittest.TestCase): + def test_list_decodes_only_counted_uuids(self): + a = uuid.UUID(int=1) + b = uuid.UUID(int=2) + + def fake_ioctl(_fd, request, reply, _mutate): + self.assertEqual(request, CLI["LIST_ENCLAVE"]) + struct.pack_into("=II", reply, 0, 2, 0) + reply[8:24] = a.bytes + reply[24:40] = b.bytes + + with patch.object(CLI["fcntl"], "ioctl", fake_ioctl): + self.assertEqual(CLI["list_identities"](7), [a, b]) + + def test_exact_target_and_keep_are_required(self): + a = uuid.UUID(int=1) + b = uuid.UUID(int=2) + self.assertIsNone(CLI["check_target"]([a, b], a, [b])) + for before, target, keep in [ + ([a, b], a, []), + ([a, b], a, [a]), + ([a, b], uuid.UUID(int=3), [b]), + ([a, b], a, [uuid.UUID(int=3)]), + ]: + with self.assertRaises(ValueError): + CLI["check_target"](before, target, keep) + + +if __name__ == "__main__": + unittest.main() From 0cfd2275d43f4d543137ffa976bcc9e9b7651a17 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 27 Oct 2026 23:24:13 +1100 Subject: [PATCH 350/388] soc: apple: snapshot a bound bag on exact-ID recovery Use the identity-bag snapshot when recovery still has a verified bag binding. Reserve the owner-Catacomb fallback for an unbound orphan and require the persisted Linux bag file before the irreversible enclave delete. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/soc/apple/sbio.rs | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/drivers/soc/apple/sbio.rs b/drivers/soc/apple/sbio.rs index 2f72bfa2ce7cc..6e2138af3154f 100644 --- a/drivers/soc/apple/sbio.rs +++ b/drivers/soc/apple/sbio.rs @@ -4037,7 +4037,7 @@ impl SepData { } // 0x57 cannot be undone. Ensure the host can save the affected user - // and master Catacombs before sending it. For an unindexed orphan the + // and master Catacombs before sending it. For an explicit recovery the // owner Catacomb provides the recovery path even when the bag snapshot // has no binding; export that owner first if this install lacks a file. for kind in [PRIVATE_TYPE_CATACOMB_USER, PRIVATE_TYPE_CATACOMB_MASTER] { @@ -4071,6 +4071,11 @@ impl SepData { dev_err!(self.dev, "bio: not deleting: indexed identity has no identity-bag binding\n"); return Err(EIO); } + if orphan && !matches!(keybag::read(keybag::Slot::Identity), + Ok(keybag::State::Present(_))) { + dev_err!(self.dev, "bio: not deleting: Linux identity bag file is unavailable\n"); + return Err(EIO); + } let op = crate::sbio::sbio_delete_identity(identity); @@ -4091,9 +4096,10 @@ impl SepData { /// the enclave marks save-pending (a SAVE_CATACOMB of one that is not has /// been refused), the user before the master because saving the user can /// change the master, state re-read before each, then the lockout and the - /// identity bag, which must match the Catacombs. Safe to repeat after a - /// failure partway: a `retry` accepts a user Catacomb an earlier attempt - /// already saved. + /// identity bag when a verified binding exists. An exact-ID orphan + /// recovery without that binding saves the Linux owner Catacomb instead. + /// Safe to repeat after a failure partway: a `retry` accepts a user + /// Catacomb an earlier attempt already saved. fn persist_deletion(&self, retry: bool, orphan: bool) -> Result<()> { let Some(user) = crate::sbio::UserId::new(SBIO_PROBE_USER_ID) else { return Err(EIO); @@ -4151,18 +4157,23 @@ impl SepData { dev_err!(self.dev, "bio: lockout persistence failed after the deletion\n"); return Err(EIO); } - if !orphan && !self.resnapshot_identity_keybag() { - dev_err!( - self.dev, - "bio: Catacombs saved after the deletion but the identity-bag snapshot failed\n" - ); - return Err(EIO); - } - if orphan { + let bag_bound = self.enrol_material.lock().as_ref() + .is_some_and(|material| material.snapshot_identity.is_some()); + if bag_bound { + if !self.resnapshot_identity_keybag() { + dev_err!( + self.dev, + "bio: Catacombs saved after the deletion but the identity-bag snapshot failed\n" + ); + return Err(EIO); + } + } else if orphan { dev_info!( self.dev, - "bio: orphan deletion saved via Linux user/master/owner Catacombs; no unbound identity bag was snapshotted\n" + "bio: orphan deletion saved via Linux user/master/owner Catacombs without a bag snapshot binding\n" ); + } else { + return Err(EIO); } Ok(()) } From 9c755514027857565e57ba7325c5de4cb8786b18 Mon Sep 17 00:00:00 2001 From: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com> Date: Tue, 27 Oct 2026 23:34:11 +1100 Subject: [PATCH 351/388] soc: apple: require a designated Linux bag for recovery ioctls The scoped list, exact-ID delete and empty-owner preparation require the Linux identity bag to be designated and persisted before issuing SBIO commands. Assisted-by: LLM Signed-off-by: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com> --- drivers/soc/apple/sbio.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/drivers/soc/apple/sbio.rs b/drivers/soc/apple/sbio.rs index 6e2138af3154f..6e51b0b081052 100644 --- a/drivers/soc/apple/sbio.rs +++ b/drivers/soc/apple/sbio.rs @@ -3542,6 +3542,12 @@ impl SepData { if !self.touchid_started.load(Relaxed) || self.xarm.lock().os_uuid.is_none() { return Err(ENXIO); } + if !self.keybag_designated.load(Relaxed) + || self.enrol_material.lock().is_none() + || !matches!(keybag::read(keybag::Slot::Identity), + Ok(keybag::State::Present(_))) { + return Err(ENODATA); + } Ok(()) } From 54f584f9cec247d60a1255caceac0279bb8cb3f0 Mon Sep 17 00:00:00 2001 From: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com> Date: Tue, 27 Oct 2026 23:34:56 +1100 Subject: [PATCH 352/388] ci: check SEP ioctl ABI and recovery package sources Run the scoped ioctl size check and existing recovery/package tests on Linux for SEP pull requests. Assisted-by: LLM Signed-off-by: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com> --- .github/workflows/sep-source.yml | 38 ++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/workflows/sep-source.yml diff --git a/.github/workflows/sep-source.yml b/.github/workflows/sep-source.yml new file mode 100644 index 0000000000000..bcb8171021b96 --- /dev/null +++ b/.github/workflows/sep-source.yml @@ -0,0 +1,38 @@ +name: SEP source checks + +on: + pull_request: + paths: + - 'drivers/soc/apple/**' + - 'include/uapi/linux/apple-sep.h' + - 'tools/aurora-sep/**' + - '.github/workflows/sep-source.yml' + +jobs: + source: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + filter: blob:none + sparse-checkout-cone-mode: false + sparse-checkout: | + drivers/soc/apple/** + include/uapi/linux/apple-sep.h + tools/aurora-sep/** + - name: Check biometric ioctl ABI sizes + run: | + cat > /tmp/sep-abi.c <<'EOF' + #include + _Static_assert(sizeof(struct sep_bio_enrol_start) == 144, "enrol ABI"); + _Static_assert(sizeof(struct sep_bio_enclave_list) == 520, "list ABI"); + _Static_assert(sizeof(struct sep_bio_delete) == 16, "delete ABI"); + int main(void) { return 0; } + EOF + cc -Wall -Werror -Iinclude/uapi /tmp/sep-abi.c -o /tmp/sep-abi + /tmp/sep-abi + - name: Test recovery utility and package sources + run: | + python3 -m unittest tools/aurora-sep/tests/test_sep_bio_identities.py -v + python3 -m unittest discover -s tools/aurora-sep/tests -p test_packaging.py -v From 3126b0c339e2cad9940d0d69337cfad5ae9e8a96 Mon Sep 17 00:00:00 2001 From: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com> Date: Tue, 27 Oct 2026 23:36:21 +1100 Subject: [PATCH 353/388] tools: aurora-sep: keep recovery source non-executable The Fedora and Arch packages install the utility with mode 0755; keep the source mode aligned with kernel checkpatch conventions. Assisted-by: LLM Signed-off-by: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com> --- tools/aurora-sep/sep-bio-identities | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 tools/aurora-sep/sep-bio-identities diff --git a/tools/aurora-sep/sep-bio-identities b/tools/aurora-sep/sep-bio-identities old mode 100755 new mode 100644 From eca653976a86572b8c04049e3b2fb2c77bd6b0e0 Mon Sep 17 00:00:00 2001 From: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com> Date: Tue, 27 Oct 2026 23:38:14 +1100 Subject: [PATCH 354/388] selftests: apple-sep: exercise orphan delete save retry Compile and execute the driver deletion orchestration with scripted enclave and persistence outcomes. Check that an indexed fprintd orphan is removable by exact ID and that failed saves retain the host index for retry. Assisted-by: LLM Signed-off-by: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com> --- .github/workflows/sep-source.yml | 1 + tools/aurora-sep/tests/test_orphan_delete.py | 151 +++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 tools/aurora-sep/tests/test_orphan_delete.py diff --git a/.github/workflows/sep-source.yml b/.github/workflows/sep-source.yml index bcb8171021b96..bb5befe0515bf 100644 --- a/.github/workflows/sep-source.yml +++ b/.github/workflows/sep-source.yml @@ -35,4 +35,5 @@ jobs: - name: Test recovery utility and package sources run: | python3 -m unittest tools/aurora-sep/tests/test_sep_bio_identities.py -v + python3 -m unittest tools/aurora-sep/tests/test_orphan_delete.py -v python3 -m unittest discover -s tools/aurora-sep/tests -p test_packaging.py -v diff --git a/tools/aurora-sep/tests/test_orphan_delete.py b/tools/aurora-sep/tests/test_orphan_delete.py new file mode 100644 index 0000000000000..229f6a2c43e38 --- /dev/null +++ b/tools/aurora-sep/tests/test_orphan_delete.py @@ -0,0 +1,151 @@ +# SPDX-License-Identifier: MIT +"""Run the driver's delete-and-persist method with scripted save failures.""" + +from pathlib import Path +import re +import subprocess +import tempfile +import unittest + + +SOURCE = Path(__file__).resolve().parents[3] / "drivers/soc/apple/sbio.rs" + + +def driver_method(name): + source = SOURCE.read_text() + match = re.search(r"^ fn " + name + r"\(", source, re.M) + if match is None: + raise AssertionError(f"missing driver method {name}") + opening = source.index("{", match.start()) + depth, end = 1, opening + 1 + while depth: + depth += (source[end] == "{") - (source[end] == "}") + end += 1 + return source[match.start():end] + + +HARNESS = r''' +#![allow(dead_code, unused_variables)] +use std::ffi::CStr; +use std::ops::Deref; +use std::sync::atomic::{AtomicBool, Ordering::Relaxed}; +use std::sync::{Mutex as StdMutex, MutexGuard}; + +type Error = i32; +type Result = std::result::Result; +const GFP_KERNEL: u32 = 0; +const EIO: Error = -5; + +macro_rules! dev_err { ($($arg:tt)*) => {}; } + +struct Mutex(StdMutex); +impl Mutex { + fn new(value: T) -> Self { Self(StdMutex::new(value)) } + fn lock(&self) -> MutexGuard<'_, T> { self.0.lock().unwrap() } +} + +struct KVec(Vec); +impl KVec { + fn with_capacity(count: usize, _: u32) -> Result { Ok(Self(Vec::with_capacity(count))) } + fn push_within_capacity(&mut self, value: T) -> Result<()> { self.0.push(value); Ok(()) } + fn is_empty(&self) -> bool { self.0.is_empty() } + fn iter(&self) -> std::slice::Iter<'_, T> { self.0.iter() } +} +impl Deref for KVec { + type Target = [T]; + fn deref(&self) -> &[T] { &self.0 } +} + +mod sbio { + #[derive(Clone)] + pub struct IdentityV1 { pub uuid: [u8; 16] } + impl IdentityV1 { pub fn uuid(&self) -> &[u8; 16] { &self.uuid } } +} +mod bio { + pub const UUID_LEN: usize = 16; + pub struct Handled { + pub delete_identity: Option, + pub delete_identities: Vec, + } +} + +struct Index { held: Vec<[u8; 16]> } +impl Index { + fn contains_uuid(&self, uuid: &[u8; 16]) -> bool { self.held.contains(uuid) } +} + +struct SepData { + dev: (), + deletion_unsaved: AtomicBool, + bio_index: Mutex, + delete_replies: Mutex>>, + save_replies: Mutex>>, + events: Mutex>, +} +impl SepData { + fn new(uuid: [u8; 16], saves: Vec>) -> Self { + Self { + dev: (), deletion_unsaved: AtomicBool::new(false), + bio_index: Mutex::new(Index { held: vec![uuid] }), + delete_replies: Mutex::new(vec![Ok(true), Ok(true)]), + save_replies: Mutex::new(saves), events: Mutex::new(Vec::new()), + } + } + fn delete_identity(&self, _: &sbio::IdentityV1, recovery: bool) -> Result { + self.events.lock().push(format!("delete:{recovery}")); + self.delete_replies.lock().remove(0) + } + fn persist_deletion(&self, retry: bool, recovery: bool) -> Result<()> { + self.events.lock().push(format!("save:{retry}:{recovery}")); + self.save_replies.lock().remove(0) + } + fn forget_identity(&self, uuid: &[u8; 16], _: &CStr) { + self.events.lock().push("forget".into()); + self.bio_index.lock().held.retain(|held| held != uuid); + } +METHOD +} + +fn handled(uuid: [u8; 16]) -> bio::Handled { + bio::Handled { delete_identity: Some(sbio::IdentityV1 { uuid }), delete_identities: vec![] } +} + +fn main() { + let uuid = [0xa5; 16]; + let sep = SepData::new(uuid, vec![Ok(())]); + sep.delete_and_persist(&handled(uuid), true).unwrap(); + assert_eq!(*sep.events.lock(), ["delete:true", "save:false:true", "forget"]); + assert!(!sep.bio_index.lock().contains_uuid(&uuid)); + + let sep = SepData::new(uuid, vec![Err(EIO), Ok(())]); + assert!(sep.delete_and_persist(&handled(uuid), true).is_err()); + assert!(sep.deletion_unsaved.load(Relaxed)); + assert!(sep.bio_index.lock().contains_uuid(&uuid)); + sep.delete_and_persist(&handled(uuid), true).unwrap(); + assert_eq!(*sep.events.lock(), ["delete:true", "save:false:true", + "delete:true", "save:true:true", "forget"]); + assert!(!sep.deletion_unsaved.load(Relaxed)); + assert!(!sep.bio_index.lock().contains_uuid(&uuid)); + + let sep = SepData::new(uuid, vec![]); + *sep.delete_replies.lock() = vec![Err(EIO)]; + assert!(sep.delete_and_persist(&handled(uuid), false).is_err()); + assert_eq!(*sep.events.lock(), ["delete:false"]); + assert!(sep.bio_index.lock().contains_uuid(&uuid)); +} +''' + + +class OrphanDeleteTests(unittest.TestCase): + def test_failure_keeps_index_and_retry_saves(self): + source = HARNESS.replace("METHOD", driver_method("delete_and_persist")) + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) + (path / "test.rs").write_text(source) + subprocess.run(["rustc", "--edition=2021", "test.rs", "-o", "test"], + cwd=path, check=True, capture_output=True, text=True) + subprocess.run([str(path / "test")], check=True, capture_output=True, text=True) + + +if __name__ == "__main__": + unittest.main() From 3694c1661ceaa16bf22fc5f9dde1672493d0fcc4 Mon Sep 17 00:00:00 2001 From: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com> Date: Tue, 27 Oct 2026 23:39:33 +1100 Subject: [PATCH 355/388] selftests: apple-sep: execute enrol refusal cases Compile the kernel admission functions in a host harness and exercise missing UID, unknown finger, OS identity, bag, Catacomb and storage-capacity refusals before hardware testing. Assisted-by: LLM Signed-off-by: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com> --- .github/workflows/sep-source.yml | 1 + .../aurora-sep/tests/test_enrol_admission.py | 129 ++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 tools/aurora-sep/tests/test_enrol_admission.py diff --git a/.github/workflows/sep-source.yml b/.github/workflows/sep-source.yml index bb5befe0515bf..cd75b0f266306 100644 --- a/.github/workflows/sep-source.yml +++ b/.github/workflows/sep-source.yml @@ -36,4 +36,5 @@ jobs: run: | python3 -m unittest tools/aurora-sep/tests/test_sep_bio_identities.py -v python3 -m unittest tools/aurora-sep/tests/test_orphan_delete.py -v + python3 -m unittest tools/aurora-sep/tests/test_enrol_admission.py -v python3 -m unittest discover -s tools/aurora-sep/tests -p test_packaging.py -v diff --git a/tools/aurora-sep/tests/test_enrol_admission.py b/tools/aurora-sep/tests/test_enrol_admission.py new file mode 100644 index 0000000000000..5bb688d3b5571 --- /dev/null +++ b/tools/aurora-sep/tests/test_enrol_admission.py @@ -0,0 +1,129 @@ +# SPDX-License-Identifier: MIT +"""Execute the kernel's enrol request and persistence admission functions.""" + +from pathlib import Path +import re +import subprocess +import tempfile +import unittest + + +APPLE = Path(__file__).resolve().parents[3] / "drivers/soc/apple" + + +def function(file, name): + source = (APPLE / file).read_text() + match = re.search(r"^fn " + name + r"\(", source, re.M) + if match is None: + raise AssertionError(f"missing driver function {name}") + opening = source.index("{", match.start()) + depth, end = 1, opening + 1 + while depth: + depth += (source[end] == "{") - (source[end] == "}") + end += 1 + return source[match.start():end] + + +HARNESS = r''' +#![allow(dead_code)] +type Error = i32; +type Result = std::result::Result; +const EINVAL: Error = -22; +const ENXIO: Error = -6; +const EIO: Error = -5; +const ENOSPC: Error = -28; +const ENODATA: Error = -61; +const EROFS: Error = -30; +const PRIVATE_TYPE_CATACOMB_OWNER: u8 = 0xf2; +const PRIVATE_TYPE_CATACOMB_USER: u8 = 0xf3; +const PRIVATE_TYPE_CATACOMB_MASTER: u8 = 0xf4; + +struct EnrolPrerequisites { + lane: bool, + live_count: Option, + bag_bound: bool, + catacomb_readable: [bool; 3], + catacomb_writable: [bool; 3], + keybag_writable: bool, + host_store_capacity: bool, +} +const LABEL_LEN: usize = 128; +struct EnrolStart { + flags: u32, + account_uid: u32, + finger: u32, + reserved: u32, + label: [u8; LABEL_LEN], +} + +FUNCTIONS + +fn ready() -> EnrolPrerequisites { + EnrolPrerequisites { + lane: true, live_count: Some(1), bag_bound: true, + catacomb_readable: [true; 3], catacomb_writable: [true; 3], + keybag_writable: true, host_store_capacity: true, + } +} +fn request() -> EnrolStart { + let mut r = EnrolStart { + flags: 0, account_uid: 1000, finger: 7, reserved: 0, + label: [0; LABEL_LEN], + }; + r.label[..28].copy_from_slice(b"FP1-20261027-7-12345678-user"); + r +} + +fn main() { + assert!(admit_enrol(&ready()).is_ok()); + let mut p = ready(); p.lane = false; assert_eq!(admit_enrol(&p), Err(ENXIO)); + let mut p = ready(); p.live_count = None; assert_eq!(admit_enrol(&p), Err(EIO)); + let mut p = ready(); p.live_count = Some(3); assert_eq!(admit_enrol(&p), Err(ENOSPC)); + let mut p = ready(); p.bag_bound = false; assert_eq!(admit_enrol(&p), Err(ENODATA)); + for i in 0..3 { + let mut p = ready(); p.catacomb_readable[i] = false; + assert_eq!(admit_enrol(&p), Err(ENODATA)); + let mut p = ready(); p.catacomb_writable[i] = false; + assert_eq!(admit_enrol(&p), Err(EROFS)); + } + let mut p = ready(); p.keybag_writable = false; assert_eq!(admit_enrol(&p), Err(EROFS)); + let mut p = ready(); p.host_store_capacity = false; assert_eq!(admit_enrol(&p), Err(ENOSPC)); + + assert!(catacomb_needed_for_enrol(PRIVATE_TYPE_CATACOMB_OWNER, Some(0))); + assert!(!catacomb_needed_for_enrol(PRIVATE_TYPE_CATACOMB_USER, Some(0))); + assert!(!catacomb_needed_for_enrol(PRIVATE_TYPE_CATACOMB_MASTER, Some(0))); + assert!(catacomb_needed_for_enrol(PRIVATE_TYPE_CATACOMB_USER, Some(1))); + + assert!(validate_enrol_request(&request()).is_ok()); + let mut r = request(); r.account_uid = u32::MAX; + assert_eq!(validate_enrol_request(&r), Err(EINVAL)); + let mut r = request(); r.finger = 0; + assert_eq!(validate_enrol_request(&r), Err(EINVAL)); + let mut r = request(); r.finger = 11; + assert_eq!(validate_enrol_request(&r), Err(EINVAL)); + let mut r = request(); r.finger = 6; + assert_eq!(validate_enrol_request(&r), Err(EINVAL)); + let mut r = request(); r.label[24] = 0; + assert_eq!(validate_enrol_request(&r), Err(EINVAL)); +} +''' + + +class EnrolAdmissionTests(unittest.TestCase): + def test_refused_cases_before_capture(self): + methods = "\n".join([ + function("sbio.rs", "admit_enrol"), + function("sbio.rs", "catacomb_needed_for_enrol"), + function("bio.rs", "validate_enrol_request"), + ]) + source = HARNESS.replace("FUNCTIONS", methods) + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) + (path / "test.rs").write_text(source) + subprocess.run(["rustc", "--edition=2021", "test.rs", "-o", "test"], + cwd=path, check=True, capture_output=True, text=True) + subprocess.run([str(path / "test")], check=True, capture_output=True, text=True) + + +if __name__ == "__main__": + unittest.main() From 60a47463ecab303d0cc30e7b637704bc8e5aed8b Mon Sep 17 00:00:00 2001 From: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com> Date: Tue, 27 Oct 2026 23:41:54 +1100 Subject: [PATCH 356/388] soc: apple: preflight clean owner preparation writes Check sensor availability, identity-bag file writability and host index capacity before PREPARE_ENROL selects or saves an empty owner context. Assisted-by: LLM Signed-off-by: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com> --- drivers/soc/apple/sbio.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/drivers/soc/apple/sbio.rs b/drivers/soc/apple/sbio.rs index 6e51b0b081052..726678efef5a2 100644 --- a/drivers/soc/apple/sbio.rs +++ b/drivers/soc/apple/sbio.rs @@ -3618,6 +3618,7 @@ impl SepData { bio::validate_enrol_start_arg(arg)?; self.require_linux_bio_lane()?; if !bio::idle_for_mutation(&self.bio_session.lock()) { return Err(EBUSY); } + if !sensor::is_bound() { return Err(ENODEV); } if !self.xart_writable() { return Err(EROFS); } let ids = self.enclave_identities_for(SBIO_PROBE_USER_ID).ok_or(EIO)?; if !ids.is_empty() { @@ -3635,6 +3636,14 @@ impl SepData { .is_some_and(|material| material.snapshot_identity.is_some()) { return Err(ENODATA); } + if shim::StoreFile::open_writable_existing(keybag::Slot::Identity.path()).is_err() { + return Err(EROFS); + } + let index = self.bio_index.lock(); + if !self.with_host_store(|store| index.can_persist_next(store)).unwrap_or(false) { + return Err(ENOSPC); + } + drop(index); let user = crate::sbio::UserId::new(SBIO_PROBE_USER_ID).ok_or(EINVAL)?; loop { self.wait_until_awake()?; From cf588fef6e074e68286dc3e62c0a15ad2c779cf9 Mon Sep 17 00:00:00 2001 From: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com> Date: Tue, 27 Oct 2026 23:42:43 +1100 Subject: [PATCH 357/388] soc: apple: reserve owner file before fresh SEP context Create and sync the missing Linux owner file before SELECT_CONTEXT writes to the enclave. Refuse an existing invalid file without truncating its failure evidence. Assisted-by: LLM Signed-off-by: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com> --- drivers/soc/apple/catacomb.rs | 12 ++++++++++++ drivers/soc/apple/sbio.rs | 3 +++ 2 files changed, 15 insertions(+) diff --git a/drivers/soc/apple/catacomb.rs b/drivers/soc/apple/catacomb.rs index 9c60e19b5861d..d71b415fcb321 100644 --- a/drivers/soc/apple/catacomb.rs +++ b/drivers/soc/apple/catacomb.rs @@ -34,6 +34,18 @@ pub(crate) fn writable(kind: u8) -> bool { .is_some() } +/// Reserve a missing Linux owner file before SELECT_CONTEXT changes SEP state. +/// A corrupt existing file is never truncated here: it is failure evidence. +pub(crate) fn prepare_missing_owner() -> Result<()> { + let path = path_for(crate::PRIVATE_TYPE_CATACOMB_OWNER).ok_or(EINVAL)?; + let (file, created) = shim::StoreFile::open_host(path)?; + if !created { + return Err(ENODATA); + } + file.sync()?; + file.sync_parent() +} + pub(crate) fn write(kind: u8, blob: &[u8]) -> Result<()> { let path = path_for(kind).ok_or(EINVAL)?; if blob.len() > MAX_CATACOMB { diff --git a/drivers/soc/apple/sbio.rs b/drivers/soc/apple/sbio.rs index 726678efef5a2..a71695a23195d 100644 --- a/drivers/soc/apple/sbio.rs +++ b/drivers/soc/apple/sbio.rs @@ -3645,6 +3645,9 @@ impl SepData { } drop(index); let user = crate::sbio::UserId::new(SBIO_PROBE_USER_ID).ok_or(EINVAL)?; + // The host must be able to create and sync the owner file before a + // system-context write. A pre-existing invalid file is left intact. + crate::catacomb::prepare_missing_owner()?; loop { self.wait_until_awake()?; let _writer = self.catacomb_writer.lock(); From f7e5b4a2d09597d6a523f8260138436f3fc488f9 Mon Sep 17 00:00:00 2001 From: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com> Date: Tue, 27 Oct 2026 23:43:16 +1100 Subject: [PATCH 358/388] selftests: apple-sep: check owner-file preparation outcomes Execute the driver preflight function with a scripted file backend and verify that a corrupt existing owner file is not truncated before SEP context writes. Assisted-by: LLM Signed-off-by: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com> --- .github/workflows/sep-source.yml | 1 + .../tests/test_owner_preparation.py | 90 +++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 tools/aurora-sep/tests/test_owner_preparation.py diff --git a/.github/workflows/sep-source.yml b/.github/workflows/sep-source.yml index cd75b0f266306..aecbf352d3ec3 100644 --- a/.github/workflows/sep-source.yml +++ b/.github/workflows/sep-source.yml @@ -37,4 +37,5 @@ jobs: python3 -m unittest tools/aurora-sep/tests/test_sep_bio_identities.py -v python3 -m unittest tools/aurora-sep/tests/test_orphan_delete.py -v python3 -m unittest tools/aurora-sep/tests/test_enrol_admission.py -v + python3 -m unittest tools/aurora-sep/tests/test_owner_preparation.py -v python3 -m unittest discover -s tools/aurora-sep/tests -p test_packaging.py -v diff --git a/tools/aurora-sep/tests/test_owner_preparation.py b/tools/aurora-sep/tests/test_owner_preparation.py new file mode 100644 index 0000000000000..29c1217d7eb24 --- /dev/null +++ b/tools/aurora-sep/tests/test_owner_preparation.py @@ -0,0 +1,90 @@ +# SPDX-License-Identifier: MIT +"""Execute the kernel's owner-file preflight against scripted file outcomes.""" + +from pathlib import Path +import re +import subprocess +import tempfile +import unittest + + +SOURCE = Path(__file__).resolve().parents[3] / "drivers/soc/apple/catacomb.rs" + + +def method(): + source = SOURCE.read_text() + match = re.search(r"^pub\(crate\) fn prepare_missing_owner\(", source, re.M) + if match is None: + raise AssertionError("missing owner preparation function") + opening = source.index("{", match.start()) + depth, end = 1, opening + 1 + while depth: + depth += (source[end] == "{") - (source[end] == "}") + end += 1 + return source[match.start():end] + + +HARNESS = r''' +use std::ffi::CStr; +use std::sync::atomic::{AtomicBool, AtomicU32, Ordering::Relaxed}; +type Error = i32; +type Result = std::result::Result; +const EINVAL: Error = -22; +const ENODATA: Error = -61; +const EROFS: Error = -30; +const PRIVATE_TYPE_CATACOMB_OWNER: u8 = 0xf2; + +fn path_for(kind: u8) -> Option<&'static CStr> { + (kind == PRIVATE_TYPE_CATACOMB_OWNER).then_some(c"/var/lib/apple-sep-catacomb-owner.bin") +} + +mod shim { + use super::*; + pub static CREATE: AtomicBool = AtomicBool::new(true); + pub static FAIL_OPEN: AtomicBool = AtomicBool::new(false); + pub static SYNC: AtomicU32 = AtomicU32::new(0); + pub static PARENT: AtomicU32 = AtomicU32::new(0); + pub struct StoreFile; + impl StoreFile { + pub fn open_host(_: &CStr) -> Result<(StoreFile, bool)> { + if FAIL_OPEN.load(Relaxed) { Err(EROFS) } + else { Ok((StoreFile, CREATE.load(Relaxed))) } + } + pub fn sync(&self) -> Result<()> { SYNC.fetch_add(1, Relaxed); Ok(()) } + pub fn sync_parent(&self) -> Result<()> { PARENT.fetch_add(1, Relaxed); Ok(()) } + } +} + +METHOD + +fn main() { + assert_eq!(prepare_missing_owner(), Ok(())); + assert_eq!(shim::SYNC.load(Relaxed), 1); + assert_eq!(shim::PARENT.load(Relaxed), 1); + + shim::CREATE.store(false, Relaxed); + assert_eq!(prepare_missing_owner(), Err(ENODATA)); + assert_eq!(shim::SYNC.load(Relaxed), 1); + assert_eq!(shim::PARENT.load(Relaxed), 1); + + shim::FAIL_OPEN.store(true, Relaxed); + assert_eq!(prepare_missing_owner(), Err(EROFS)); + assert_eq!(shim::SYNC.load(Relaxed), 1); + assert_eq!(shim::PARENT.load(Relaxed), 1); +} +''' + + +class OwnerPreparationTests(unittest.TestCase): + def test_invalid_file_is_preserved_before_sep_write(self): + source = HARNESS.replace("METHOD", method()) + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) + (path / "test.rs").write_text(source) + subprocess.run(["rustc", "--edition=2021", "test.rs", "-o", "test"], + cwd=path, check=True, capture_output=True, text=True) + subprocess.run([str(path / "test")], check=True, capture_output=True, text=True) + + +if __name__ == "__main__": + unittest.main() From 78d4fc29388e2b4f974818702c8ac786ff4fbf6e Mon Sep 17 00:00:00 2001 From: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com> Date: Tue, 27 Oct 2026 23:56:13 +1100 Subject: [PATCH 359/388] ci: cross-compile Apple SEP Rust module Add a targeted arm64 module build so the biometric driver changes receive a kernel type-check in addition to the source and synthetic tests. Assisted-by: LLM Signed-off-by: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com> --- .github/workflows/sep-source.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/.github/workflows/sep-source.yml b/.github/workflows/sep-source.yml index aecbf352d3ec3..70b8320a4b962 100644 --- a/.github/workflows/sep-source.yml +++ b/.github/workflows/sep-source.yml @@ -39,3 +39,29 @@ jobs: python3 -m unittest tools/aurora-sep/tests/test_enrol_admission.py -v python3 -m unittest tools/aurora-sep/tests/test_owner_preparation.py -v python3 -m unittest discover -s tools/aurora-sep/tests -p test_packaging.py -v + + rust-build: + runs-on: ubuntu-24.04 + timeout-minutes: 45 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + - name: Install kernel build dependencies + run: | + sudo apt-get update + sudo apt-get install -y clang lld llvm libclang-dev libelf-dev libssl-dev \ + flex bison bc pkg-config make perl + rustup component add rust-src + cargo install bindgen-cli --version 0.71.1 --locked + - name: Compile Apple SEP module for arm64 + env: + LIBCLANG_PATH: /usr/lib/llvm-18/lib + run: | + make LLVM=1 ARCH=arm64 rustavailable + make LLVM=1 ARCH=arm64 defconfig + scripts/kconfig/merge_config.sh -m .config arch/arm64/configs/asahi.config + make LLVM=1 ARCH=arm64 olddefconfig + grep -qx 'CONFIG_APPLE_SEP=m' .config + make -j2 LLVM=1 ARCH=arm64 modules_prepare + make -j2 LLVM=1 ARCH=arm64 drivers/soc/apple/apple-sep.o From b8b8e2693c47cbb94ba181a7a700df81b7d0e6fa Mon Sep 17 00:00:00 2001 From: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com> Date: Tue, 27 Oct 2026 23:59:44 +1100 Subject: [PATCH 360/388] ci: type-check Apple SEP KUnit cases in arm64 build Enable KUnit and the SEP test configuration for the targeted Rust module build so the new admission and identity tests are compiled. Assisted-by: LLM Signed-off-by: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com> --- .github/workflows/sep-source.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/sep-source.yml b/.github/workflows/sep-source.yml index 70b8320a4b962..052f282d60061 100644 --- a/.github/workflows/sep-source.yml +++ b/.github/workflows/sep-source.yml @@ -61,7 +61,9 @@ jobs: make LLVM=1 ARCH=arm64 rustavailable make LLVM=1 ARCH=arm64 defconfig scripts/kconfig/merge_config.sh -m .config arch/arm64/configs/asahi.config + scripts/config -e KUNIT -e APPLE_SEP_KUNIT_TEST make LLVM=1 ARCH=arm64 olddefconfig grep -qx 'CONFIG_APPLE_SEP=m' .config + grep -qx 'CONFIG_APPLE_SEP_KUNIT_TEST=y' .config make -j2 LLVM=1 ARCH=arm64 modules_prepare make -j2 LLVM=1 ARCH=arm64 drivers/soc/apple/apple-sep.o From 763b4dd1e7c1d1a5e1e330b974d20c481da51d0f Mon Sep 17 00:00:00 2001 From: Jonathan Goyvaerts Date: Mon, 26 Oct 2026 22:02:48 -0300 Subject: [PATCH 361/388] drm/asahi: g17: never report a busy fragment queue on 25G83 The 25G83 firmware reads the tiling descriptor's fragment_busy word (TA+0x7a4) only in its out-of-memory lane, at the first out-of-memory stop of a pass. When it is non-zero the firmware compares the pass-seq byte of the last completed fragment kick on that queue with the lane's copy of TA+0x7b0. On a mismatch it resumes the stopped TA with the resume-after-grow register write but without adding memory, and retires the lane: the tiler then stops silently with no further out-of-memory stop and no completion, and the forward-progress watchdog fires (reason 0). We set fragment_busy whenever the previous fragment kick had not completed at publish, which is common in a pipelined stream. macOS always writes 0 there on this firmware. Add Variant::fragment_busy and clear it for V25G83, so the out-of-memory lane takes the path macOS takes. Renders that fit their tiler heap never reach that lane, so they are unaffected. V26A428 keeps its current behaviour. The firmware behaviour above is from a static disassembly of the 25G83 firmware and the macOS AGXG16X kext. In our testing on a J616s (T6040, 25G83), with the tiler heap at its tile-count size so ordinary heavy passes must grow, this alone took dEQP-VK.memory.pipeline_barrier.graphics.all_device.1048576_vertex_buffer_stride_2 from 7 firmware recoveries over three runs to 4; the next change removes the rest. Signed-off-by: Jonathan Goyvaerts Assisted-by: LLM --- drivers/gpu/drm/asahi/g17/fw/variant.rs | 7 +++++++ drivers/gpu/drm/asahi/g17/queue/render.rs | 8 ++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/fw/variant.rs b/drivers/gpu/drm/asahi/g17/fw/variant.rs index d2d6d2404dfc8..60b46f628eb8a 100644 --- a/drivers/gpu/drm/asahi/g17/fw/variant.rs +++ b/drivers/gpu/drm/asahi/g17/fw/variant.rs @@ -57,6 +57,9 @@ pub(crate) struct Variant { pub(crate) few_primitives: bool, /// The firmware runs ray-traversal work (ray queries and intersection programs). pub(crate) ray_traversal: bool, + /// Tiling descriptors report whether the fragment queue still had earlier kicks + /// outstanding (`fragment_busy`). + pub(crate) fragment_busy: bool, /// Render item rings carry lead commands: InitBufferManager before the first tiling pass /// of a buffer manager, and FragmentBarrier before every fragment descriptor. pub(crate) lead_commands: bool, @@ -101,6 +104,7 @@ pub(crate) const V26A428: Variant = Variant { mcache: true, few_primitives: true, ray_traversal: true, + fragment_busy: true, lead_commands: false, programs: super::render::RegisterPrograms::G17P, }; @@ -126,6 +130,9 @@ pub(crate) const V25G83: Variant = Variant { few_primitives: false, // A stalled ray traversal wedges the GPU without a firmware recovery. ray_traversal: false, + // When a tiling pass runs out of memory with this set, the firmware resumes it without + // adding memory and the tiler stops silently; macOS always writes 0. + fragment_busy: false, lead_commands: true, programs: super::render::RegisterPrograms::V25G83, }; diff --git a/drivers/gpu/drm/asahi/g17/queue/render.rs b/drivers/gpu/drm/asahi/g17/queue/render.rs index 3261f2f0640b3..41a2a4bfbbbd9 100644 --- a/drivers/gpu/drm/asahi/g17/queue/render.rs +++ b/drivers/gpu/drm/asahi/g17/queue/render.rs @@ -587,8 +587,12 @@ impl Pair { let stamps = host.stamps(qids)?; fence(Ordering::Acquire); let covered = stamps.map(|stamp| retirement::stamp_covers(stamp, ordinal)); - self.memory - .completion_links(ordinal, kicks, payload, !covered[1])?; + self.memory.completion_links( + ordinal, + kicks, + payload, + self.variant.fragment_busy && !covered[1], + )?; let leads = match self.variant.lead_commands { true => { let init_manager = !self.memory.manager.initialized(); From ad14f2da457be86a30e212852f23d8e18166f6d7 Mon Sep 17 00:00:00 2001 From: Jonathan Goyvaerts Date: Mon, 26 Oct 2026 22:03:56 -0300 Subject: [PATCH 362/388] drm/asahi: g17: drop the 25G83 fragment barrier Commit 0d9efa515 ("drm/asahi: g17: render lead commands for the 25G83 firmware") led every 25G83 fragment descriptor with a FragmentBarrier (tag 4) that waits for the paired tiling stamp. macOS does not send that item: its fragment submission stores item tags 1, 0xc, 0xe, 0xf and 0x10 only, and keeps fragment-after-tiling ordering in the kick entry's dependency 0, which we already set. The barrier deadlocks a pass that runs out of tiler memory. The firmware's item-ring loop stops at an item whose handler returns 0, and the tag-4 handler returns 0 until the tiling stamp is reached, so the fragment kick's AddKicks is only processed after its tiling pass completes. A partial render needs that fragment kick announced while the tiling pass is stopped (the firmware releases it in out-of-memory state 7 by clearing the kick entry's dependency 0), so the tiling pass waits for the partial render and the partial render waits for the tiling pass. Remove FragmentBarrier and write only InitBufferManager as a lead command. V26A428 has no lead commands and is unaffected. The firmware behaviour above is from a static disassembly of the 25G83 firmware and the macOS AGXG16X kext. On a J616s (T6040, 25G83), with the tiler heap left at its tile-count size so ordinary heavy passes must grow, this and the previous change take dEQP-VK.memory.pipeline_barrier.graphics.all_device.1048576_vertex_buffer_stride_2 from 7 firmware recoveries over three runs to none. A 1/2000 vk-default slice is 769 pass / 0 fail / 0 crash, and the desktop and GL clients render as before. Fixes: 0d9efa515 ("drm/asahi: g17: render lead commands for the 25G83 firmware") Signed-off-by: Jonathan Goyvaerts Assisted-by: LLM --- drivers/gpu/drm/asahi/g17/fw/queue.rs | 59 ------------------- drivers/gpu/drm/asahi/g17/fw/variant.rs | 2 +- drivers/gpu/drm/asahi/g17/queue/render.rs | 2 +- .../gpu/drm/asahi/g17/queue/render/memory.rs | 36 +++-------- 4 files changed, 10 insertions(+), 89 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/fw/queue.rs b/drivers/gpu/drm/asahi/g17/fw/queue.rs index 5708a2251a062..66e7d07db48a5 100644 --- a/drivers/gpu/drm/asahi/g17/fw/queue.rs +++ b/drivers/gpu/drm/asahi/g17/fw/queue.rs @@ -96,8 +96,6 @@ pub(crate) enum CommandTag { QueueConfig = 0x0f, /// [`KickPredecessor`]. KickPredecessor = 0x10, - /// [`FragmentBarrier`] (25G83 firmware). - FragmentBarrier = 4, /// [`InitBufferManager`] (25G83 firmware). InitBufferManager = 5, } @@ -569,63 +567,6 @@ impl QueueConfig { } } -/// The 25G83 firmware's barrier before every fragment descriptor (tag 4): the fragment work -/// waits for the paired tiling stamp, in addition to the kick-entry dependency. -#[repr(C, packed)] -#[derive(Copy, Clone, Debug)] -pub(crate) struct FragmentBarrier { - /// [`CommandTag::FragmentBarrier`]. - pub(crate) tag: u32, - /// GPU address of the tiling queue's completion stamp word. - pub(crate) stamp_va: u64, - /// Timestamp of the tiling kick. - pub(crate) tiling_kick: u64, - /// QID of the tiling queue. - pub(crate) tiling_qid: u32, - /// Tiling stamp value waited for. - pub(crate) wait: u32, - pub(crate) unk_1c: [u8; 8], - /// Event of the wait: the tiling QID. - pub(crate) event: u32, - /// Fragment stamp value. - pub(crate) stamp_self: u32, - /// Current object ID of the fragment stage. - pub(crate) fragment_id: u64, - pub(crate) unk_34: u32, - pub(crate) unk_38: [u8; 0x10], -} - -static_assert!(size_of::() == 0x48); -static_assert!(core::mem::offset_of!(FragmentBarrier, wait) == 0x18); -static_assert!(core::mem::offset_of!(FragmentBarrier, event) == 0x24); -static_assert!(core::mem::offset_of!(FragmentBarrier, fragment_id) == 0x2c); - -impl FragmentBarrier { - /// The barrier of a pass whose tiling stage, kicked at `tiling_kick` on `tiling_qid`, - /// signals `stamp_value` at `stamp_va`. - pub(crate) fn new( - stamp_va: u64, - tiling_kick: KickTimestamp, - tiling_qid: u32, - stamp_value: u32, - fragment_id: u64, - ) -> Self { - Self { - tag: CommandTag::FragmentBarrier as u32, - stamp_va, - tiling_kick: tiling_kick.get(), - tiling_qid, - wait: stamp_value, - unk_1c: [0; 8], - event: tiling_qid, - stamp_self: stamp_value, - fragment_id, - unk_34: 1, - unk_38: [0; 0x10], - } - } -} - /// The 25G83 firmware's buffer-manager initialization (tag 5), before the first tiling /// descriptor that uses a parameter buffer. The block count is read from the manager. #[repr(C, packed)] diff --git a/drivers/gpu/drm/asahi/g17/fw/variant.rs b/drivers/gpu/drm/asahi/g17/fw/variant.rs index 60b46f628eb8a..6b1a6af2d2cfc 100644 --- a/drivers/gpu/drm/asahi/g17/fw/variant.rs +++ b/drivers/gpu/drm/asahi/g17/fw/variant.rs @@ -61,7 +61,7 @@ pub(crate) struct Variant { /// outstanding (`fragment_busy`). pub(crate) fragment_busy: bool, /// Render item rings carry lead commands: InitBufferManager before the first tiling pass - /// of a buffer manager, and FragmentBarrier before every fragment descriptor. + /// of a buffer manager. pub(crate) lead_commands: bool, /// Register programs of the render descriptors. pub(crate) programs: super::render::RegisterPrograms, diff --git a/drivers/gpu/drm/asahi/g17/queue/render.rs b/drivers/gpu/drm/asahi/g17/queue/render.rs index 41a2a4bfbbbd9..1d0a584f171ae 100644 --- a/drivers/gpu/drm/asahi/g17/queue/render.rs +++ b/drivers/gpu/drm/asahi/g17/queue/render.rs @@ -596,7 +596,7 @@ impl Pair { let leads = match self.variant.lead_commands { true => { let init_manager = !self.memory.manager.initialized(); - self.memory.write_leads(ordinal, kicks[0], init_manager)? + self.memory.write_leads(ordinal, init_manager)? } false => [false; 2], }; diff --git a/drivers/gpu/drm/asahi/g17/queue/render/memory.rs b/drivers/gpu/drm/asahi/g17/queue/render/memory.rs index 884537b99b6d8..90a11ca977684 100644 --- a/drivers/gpu/drm/asahi/g17/queue/render/memory.rs +++ b/drivers/gpu/drm/asahi/g17/queue/render/memory.rs @@ -31,8 +31,8 @@ pub(super) const ANNOUNCE: [usize; 2] = [0x17000, 0x1b000]; pub(super) const JOB_LIST: usize = 0xa000; pub(super) const CONFIG: [usize; 2] = [0, 0x4000]; pub(super) const CONFIG_STRIDE: usize = 0x180; -/// The lead command of a stage (InitBM before tiling, the barrier before fragment), after the -/// stage's configuration record in its slot ([`crate::g17::fw::Variant::lead_commands`]). +/// The lead command of a stage (InitBM before tiling), after the stage's configuration record +/// in its slot ([`crate::g17::fw::Variant::lead_commands`]). pub(super) const LEAD: usize = 0xc0; const DEPTH: usize = queues::RENDER_DEPTH as usize; /// Offsets of the priority pair and the policy word in a queue record. @@ -534,7 +534,6 @@ const _: () = { assert!(abi::FRAGMENT_SLOT_SIZE * RECORDS <= FRAGMENT_BYTES); assert!(RECORDS * CONFIG_STRIDE <= 0x4000); assert!(size_of::() <= LEAD); - assert!(LEAD + size_of::() <= CONFIG_STRIDE); assert!(LEAD + size_of::() <= CONFIG_STRIDE); }; @@ -648,14 +647,8 @@ impl Memory { } /// Writes the lead commands of `ordinal` from its linked descriptors: InitBM before the - /// tiling descriptor when `init_manager`, and the fragment barrier. Returns which stages - /// have one. - pub(super) fn write_leads( - &mut self, - ordinal: u64, - tiling_kick: fw::kick::KickTimestamp, - init_manager: bool, - ) -> Result<[bool; 2]> { + /// tiling descriptor when `init_manager`. Returns which stages have one. + pub(super) fn write_leads(&mut self, ordinal: u64, init_manager: bool) -> Result<[bool; 2]> { if !self.owns(ordinal) { return Err(EIO); } @@ -664,23 +657,10 @@ impl Memory { .descriptors .pointer(Graph::offset(0, ordinal), size_of::())? .cast::(); - let fragment = self - .graph - .descriptors - .pointer(Graph::offset(1, ordinal), size_of::())? - .cast::(); - // SAFETY: Both views were checked during preparation and the ordinal still owns them; - // only integer fields are read. - let (ta, fragment_id) = unsafe { (&*ta, (*fragment).descriptor.current_id) }; + // SAFETY: The view was checked during preparation and the ordinal still owns it; only + // integer fields are read. + let ta = unsafe { &*ta }; let slot = ordinal as usize % RECORDS * CONFIG_STRIDE + LEAD; - let barrier = queue::FragmentBarrier::new( - ta.stamp_va, - tiling_kick, - ta.qid, - ta.stamp_value, - fragment_id, - ); - self.graph.config.write(CONFIG[1] + slot, barrier)?; if init_manager { let init = queue::InitBufferManager::new( ta.context_id, @@ -690,7 +670,7 @@ impl Memory { ); self.graph.config.write(CONFIG[0] + slot, init)?; } - Ok([init_manager, true]) + Ok([init_manager, false]) } } From d769aaf5b389a4d6ebd4687cc50413b83634cee8 Mon Sep 17 00:00:00 2001 From: Ryan <81541646+RyanTheTide@users.noreply.github.com> Date: Wed, 28 Oct 2026 01:28:13 +1100 Subject: [PATCH 363/388] usb: typec: tipd: reconcile firmware-entered J616s DP mode Publish the entered partner DisplayPort alternate mode, consume SPMI events latched before IRQ registration, expose pin E, and provide the connector fwnode fallback. The tether route gate follow-up belongs with #451 and is included in the batch3 candidate. Assisted-by: LLM --- drivers/usb/typec/tipd/core.c | 46 ++++++++++++++++++++++++++++--- drivers/usb/typec/tipd/spmi.c | 9 ++++-- drivers/usb/typec/tipd/tps6598x.h | 1 + 3 files changed, 50 insertions(+), 6 deletions(-) diff --git a/drivers/usb/typec/tipd/core.c b/drivers/usb/typec/tipd/core.c index aa630b95dafe9..dd6f7b99e698a 100644 --- a/drivers/usb/typec/tipd/core.c +++ b/drivers/usb/typec/tipd/core.c @@ -290,6 +290,11 @@ static int tps6598x_connect(struct tps6598x *tps, u32 status) static void tps6598x_drop_partner(struct tps6598x *tps, u32 status) { + if (tps->partner_altmode_dp) { + typec_altmode_update_active(tps->partner_altmode_dp, false); + typec_unregister_altmode(tps->partner_altmode_dp); + tps->partner_altmode_dp = NULL; + } if (!IS_ERR(tps->partner)) typec_unregister_partner(tps->partner); tps->partner = NULL; @@ -1171,6 +1176,11 @@ static void cd321x_update_work(struct work_struct *work) /* Process partner disconnection or change */ if (!new_connected || partner_changed) { + if (tps->partner_altmode_dp) { + typec_altmode_update_active(tps->partner_altmode_dp, false); + typec_unregister_altmode(tps->partner_altmode_dp); + tps->partner_altmode_dp = NULL; + } if (!IS_ERR(tps->partner)) typec_unregister_partner(tps->partner); tps->partner = NULL; @@ -1245,6 +1255,29 @@ static void cd321x_update_work(struct work_struct *work) typec_partner_set_identity(tps->partner); } + /* The CD321x firmware owns VDM discovery and Enter Mode. Publish the + * discovered DP mode only after it reports an entered DP connection; + * keep Linux's DP Alt Mode driver from issuing a second Enter Mode. + */ + if (dp_connected && !tps->partner_altmode_dp) { + struct typec_altmode_desc desc = { + .svid = USB_TYPEC_DP_SID, + .mode = USB_TYPEC_DP_MODE, + .vdo = le32_to_cpu(st.dp_sid_status.mode_data), + .mode_selection = true, + }; + + tps->partner_altmode_dp = typec_partner_register_altmode(tps->partner, + &desc); + if (IS_ERR(tps->partner_altmode_dp)) { + ret = PTR_ERR(tps->partner_altmode_dp); + tps->partner_altmode_dp = NULL; + goto retry; + } + } + if (tps->partner_altmode_dp) + typec_altmode_update_active(tps->partner_altmode_dp, dp_connected); + /* Update the TypeC MUX/PHY state */ if (dp_route_was_active && dp_connected && !was_disconnected && !dp_mode_changed && st.dp_sid_event) { @@ -1997,7 +2030,8 @@ static int cd321x_register_port_altmodes(struct cd321x *cd321x) memset(&desc, 0, sizeof(desc)); desc.svid = USB_TYPEC_DP_SID; desc.mode = USB_TYPEC_DP_MODE; - desc.vdo = DP_CONF_SET_PIN_ASSIGN(BIT(DP_PIN_ASSIGN_C) | BIT(DP_PIN_ASSIGN_D)); + desc.vdo = DP_CONF_SET_PIN_ASSIGN(BIT(DP_PIN_ASSIGN_C) | + BIT(DP_PIN_ASSIGN_D) | BIT(DP_PIN_ASSIGN_E)); desc.vdo |= DP_CAP_DFP_D; amode = typec_port_register_altmode(cd321x->tps.port, &desc); if (IS_ERR(amode)) @@ -2111,6 +2145,7 @@ EXPORT_SYMBOL_IF_KUNIT(cd321x_init_work); static int cd321x_register_port(struct tps6598x *tps, struct fwnode_handle *fwnode) { + struct fwnode_handle *connector_fwnode; struct cd321x *cd321x = container_of(tps, struct cd321x, tps); int ret; @@ -2130,9 +2165,12 @@ cd321x_register_port(struct tps6598x *tps, struct fwnode_handle *fwnode) goto err_unregister_altmodes; } - cd321x->connector_fwnode = fwnode_find_reference(fwnode, "displayport", 0); - if (IS_ERR(cd321x->connector_fwnode)) - cd321x->connector_fwnode = NULL; + if (fwnode_property_present(fwnode, "displayport")) + connector_fwnode = fwnode_find_reference(fwnode, "displayport", 0); + else + connector_fwnode = fwnode_handle_get(fwnode); + if (!IS_ERR_OR_NULL(connector_fwnode)) + cd321x->connector_fwnode = connector_fwnode; cd321x->tbt_switch = cd321x_tbt_switch_get(tps->dev, fwnode); if (IS_ERR(cd321x->tbt_switch)) { diff --git a/drivers/usb/typec/tipd/spmi.c b/drivers/usb/typec/tipd/spmi.c index ba89b4b57a255..4f0ceca25ee7f 100644 --- a/drivers/usb/typec/tipd/spmi.c +++ b/drivers/usb/typec/tipd/spmi.c @@ -318,9 +318,14 @@ static int sn201202x_probe(struct spmi_device *device) return ret; ret = tipd_init(tps); - if (ret) + if (ret) { spmi_command_sleep(device); - return ret; + return ret; + } + + /* Events can latch before the IRQ is registered. Consume that state now. */ + tps->data->irq_handler(tps->irq, tps); + return 0; } static void sn201202x_remove(struct spmi_device *device) diff --git a/drivers/usb/typec/tipd/tps6598x.h b/drivers/usb/typec/tipd/tps6598x.h index caac3cb771bd4..f6d51724a1f5d 100644 --- a/drivers/usb/typec/tipd/tps6598x.h +++ b/drivers/usb/typec/tipd/tps6598x.h @@ -371,6 +371,7 @@ struct tps6598x { const struct tipd_data *data; struct tipd_debugfs *debugfs; + struct typec_altmode *partner_altmode_dp; }; /* TPS_REG_USB4_STATUS */ From 73e0d920b9386b8495e5a76a5dc1bd042c520244 Mon Sep 17 00:00:00 2001 From: fbalazsy Date: Tue, 27 Oct 2026 15:29:38 +0100 Subject: [PATCH 364/388] drm/apple: connect a Type-C display again when a retrain after a hotplug finds no mode A Type-C display that wakes from system sleep may list only its base modes at first: its EDID is not ready yet. The retrain after that hotplug then finds no mode for the active CRTC (the sink's 7680x2160 is not listed), and nothing reads the EDID again until the next HPD event, which can be a minute away; the compositor stays at a fallback mode (a 3840x2160 picture stretched over a 7680x2160 monitor). Disconnect and connect the link again, as a replug does, two seconds after such a retrain failed, unless a mode set completed in between; at most four times per resume. The work goes with the other Type-C works: disabled at probe, while unbound and across system sleep, so that nothing arms it behind a cancel. --- drivers/gpu/drm/apple/dcp-internal.h | 9 ++++ drivers/gpu/drm/apple/dcp.c | 42 +++++++++++++++++++ drivers/gpu/drm/apple/dcp.h | 1 + .../drm/apple/dcp_external_announce_test.c | 1 + drivers/gpu/drm/apple/iomfb.c | 4 +- drivers/gpu/drm/apple/iomfb_template.c | 4 ++ 6 files changed, 60 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index f233e9486f787..e2014142187e5 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -715,6 +715,15 @@ struct apple_dcp { struct dcp_fabric_presence hdmi_presence; /* The state before the current series of HPD edges began. */ enum dcp_fabric_presence_state hdmi_presence_prev; + /* + * A desktop 26.x Type-C display that came back from system sleep with a + * partial mode list (the sink was not ready for its EDID yet) is + * connected again after a retrain found no mode. + */ + struct delayed_work link_recovery_wq; + unsigned int link_recovery_tries; + unsigned int link_recovery_snap; + atomic_t modesets_done; unsigned long hdmi_edge_jiffies; bool hdmi_edge_seen; struct delayed_work hdmi_settle_wq; diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 786f505dd9eac..e66e02120d4bd 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -2328,6 +2328,43 @@ static void dcp_placeholder_edid_work(struct work_struct *work) mutex_unlock(&dcp->hpd_mutex); } +#define DCP_LINK_RECOVERY_TRIES 4 +#define DCP_LINK_RECOVERY_DELAY_MS 2000 + +/* + * A retrain after a hotplug found no mode for the active CRTC: the sink's + * EDID was incomplete (it had just woken from sleep and listed only its base + * modes). Nothing re-reads it until the next HPD event, which can be a + * minute away, so a Type-C display stays at a fallback mode. Connect the + * link again, as a replug does, unless a mode set completed in between. + */ +void dcp_link_recovery_schedule(struct apple_dcp *dcp) +{ + if (dcp->fw_compat != DCP_FIRMWARE_V_26_6_DESKTOP || dcp->main_display || + !dcp_is_typec_output(dcp) || !READ_ONCE(dcp->typec_cable_connected) || + dcp->link_recovery_tries >= DCP_LINK_RECOVERY_TRIES) + return; + dcp->link_recovery_snap = atomic_read(&dcp->modesets_done); + mod_delayed_work(system_freezable_wq, &dcp->link_recovery_wq, + msecs_to_jiffies(DCP_LINK_RECOVERY_DELAY_MS)); +} + +static void dcp_link_recovery_work(struct work_struct *work) +{ + struct apple_dcp *dcp = container_of(to_delayed_work(work), + struct apple_dcp, link_recovery_wq); + struct platform_device *pdev = to_platform_device(dcp->dev); + + if (!READ_ONCE(dcp->typec_cable_connected) || + atomic_read(&dcp->modesets_done) != dcp->link_recovery_snap) + return; + dev_info(dcp->dev, "link recovery %u: connecting again after a retrain that found no mode\n", + ++dcp->link_recovery_tries); + dcp_dptx_disconnect_oob(pdev, 0); + msleep(500); + dcp_dptx_connect_oob(pdev, 0); +} + static void dcp_typec_reconnect_work(struct work_struct *work) { struct apple_dcp *dcp = @@ -4411,6 +4448,7 @@ VISIBLE_IF_KUNIT void dcp_disable_typec_work(struct apple_dcp *dcp, bool release cancel_delayed_work_sync(&dcp->external_stop_retry_wq); disable_delayed_work_sync(&dcp->hdmi_settle_wq); disable_delayed_work_sync(&dcp->hdmi_recheck_wq); + disable_delayed_work_sync(&dcp->link_recovery_wq); dcp_hdmi_disable(dcp); } @@ -4424,6 +4462,7 @@ VISIBLE_IF_KUNIT void dcp_enable_typec_work(struct apple_dcp *dcp) enable_delayed_work(&dcp->announce_wq); enable_delayed_work(&dcp->hdmi_settle_wq); enable_delayed_work(&dcp->hdmi_recheck_wq); + enable_delayed_work(&dcp->link_recovery_wq); dcp_hdmi_enable(dcp); scoped_guard(mutex, &dcp->hpd_mutex) dcp->typec_work_enabled = true; @@ -5255,6 +5294,7 @@ static int dcp_platform_probe_pipe(struct platform_device *pdev) init_completion(&dcp->typec_iomfb_hpd_ready); INIT_DELAYED_WORK(&dcp->typec_reconnect_wq, dcp_typec_reconnect_work); + INIT_DELAYED_WORK(&dcp->link_recovery_wq, dcp_link_recovery_work); INIT_DELAYED_WORK(&dcp->placeholder_edid_wq, dcp_placeholder_edid_work); INIT_DELAYED_WORK(&dcp->typec_relink_wq, dcp_typec_relink_work); @@ -5272,6 +5312,7 @@ static int dcp_platform_probe_pipe(struct platform_device *pdev) disable_delayed_work(&dcp->placeholder_edid_wq); disable_delayed_work(&dcp->typec_relink_wq); disable_delayed_work(&dcp->announce_wq); + disable_delayed_work(&dcp->link_recovery_wq); dcp->announce.stopped = true; /* * A native external pipe's bind enables these; firmware callbacks may @@ -6001,6 +6042,7 @@ static int dcp_platform_resume(struct device *dev) if (READ_ONCE(dcp->ext_dead)) return 0; + dcp->link_recovery_tries = 0; dcp_release_after_sleep(dcp); dcp_enable_typec_work(dcp); /* The HDMI output's PHY and crossbar may have lost their setup in sleep. */ diff --git a/drivers/gpu/drm/apple/dcp.h b/drivers/gpu/drm/apple/dcp.h index 86007f00c9a4f..89f72eed69ad1 100644 --- a/drivers/gpu/drm/apple/dcp.h +++ b/drivers/gpu/drm/apple/dcp.h @@ -144,6 +144,7 @@ unsigned int apple_j613_25g83_clock_hz(void); bool apple_t8122_25g83_board(void); void dcp_typec_hybrid_first_dt(void); +void dcp_link_recovery_schedule(struct apple_dcp *dcp); /* Desktop 26.6 dual-pipe peer (dcp-peer-v26_6.inc, included from dcp.c) */ struct dptx_port; diff --git a/drivers/gpu/drm/apple/dcp_external_announce_test.c b/drivers/gpu/drm/apple/dcp_external_announce_test.c index bd0af843698f9..021293c10ba1d 100644 --- a/drivers/gpu/drm/apple/dcp_external_announce_test.c +++ b/drivers/gpu/drm/apple/dcp_external_announce_test.c @@ -111,6 +111,7 @@ static struct announce_test *announce_test_init(struct kunit *test) INIT_DELAYED_WORK(&dcp->typec_relink_wq, announce_test_noop); INIT_DELAYED_WORK(&dcp->hdmi_settle_wq, announce_test_noop); INIT_DELAYED_WORK(&dcp->hdmi_recheck_wq, announce_test_noop); + INIT_DELAYED_WORK(&dcp->link_recovery_wq, announce_test_noop); INIT_DELAYED_WORK(&dcp->bl_fallback_wq, announce_test_noop); INIT_DELAYED_WORK(&dcp->backlight.fade_work, announce_test_noop); INIT_WORK(&dcp->external_stop_work, announce_test_noop); diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index b2a34c28f3008..7daa5ac848a5c 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -1221,10 +1221,12 @@ void dcp_hotplug(struct work_struct *work) * callback has completed. */ ret = dcp_retrain_active_crtc(connector); - if (ret) + if (ret) { dev_warn(dcp->dev, "failed to retrain active CRTC after hotplug: %d\n", ret); + dcp_link_recovery_schedule(dcp); + } } dcp_test_point(dcp, DCP_TEST_HOTPLUG_EVENT); diff --git a/drivers/gpu/drm/apple/iomfb_template.c b/drivers/gpu/drm/apple/iomfb_template.c index 59f5c1bd622cb..3d1d0b54f92f2 100644 --- a/drivers/gpu/drm/apple/iomfb_template.c +++ b/drivers/gpu/drm/apple/iomfb_template.c @@ -3025,6 +3025,8 @@ int DCP_FW_NAME(iomfb_modeset)(struct apple_dcp *dcp, mode->min_vrr : 0); if (ret != -EOPNOTSUPP) { dcp->desktop->mode_live = !ret; + if (!ret) + atomic_inc(&dcp->modesets_done); return ret; } } @@ -3116,6 +3118,8 @@ int DCP_FW_NAME(iomfb_modeset)(struct apple_dcp *dcp, kref_put(&cookie->refcount, release_wait_cookie); dcp->desktop->mode_live = !ret; + if (!ret) + atomic_inc(&dcp->modesets_done); return ret; } #else From bac7c51b4053f1fbb022b09d75728d240dc4745d Mon Sep 17 00:00:00 2001 From: fbalazsy Date: Tue, 27 Oct 2026 15:29:38 +0100 Subject: [PATCH 365/388] drm/apple: dual pipe: install the pair with the mode's VRR range The pair was installed first with adaptive sync off, because the CRTC still had VRR disabled, and the compositor then switched VRR on: that stopped the pair and installed it again. When the sink drops its link in between (a monitor waking up or switching its input layout), the second install fails (set_digital_out_mode 0x6) and the display stays dark until the next replug. Install the pair with the mode's minimum refresh rate from the start, whatever the CRTC asks. A VRR toggle then finds the pair with the same timing, colour and rate and keeps it. Swaps on a VRR mode carry their timestamps either way, so the compositor still paces the frames. --- drivers/gpu/drm/apple/dcp-peer-v26_6.inc | 7 ++++--- drivers/gpu/drm/apple/iomfb_template.c | 9 ++++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp-peer-v26_6.inc b/drivers/gpu/drm/apple/dcp-peer-v26_6.inc index 70e16c5330a2b..67dea03eb5d17 100644 --- a/drivers/gpu/drm/apple/dcp-peer-v26_6.inc +++ b/drivers/gpu/drm/apple/dcp-peer-v26_6.inc @@ -1040,9 +1040,10 @@ int dcp_peer_drm_modeset_26_6(struct apple_dcp *dcp, u32 hdisplay, /* * DRM sets the same mode again right after the pair came up. The * merged pipe no longer resolves the original timing in A364, so a - * second install would fail and tear the pair down: keep it. A - * changed adaptive-sync rate (VRR switched on or off) is applied with a - * mode set, as on one pipe: the pair is stopped and installed again. + * second install would fail and tear the pair down: keep it. The pair + * runs with the mode's adaptive-sync rate, so VRR switched on or off + * keeps it too; only a changed rate for the same timing (a new mode + * list) stops the pair and installs it again. */ scoped_guard(mutex, &dcp_peer_lock) { if (dcp->peer_tiling.modeset_complete && diff --git a/drivers/gpu/drm/apple/iomfb_template.c b/drivers/gpu/drm/apple/iomfb_template.c index 3d1d0b54f92f2..d6334e88d3ed0 100644 --- a/drivers/gpu/drm/apple/iomfb_template.c +++ b/drivers/gpu/drm/apple/iomfb_template.c @@ -3012,7 +3012,11 @@ int DCP_FW_NAME(iomfb_modeset)(struct apple_dcp *dcp, /* * A mode wider than one pipe goes to both pipes of a reserved pair, - * with adaptive sync set the same on both. + * with adaptive sync set the same on both. The pair takes the mode's + * VRR range from its first install, whatever the CRTC asks: switching + * VRR on later would stop and install the pair again, and an install + * that fails (the sink dropped its link meanwhile) leaves the display + * dark. Swaps keep their timestamps, so the compositor still paces it. */ if (!dcp->main_display) { dev_dbg(dcp->dev, "modeset %ux%u vrr %u min %#x enabled %u\n", @@ -3021,8 +3025,7 @@ int DCP_FW_NAME(iomfb_modeset)(struct apple_dcp *dcp, ret = dcp_peer_drm_modeset_26_6(dcp, crtc_state->mode.hdisplay, mode->timing_mode_id, mode->color_mode_id, - mode->vrr && crtc_state->vrr_enabled ? - mode->min_vrr : 0); + mode->vrr ? mode->min_vrr : 0); if (ret != -EOPNOTSUPP) { dcp->desktop->mode_live = !ret; if (!ret) From 7b990950a349dd1e88cfcd508d589d8216525597 Mon Sep 17 00:00:00 2001 From: fbalazsy Date: Tue, 27 Oct 2026 15:29:38 +0100 Subject: [PATCH 366/388] drm/apple: fabric: keep the hybrid pipeline's HDMI crossbar apart from its Type-C route's On desktop 26.x a Type-C route sets dcp->xbar to its own crossbar control, for the compact DPTX link configuration. dcp->xbar was also the fixed HDMI output's control, so on the hybrid dcpext0 every later use for HDMI went to the Type-C port's control instead: when the route was let go with an HDMI display present, handing the pipeline back to HDMI selected the Type-C port's control with the HDMI index. The next route of that port, on dcpext1, then waited in mux_control_select() for good, holding the fabric lock, with the port controller's update work and interrupt thread stuck behind it (seen with a G95NC on USB-C and HDMI, switching its picture-by-picture layout). Keep the fixed output's control in dcp->fixed_xbar and select, release and check the fixed route with it only; dcp->xbar stays the control a link configuration takes up and down, and points at the fixed one again when the pipeline is handed back to HDMI. A Type-C route's control is taken with mux_control_try_select(): only a route of the same port can hold it, and that one lets it go under the fabric lock held here, so a wait could never end. --- drivers/gpu/drm/apple/dcp-fabric.c | 40 ++++++++++++++++++---------- drivers/gpu/drm/apple/dcp-internal.h | 6 +++++ drivers/gpu/drm/apple/dcp.c | 8 +++--- 3 files changed, 37 insertions(+), 17 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp-fabric.c b/drivers/gpu/drm/apple/dcp-fabric.c index 6e0e049d47005..4373ce946536e 100644 --- a/drivers/gpu/drm/apple/dcp-fabric.c +++ b/drivers/gpu/drm/apple/dcp-fabric.c @@ -413,7 +413,7 @@ static int dcp_typec_route_activate(struct apple_dcp_typec_route *route, return ret; if (dcp->fixed_route_selected) { - ret = mux_control_deselect(dcp->xbar); + ret = mux_control_deselect(dcp->fixed_xbar); dcp->fixed_route_selected = false; if (ret) return ret; @@ -433,15 +433,22 @@ static int dcp_typec_route_activate(struct apple_dcp_typec_route *route, ret = dcp_dpxbar_tunnel_select_source(xbar, route->mux_index); if (ret == -EOPNOTSUPP) ret = 0; - } else { - ret = xbar == route->xbar ? - mux_control_select(xbar, route->mux_index) : 0; + } else if (xbar == route->xbar) { + /* + * The control is only ever held by a route of this port, which + * lets it go under the fabric lock held here: waiting for it + * would wait for good, with the port controller's update work + * and its interrupt stuck behind us. + */ + ret = mux_control_try_select(xbar, route->mux_index); + if (ret == -EBUSY) + dev_err(dcp->dev, "Type-C crossbar control still held, route not taken\n"); } if (ret) { - if (dcp->xbar) { + if (dcp->fixed_xbar) { int restore_ret; - restore_ret = mux_control_select(dcp->xbar, + restore_ret = mux_control_select(dcp->fixed_xbar, dcp->fixed_mux_index); if (!restore_ret) dcp->fixed_route_selected = true; @@ -627,8 +634,10 @@ static int dcp_typec_route_deactivate(struct apple_dcp_typec_route *route) dcp->phy = dcp->fixed_phy; dcp->dptx_phy = dcp->fixed_dptx_phy; - if (dcp->xbar && !dcp->fixed_route_selected) { - ret = mux_control_select(dcp->xbar, + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) + WRITE_ONCE(dcp->xbar, dcp->fixed_xbar); + if (dcp->fixed_xbar && !dcp->fixed_route_selected) { + ret = mux_control_select(dcp->fixed_xbar, dcp->fixed_mux_index); if (ret) return ret; @@ -945,10 +954,10 @@ static void dcp_fixed_hdmi_reinit_locked(struct apple_dcp *dcp, const char *why) if (ret) dev_warn(dcp->dev, "HDMI: PHY not set up again: %d\n", ret); } - if (dcp->xbar && dcp->fixed_route_selected && !dcp->active_typec_route) { + if (dcp->fixed_xbar && dcp->fixed_route_selected && !dcp->active_typec_route) { scoped_guard(mutex, &dcp->tb_lock) dcp->direct_xbar_up = NULL; - ret = mux_control_deselect(dcp->xbar); + ret = mux_control_deselect(dcp->fixed_xbar); dcp->fixed_route_selected = false; if (!ret) ret = dcp_fixed_output_select(dcp); @@ -1917,7 +1926,7 @@ int dcp_direct_crossbar_link(struct apple_dcp *dcp, bool up) if (route && !route->tunnel && route->selected) xbar = route->active_xbar ?: route->xbar; else if (!route && dcp->fixed_route_selected) - xbar = dcp->xbar; + xbar = dcp->fixed_xbar; /* A deselected output went idle, and its clocks with it. */ if (dcp->direct_xbar_up && dcp->direct_xbar_up != xbar) dcp->direct_xbar_up = NULL; @@ -3811,10 +3820,13 @@ static void dcp_fixed_restore_phy(void *ctx) static int dcp_fixed_select_mux(void *ctx) { struct apple_dcp *dcp = ctx; - int ret = mux_control_select(dcp->xbar, dcp->fixed_mux_index); + int ret = mux_control_select(dcp->fixed_xbar, dcp->fixed_mux_index); - if (!ret) + if (!ret) { dcp->fixed_route_selected = true; + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) + WRITE_ONCE(dcp->xbar, dcp->fixed_xbar); + } return ret; } @@ -3827,7 +3839,7 @@ static int dcp_fixed_output_select(struct apple_dcp *dcp) { lockdep_assert_held(&dcp_typec_fabric_lock); return dcp_fabric_run_fixed(!!dcp->fixed_phy, !!dcp->active_typec_route, - dcp->xbar && !dcp->fixed_route_selected, + dcp->fixed_xbar && !dcp->fixed_route_selected, &dcp_fixed_ops, dcp); } diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index e2014142187e5..af5263bd31ca6 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -588,7 +588,13 @@ struct apple_dcp { /* these fields are output port specific */ struct phy *phy; struct phy *fixed_phy; + /* + * The crossbar control a link configuration takes up and down: the + * fixed output's, or on desktop 26.x the active Type-C route's. + */ struct mux_control *xbar; + /* The fixed (HDMI) output's own crossbar control, selected for it. */ + struct mux_control *fixed_xbar; struct typec_mux *typec_mux; struct apple_dcp_typec_route typec_routes[DCP_MAX_TYPEC_ROUTES]; struct apple_dcp_typec_route *active_typec_route; diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index e66e02120d4bd..6b63f232b208b 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -4417,7 +4417,7 @@ static void dcp_release_fixed_route(void *data) struct apple_dcp *dcp = data; if (dcp->fixed_route_selected) { - mux_control_deselect(dcp->xbar); + mux_control_deselect(dcp->fixed_xbar); dcp->fixed_route_selected = false; } typec_mux_put(dcp->typec_mux); @@ -5092,8 +5092,9 @@ static int dcp_fixed_output_error(struct apple_dcp *dcp, int err, const char *wh dev_err(dcp->dev, "HDMI output unusable (%s: %d); USB-C displays are not affected\n", what, err); if (dcp->fixed_route_selected) - mux_control_deselect(dcp->xbar); + mux_control_deselect(dcp->fixed_xbar); dcp->fixed_route_selected = false; + dcp->fixed_xbar = NULL; dcp->xbar = NULL; if (!IS_ERR_OR_NULL(dcp->typec_mux)) typec_mux_put(dcp->typec_mux); @@ -5505,7 +5506,8 @@ static int dcp_platform_probe_pipe(struct platform_device *pdev) ret = devm_add_action_or_reset(dev, dcp_release_fixed_route, dcp); if (ret) return ret; - ret = mux_control_select(dcp->xbar, mux_index); + dcp->fixed_xbar = dcp->xbar; + ret = mux_control_select(dcp->fixed_xbar, mux_index); if (ret) dev_warn(dev, "mux_control_select failed: %d\n", ret); else From 8a1a849ddc5aa566c716dee1c1b27ad5b82e2589 Mon Sep 17 00:00:00 2001 From: fbalazsy Date: Tue, 27 Oct 2026 15:29:38 +0100 Subject: [PATCH 367/388] drm/apple: fabric: do not route a Type-C port again when its sink drops HPD When the sink of a desktop 26.x Type-C route drops HPD, the route gives its pipeline back and the freed pipeline goes to a port left waiting. The port's own DP state still said HPD at that point, so it was taken as waiting itself and routed again at once, onto the same pipeline or, with an HDMI display on the hybrid, onto the other one, and its connection was replayed against a sink without HPD. Clear the port's HPD before the pipeline is freed (the pairing pass still sees what it counted before), connect a live HDMI display the hybrid has gone back to, as the DP-exit path does, and leave a parked hybrid without a link crossbar: the released route's control may now belong to the other pipeline. The error path that restores the fixed route points the link crossbar at it again. --- drivers/gpu/drm/apple/dcp-fabric.c | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp-fabric.c b/drivers/gpu/drm/apple/dcp-fabric.c index 4373ce946536e..f1cede0e34e7e 100644 --- a/drivers/gpu/drm/apple/dcp-fabric.c +++ b/drivers/gpu/drm/apple/dcp-fabric.c @@ -450,9 +450,11 @@ static int dcp_typec_route_activate(struct apple_dcp_typec_route *route, restore_ret = mux_control_select(dcp->fixed_xbar, dcp->fixed_mux_index); - if (!restore_ret) + if (!restore_ret) { dcp->fixed_route_selected = true; - else + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) + WRITE_ONCE(dcp->xbar, dcp->fixed_xbar); + } else dev_err(dcp->dev, "failed to restore fixed display route: %d\n", restore_ret); @@ -646,6 +648,9 @@ static int dcp_typec_route_deactivate(struct apple_dcp_typec_route *route) } else { dcp->phy = dcp->typec_routes[0].phy; dcp->dptx_phy = dcp->typec_routes[0].dptx_phy; + /* The released route's control may go to another pipeline. */ + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) + WRITE_ONCE(dcp->xbar, NULL); } } else { /* @@ -1555,6 +1560,10 @@ static int dcp_typec_route_set(struct typec_mux_dev *mux, port->applied_conf = dp_conf; /* Failed route acquisition must remain retryable on the next update. */ port->applied_valid = false; + + /* did the pairing pass count this port's direct stream so far? */ + was_counted = port->dp_wanted && port->dp_hpd; + /* Keep a direct route until DCP acknowledges release of its display. */ if (!hpd && port->owner && !port->owner->tunnel) { struct apple_dcp *dcp = port->owner->dcp; @@ -1577,17 +1586,24 @@ static int dcp_typec_route_set(struct typec_mux_dev *mux, if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) { port->preferred_route = port->owner; port->hpd = false; + /* + * Not waiting for a pipeline either until HPD returns: + * the pipeline just freed must not route this port again. + */ + port->dp_hpd = false; ret = dcp_typec_route_deactivate(port->owner); if (ret) return ret; port->owner = NULL; + + /* The hybrid went back to a live HDMI display. */ + if (dcp->hdmi_hpd && dcp->active && + gpiod_get_value_cansleep(dcp->hdmi_hpd)) + dcp_dptx_connect(dcp, 0); dcp_typec_pipeline_freed(); } } - /* did the pairing pass count this port's direct stream so far? */ - was_counted = port->dp_wanted && port->dp_hpd; - if (!is_dp) { port->dp_wanted = false; port->dp_hpd = false; From d72a1645013ade3e6b74f8663189e6f2f1a0b1cb Mon Sep 17 00:00:00 2001 From: fbalazsy Date: Tue, 27 Oct 2026 15:29:38 +0100 Subject: [PATCH 368/388] drm/apple: desktop HDMI: report the unplug, move a two-pipe Type-C display onto the freed hybrid On a desktop 26.x processor the HDMI HPD handler left disconnects to the firmware. The J616s's DP-to-HDMI converter (a Parade DP855) keeps its DP link up without a sink, so the firmware never reports an unplug: the HDMI connector stayed connected and the hybrid pipeline busy until the next plug. Debounce both edges for 500 ms; when HPD stays low, report the unplug as a CRTC power-off with HPD low does, and start the HDMI presence hold, which frees the hybrid when it expires. A Type-C display wider than one pipe that came while HDMI held the hybrid sits on the Type-C-only pipeline, which pairs with nothing above it. When the hybrid becomes free again, move such a display onto it, as a replug does, so that it gets both pipes. While the hybrid drives a Type-C display, an HDMI plug only waits: the cold-plug HPD pulse would have hit the Type-C display's link. --- drivers/gpu/drm/apple/dcp-fabric.c | 78 ++++++++++++++++++++++++ drivers/gpu/drm/apple/dcp-internal.h | 5 ++ drivers/gpu/drm/apple/dcp-peer-v26_6.inc | 2 + drivers/gpu/drm/apple/dcp.c | 32 +++++++--- drivers/gpu/drm/apple/dcp.h | 1 + 5 files changed, 108 insertions(+), 10 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp-fabric.c b/drivers/gpu/drm/apple/dcp-fabric.c index f1cede0e34e7e..fa76b4ea9b7de 100644 --- a/drivers/gpu/drm/apple/dcp-fabric.c +++ b/drivers/gpu/drm/apple/dcp-fabric.c @@ -530,6 +530,8 @@ static int dcp_typec_route_deactivate(struct apple_dcp_typec_route *route) bool ended_attachment; int ret = 0; + WRITE_ONCE(dcp->peer_starved, false); + guard(mutex)(&dcp->external_lock); if (dcp_is_external_h17p(dcp) && (dcp->external_error || (dcp->external_power_attempted && !dcp->external_stopped))) @@ -737,6 +739,7 @@ static bool dcp_typec_keep_order(void) static void dcp_typec_pipeline_freed(void); static int dcp_fixed_output_select(struct apple_dcp *dcp); +static void dcp_typec_port_attach(struct apple_dcp_typec_port *port); static bool dcp_hdmi_settle_enabled(struct apple_dcp *dcp) { @@ -821,9 +824,84 @@ static bool dcp_hdmi_sample(struct apple_dcp *dcp, u64 generation, int level) return accepted; } +/* + * A Type-C display wider than one pipe sits on a Type-C-only pipeline, + * which has no peer above it, because the hybrid was taken when it came + * (by an HDMI display, gone since). Move it onto the free hybrid, as a + * replug does, so that it gets both pipes. + */ +static void dcp_typec_move_starved_locked(void) +{ + struct apple_dcp_typec_port *port; + + lockdep_assert_held(&dcp_typec_fabric_lock); + if (!dcp_typec_hybrid_first()) + return; + + list_for_each_entry(port, &dcp_typec_ports, link) { + struct apple_dcp_typec_route *owner = port->owner, *to = NULL; + struct apple_dcp_typec_route *route; + struct apple_dcp *dcp; + + if (!owner || owner->tunnel || !READ_ONCE(owner->dcp->peer_starved)) + continue; + list_for_each_entry(route, &port->routes, port_link) { + if (route != owner && route->dcp->fixed_phy && + dcp_typec_route_available(route)) { + to = route; + break; + } + } + if (!to) + continue; + + dcp = owner->dcp; + dev_info(dcp->dev, "display wider than one pipe moves to the free %s\n", + dev_name(to->dcp->dev)); + if (dcp_dptx_disconnect_oob(to_platform_device(dcp->dev), 0)) + continue; + port->preferred_route = to; + port->hpd = false; + if (dcp_typec_route_deactivate(owner)) + continue; + port->owner = NULL; + if (dcp_typec_route_activate(to, to->xbar)) { + dev_warn(dcp->dev, "hybrid not taken, the display stays on its pipeline\n"); + if (dcp_typec_route_activate(owner, owner->xbar)) + continue; + to = owner; + } + port->owner = to; + dcp_typec_port_attach(port); + } +} + static void dcp_hdmi_capacity_available(void *ctx) { dcp_typec_pipeline_freed(); + dcp_typec_move_starved_locked(); +} + +/* + * Desktop 26.x: the HDMI display is gone (its converter's HPD fell and + * stayed low); its firmware reports no unplug. Start the presence hold from + * here, as the edge handler of other machines does, also where presence was + * never tracked: when the hold expires, the desktop has let the HDMI CRTC go + * and the hybrid is free for a Type-C display again. + */ +void dcp_fabric_hdmi_unplugged(struct apple_dcp *dcp) +{ + unsigned long flags; + u64 generation; + + guard(mutex)(&dcp_typec_fabric_lock); + generation = dcp_hdmi_edge(dcp); + if (!generation) + return; + spin_lock_irqsave(&dcp->hdmi_presence_lock, flags); + dcp->hdmi_presence_prev = DCP_FABRIC_PRESENT; + spin_unlock_irqrestore(&dcp->hdmi_presence_lock, flags); + dcp_hdmi_sample(dcp, generation, gpiod_get_value_cansleep(dcp->hdmi_hpd)); } static void dcp_hdmi_settle_work(struct work_struct *work) diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index af5263bd31ca6..48d47e057d109 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -293,6 +293,11 @@ struct apple_dcp { */ struct apple_dcp *peer_reserved; struct apple_dcp *peer_owner; + /* + * This pipeline's Type-C display is wider than one pipe and found no + * peer above it: it needs the hybrid, which was taken when it came. + */ + bool peer_starved; struct dcp_peer_tiling peer_tiling; bool peer_unbinding; /* Firmware display endpoint (its Disp_Index), apple,dcp-endpoint; diff --git a/drivers/gpu/drm/apple/dcp-peer-v26_6.inc b/drivers/gpu/drm/apple/dcp-peer-v26_6.inc index 67dea03eb5d17..94467fbf0bde9 100644 --- a/drivers/gpu/drm/apple/dcp-peer-v26_6.inc +++ b/drivers/gpu/drm/apple/dcp-peer-v26_6.inc @@ -272,9 +272,11 @@ int dcp_peer_hints_26_6(struct apple_dcp *dcp, struct dptx_port *port, } if (!peer) { dev_info(dcp->dev, "no idle peer pipe for a %u px display\n", width); + WRITE_ONCE(dcp->peer_starved, !!READ_ONCE(dcp->active_typec_route)); memset(&r->primary_id, 0, 32); return 0; } + WRITE_ONCE(dcp->peer_starved, false); dcp->peer_reserved = peer; peer->peer_owner = dcp; dev_info(dcp->dev, "reserved peer %s endpoint %u for %ux%u\n", diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 6b63f232b208b..28b6d03bbc22f 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -2010,24 +2010,36 @@ static int dcp_desktop_dptx_disconnect_oob(struct apple_dcp *dcp, u32 port) } /* - * HDMI HPD of a desktop 26.x external processor. A disconnect is left to - * the firmware, which detects it itself. A cold plug after boot finds the - * port connected without a sink: the firmware never saw this HPD, so pulse - * the AP-asserted HPD to make it export the modes now. + * HDMI HPD of a desktop 26.x external processor. A cold plug after boot + * finds the port connected without a sink: the firmware never saw this HPD, + * so pulse the AP-asserted HPD to make it export the modes now. An unplug + * is reported by nothing else: the DP-to-HDMI converter keeps its DP link + * up without a sink and the firmware sees no change. */ irqreturn_t dcp_desktop_dp2hdmi_hpd(struct apple_dcp *dcp) { struct dptx_port *dptx = &dcp->dptxport[0]; - bool connected = gpiod_get_value_cansleep(dcp->hdmi_hpd); + bool connected; int ret; - if (connected) { - msleep(500); - connected = gpiod_get_value_cansleep(dcp->hdmi_hpd); - } + msleep(500); + connected = gpiod_get_value_cansleep(dcp->hdmi_hpd); dev_dbg(dcp->dev, "DP2HDMI HPD irq, connected:%d after 500 ms\n", connected); - if (!connected) + if (!connected) { + if (!READ_ONCE(dcp->active_typec_route) && dcp->connector && + READ_ONCE(dcp->connector->connected)) { + dev_info(dcp->dev, "HDMI display unplugged\n"); + disconnected_hpd_event(dcp->connector); + dcp_dptx_disconnect(dcp, 0); + dcp_fabric_hdmi_unplugged(dcp); + } + return IRQ_HANDLED; + } + /* The pipeline drives a Type-C display: the HDMI display waits for it. */ + if (READ_ONCE(dcp->active_typec_route)) { + dev_info(dcp->dev, "HDMI display waits: its pipeline drives a Type-C display\n"); return IRQ_HANDLED; + } mutex_lock(&dcp->hpd_mutex); if (dptx_service_available(dptx) && dptx->connected && diff --git a/drivers/gpu/drm/apple/dcp.h b/drivers/gpu/drm/apple/dcp.h index 89f72eed69ad1..9d39c728ccd78 100644 --- a/drivers/gpu/drm/apple/dcp.h +++ b/drivers/gpu/drm/apple/dcp.h @@ -144,6 +144,7 @@ unsigned int apple_j613_25g83_clock_hz(void); bool apple_t8122_25g83_board(void); void dcp_typec_hybrid_first_dt(void); +void dcp_fabric_hdmi_unplugged(struct apple_dcp *dcp); void dcp_link_recovery_schedule(struct apple_dcp *dcp); /* Desktop 26.6 dual-pipe peer (dcp-peer-v26_6.inc, included from dcp.c) */ From 0d43a7f471acfa5b24893f43bb0d86d5021706b1 Mon Sep 17 00:00:00 2001 From: fbalazsy Date: Tue, 27 Oct 2026 15:29:39 +0100 Subject: [PATCH 369/388] drm/apple: fabric: hand the hybrid between an HDMI and a two-pipe Type-C display A monitor on both the HDMI port and a USB-C port of a J616s (a Samsung G95NC at 7680x2160) can have both display pipes on one input only: the hybrid dcpext0 is the HDMI output's pipeline and the only primary of a pair. The monitor does not tell which input it shows: both keep HPD up, and the unselected DP input still answers for its EDID. So typec_hybrid_first says who gets the hybrid: -1 follows the device tree (the default), 0 puts an HDMI display first, 1 a Type-C display. A write applies at once, so that the desktop can follow the monitor's input. - Type-C first: a Type-C display that asked for two pipes and found no peer takes the hybrid. The HDMI display is reported gone, its link released (retried while the firmware is busy with a link nobody answers) and it waits; it gets the hybrid back when the Type-C display sets a mode within one pipe (picture by picture) or leaves. - HDMI first: an HDMI display keeps the hybrid. One wider than one pipe also takes the other pipe from a Type-C display, which waits, routed nowhere, until the HDMI display fits one pipe or leaves. A hand-over is an unplug and replug: at most one per ten seconds, and no more than two failed takes until the next plug. Peer reservations that outlived their pair are dropped: when a Type-C route takes a pipeline reserved as another display's peer, and when a display asks for a pair while the pipe it reserved, or the pipe it is reserved for, drives a display of its own. --- drivers/gpu/drm/apple/dcp-fabric.c | 327 +++++++++++++++++- drivers/gpu/drm/apple/dcp-internal.h | 9 + drivers/gpu/drm/apple/dcp-peer-v26_6.inc | 30 +- drivers/gpu/drm/apple/dcp.c | 35 +- drivers/gpu/drm/apple/dcp.h | 2 + .../drm/apple/dcp_external_announce_test.c | 1 + drivers/gpu/drm/apple/iomfb_template.c | 9 + 7 files changed, 394 insertions(+), 19 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp-fabric.c b/drivers/gpu/drm/apple/dcp-fabric.c index fa76b4ea9b7de..b3bb5982524cc 100644 --- a/drivers/gpu/drm/apple/dcp-fabric.c +++ b/drivers/gpu/drm/apple/dcp-fabric.c @@ -45,11 +45,31 @@ module_param(typec_follow_crtc, bool, 0444); * is also the only pipe that can be the primary of a two-pipe display * (firmware endpoint 1; the pipes merge upwards only): with this set a * Type-C port takes it first, so a 7680 px sink on USB-C can pair with - * dcpext1 as its peer. Read when a port is planned (replug to apply). + * dcpext1 as its peer. -1 follows the device tree; a write applies at + * once, handing the hybrid between an HDMI display and a Type-C display + * wider than one pipe (one monitor on two inputs: the desktop's choice of + * input, the monitor cannot tell which one it shows). */ -static bool typec_hybrid_first; -module_param(typec_hybrid_first, bool, 0644); -MODULE_PARM_DESC(typec_hybrid_first, "Type-C ports take the hybrid (HDMI) pipeline first (dual-pipe primary)"); +static int typec_hybrid_first = -1; + +static void dcp_fabric_hybrid_policy_changed(void); + +static int dcp_typec_hybrid_first_set(const char *val, const struct kernel_param *kp) +{ + int ret = param_set_int(val, kp); + + if (!ret) + dcp_fabric_hybrid_policy_changed(); + return ret; +} + +static const struct kernel_param_ops dcp_typec_hybrid_first_ops = { + .set = dcp_typec_hybrid_first_set, + .get = param_get_int, +}; +module_param_cb(typec_hybrid_first, &dcp_typec_hybrid_first_ops, &typec_hybrid_first, 0644); +MODULE_PARM_DESC(typec_hybrid_first, + "Hybrid (HDMI) pipeline: -1 from the device tree (default), 0 HDMI display first, 1 Type-C display first (dual-pipe primary)"); /* Set by a pipeline whose device tree asks for it (apple,typec-hybrid-first). */ static bool typec_hybrid_first_dt; @@ -61,7 +81,9 @@ void dcp_typec_hybrid_first_dt(void) static bool dcp_typec_hybrid_first(void) { - return READ_ONCE(typec_hybrid_first) || READ_ONCE(typec_hybrid_first_dt); + int policy = READ_ONCE(typec_hybrid_first); + + return policy < 0 ? READ_ONCE(typec_hybrid_first_dt) : policy > 0; } MODULE_PARM_DESC(typec_follow_crtc, "Let direct DP-alt routes follow their selected CRTC (default: on)"); @@ -104,6 +126,12 @@ struct apple_dcp_typec_port { struct apple_dcp_typec_route *secondary_target; /* left out of the plan: its planned pipeline is held by a tunnel */ bool plan_dark; + /* + * HDMI display first (typec_hybrid_first): the display on this port + * has given both pipes to an HDMI display wider than one pipe and + * waits, routed nowhere, until that one fits one pipe or leaves. + */ + bool typec_yielded; }; /* @@ -118,6 +146,12 @@ struct apple_dcp_typec_port { static DEFINE_MUTEX(dcp_typec_fabric_lock); static LIST_HEAD(dcp_typec_ports); +/* hybrid-first hand-over (dcp_hybrid_work()), under dcp_typec_fabric_lock: */ +/* when the hybrid last changed hands, 0 if never */ +static unsigned long dcp_hybrid_moved; +/* failed takes of the hybrid since the last success or Type-C DP exit */ +static unsigned int dcp_hybrid_fails; +#define DCP_HYBRID_MAX_FAILS 2 /* Current DTs wire the same stream capacity on every physical port. */ static atomic_t dcp_dual_stream_routes = ATOMIC_INIT(0); @@ -169,6 +203,9 @@ static bool dcp_typec_route_fixed_output_busy(struct apple_dcp_typec_route *rout if (dcp->fixed_connector_type == DRM_MODE_CONNECTOR_USB) return false; + /* Its HDMI display gave the pipeline to a Type-C display. */ + if (READ_ONCE(dcp->hdmi_yielded)) + return false; /* * The 14.7 firmware drives the internal panel on the IOMFB path. * The DPTX service is a separate output, so the live panel does not @@ -419,6 +456,20 @@ static int dcp_typec_route_activate(struct apple_dcp_typec_route *route, return ret; } + /* + * A pipeline reserved as the peer of a display that has not paired + * (an HDMI display on its monitor's unselected input, or a pair whose + * cable went) takes this display: drop the reservation, or this + * display could neither pair nor ask for the hybrid. A running pair + * keeps its peer (dcp_peer_release_26_6() refuses). + */ + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) { + struct apple_dcp *owner = READ_ONCE(dcp->peer_owner); + + if (owner) + dcp_peer_release_26_6(owner); + } + /* * Thunderbolt DP IN: the crossbar connection may only be brought up * once DCP has configured the link and the tunnel pixel clock runs, @@ -531,6 +582,10 @@ static int dcp_typec_route_deactivate(struct apple_dcp_typec_route *route) int ret = 0; WRITE_ONCE(dcp->peer_starved, false); + WRITE_ONCE(dcp->typec_narrow, false); + /* A peer reserved for this route's display is not needed any more. */ + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) + dcp_peer_release_26_6(dcp); guard(mutex)(&dcp->external_lock); if (dcp_is_external_h17p(dcp) && @@ -634,7 +689,12 @@ static int dcp_typec_route_deactivate(struct apple_dcp_typec_route *route) * following DPTX call times out. Park on a Type-C PHY instead, * for the same reason the USB-C-only case does below. */ - if (dcp->hdmi_hpd && gpiod_get_value_cansleep(dcp->hdmi_hpd)) { + /* + * A yielded hybrid stays with the Type-C display through its + * sink's HPD blinks; the HDMI display waits. + */ + if (dcp->hdmi_hpd && !READ_ONCE(dcp->hdmi_yielded) && + gpiod_get_value_cansleep(dcp->hdmi_hpd)) { dcp->phy = dcp->fixed_phy; dcp->dptx_phy = dcp->fixed_dptx_phy; @@ -753,7 +813,8 @@ static enum dcp_fabric_presence_state dcp_hdmi_presence(struct apple_dcp *dcp) enum dcp_fabric_presence_state state; unsigned long flags; - if (!dcp_hdmi_settle_enabled(dcp)) + /* A yielded hybrid is free for Type-C, whatever HDMI shows. */ + if (!dcp_hdmi_settle_enabled(dcp) || READ_ONCE(dcp->hdmi_yielded)) return DCP_FABRIC_ABSENT; spin_lock_irqsave(&dcp->hdmi_presence_lock, flags); state = dcp->hdmi_presence.state; @@ -830,13 +891,12 @@ static bool dcp_hdmi_sample(struct apple_dcp *dcp, u64 generation, int level) * (by an HDMI display, gone since). Move it onto the free hybrid, as a * replug does, so that it gets both pipes. */ -static void dcp_typec_move_starved_locked(void) +static bool dcp_typec_move_starved_locked(void) { struct apple_dcp_typec_port *port; + bool moved = false; lockdep_assert_held(&dcp_typec_fabric_lock); - if (!dcp_typec_hybrid_first()) - return; list_for_each_entry(port, &dcp_typec_ports, link) { struct apple_dcp_typec_route *owner = port->owner, *to = NULL; @@ -873,7 +933,55 @@ static void dcp_typec_move_starved_locked(void) } port->owner = to; dcp_typec_port_attach(port); + moved |= to != owner; } + return moved; +} + +/* + * The Type-C display on the hybrid fits one pipe again and an HDMI display + * waits: move the Type-C display onto a free Type-C-only pipeline, as a + * replug does, and connect the HDMI display. The reclaim of the CRTC + * following machines does not apply on desktop 26.x. + */ +static bool dcp_typec_move_off_hybrid_locked(struct apple_dcp_typec_port *port, + struct apple_dcp *hyb) +{ + struct apple_dcp_typec_route *owner = port->owner, *route, *to = NULL; + + lockdep_assert_held(&dcp_typec_fabric_lock); + if (!owner || owner->dcp != hyb || owner->tunnel || port->safe_pending) + return false; + list_for_each_entry(route, &port->routes, port_link) { + if (route != owner && !route->dcp->fixed_phy && route->dcp->crtc && + dcp_typec_route_available(route)) { + to = route; + break; + } + } + if (!to) + return false; + + dev_info(hyb->dev, "Type-C display moves to %s, HDMI display connects\n", + dev_name(to->dcp->dev)); + if (dcp_dptx_disconnect_oob(to_platform_device(hyb->dev), 0)) + return false; + port->preferred_route = to; + port->hpd = false; + if (dcp_typec_route_deactivate(owner)) + return false; + port->owner = NULL; + if (dcp_typec_route_activate(to, to->xbar)) { + dev_warn(hyb->dev, "Type-C display not moved, it stays on the hybrid\n"); + if (dcp_typec_route_activate(owner, owner->xbar)) + return false; + to = owner; + } + port->owner = to; + dcp_typec_port_attach(port); + if (to != owner && hyb->active && gpiod_get_value_cansleep(hyb->hdmi_hpd) > 0) + dcp_dptx_connect(hyb, 0); + return to != owner; } static void dcp_hdmi_capacity_available(void *ctx) @@ -902,6 +1010,8 @@ void dcp_fabric_hdmi_unplugged(struct apple_dcp *dcp) dcp->hdmi_presence_prev = DCP_FABRIC_PRESENT; spin_unlock_irqrestore(&dcp->hdmi_presence_lock, flags); dcp_hdmi_sample(dcp, generation, gpiod_get_value_cansleep(dcp->hdmi_hpd)); + /* A Type-C display that gave its pipes to this one takes them back. */ + dcp_fabric_hybrid_kick(dcp, 0); } static void dcp_hdmi_settle_work(struct work_struct *work) @@ -968,6 +1078,8 @@ static void dcp_hdmi_recheck_work(struct work_struct *work) mod_delayed_work(system_freezable_wq, &dcp->hdmi_recheck_wq, 0); } +static void dcp_hybrid_work(struct work_struct *work); + void dcp_fabric_init(struct apple_dcp *dcp) { spin_lock_init(&dcp->hdmi_presence_lock); @@ -975,8 +1087,10 @@ void dcp_fabric_init(struct apple_dcp *dcp) dcp->hdmi_presence_prev = DCP_FABRIC_ABSENT; INIT_DELAYED_WORK(&dcp->hdmi_settle_wq, dcp_hdmi_settle_work); INIT_DELAYED_WORK(&dcp->hdmi_recheck_wq, dcp_hdmi_recheck_work); + INIT_DELAYED_WORK(&dcp->hybrid_wq, dcp_hybrid_work); disable_delayed_work(&dcp->hdmi_settle_wq); disable_delayed_work(&dcp->hdmi_recheck_wq); + disable_delayed_work(&dcp->hybrid_wq); } /* Caller enables the HPD IRQ first; a later edge invalidates this sample. */ @@ -1082,7 +1196,7 @@ static void dcp_fabric_snapshot_port(struct apple_dcp_typec_port *port, port->secondary_owner ? &port->secondary_owner->core : NULL; port->core.preferred = port->preferred_route ? &port->preferred_route->core : NULL; - port->core.wanted = port->dp_wanted; + port->core.wanted = port->dp_wanted && !port->typec_yielded; port->core.hpd = port->dp_hpd; port->core.candidate_crtcs[0] = port->connector ? port->connector->candidate_crtcs : 0; @@ -1224,7 +1338,7 @@ static void dcp_typec_route_waiting(void) list_for_each_entry(port, &dcp_typec_ports, link) { struct dcp_fabric_port state = { .owner[0] = port->owner ? &port->owner->core : NULL, - .wanted = port->dp_wanted, + .wanted = port->dp_wanted && !port->typec_yielded, .hpd = port->dp_hpd, }; @@ -1675,7 +1789,7 @@ static int dcp_typec_route_set(struct typec_mux_dev *mux, port->owner = NULL; /* The hybrid went back to a live HDMI display. */ - if (dcp->hdmi_hpd && dcp->active && + if (dcp->hdmi_hpd && dcp->active && !READ_ONCE(dcp->hdmi_yielded) && gpiod_get_value_cansleep(dcp->hdmi_hpd)) dcp_dptx_connect(dcp, 0); dcp_typec_pipeline_freed(); @@ -1683,9 +1797,20 @@ static int dcp_typec_route_set(struct typec_mux_dev *mux, } if (!is_dp) { + struct apple_dcp_typec_route *r; + port->dp_wanted = false; port->dp_hpd = false; port->busy_noted = false; + /* The Type-C display is gone: a hybrid it took goes back to HDMI. */ + dcp_hybrid_fails = 0; + port->typec_yielded = false; + list_for_each_entry(r, &port->routes, port_link) { + if (!READ_ONCE(r->dcp->hdmi_yielded)) + continue; + dev_info(r->dcp->dev, "Type-C display gone: the hybrid goes back to HDMI\n"); + WRITE_ONCE(r->dcp->hdmi_yielded, false); + } /* a Thunderbolt/USB4 DP tunnel is torn down by its own path */ if (port->owner && port->owner->tunnel) { @@ -1767,6 +1892,12 @@ static int dcp_typec_route_set(struct typec_mux_dev *mux, return 0; } + /* It gave its pipes to an HDMI display, see dcp_hybrid_work(). */ + if (!port->owner && port->typec_yielded) { + port->applied_valid = true; + return 0; + } + if (!port->owner) { best = dcp_typec_free_route(port); /* Every pipeline is driving another port's display. */ @@ -4249,6 +4380,176 @@ static const struct dcp_fabric_hdmi_ops dcp_hdmi_ops = { .connect_fixed = dcp_hdmi_connect_fixed, }; +#define DCP_HYBRID_SETTLE_MS 3000 +#define DCP_HYBRID_COOLDOWN_MS 10000 + + +void dcp_fabric_hybrid_kick(struct apple_dcp *dcp, unsigned int delay_ms) +{ + if (dcp->fw_compat != DCP_FIRMWARE_V_26_6_DESKTOP) + return; + mod_delayed_work(system_freezable_wq, &dcp->hybrid_wq, msecs_to_jiffies(delay_ms)); +} + +/* typec_hybrid_first was written: apply it now, past any cooldown. */ +static void dcp_fabric_hybrid_policy_changed(void) +{ + struct apple_dcp_typec_port *port; + struct apple_dcp_typec_route *route; + + guard(mutex)(&dcp_typec_fabric_lock); + dcp_hybrid_moved = 0; + dcp_hybrid_fails = 0; + list_for_each_entry(port, &dcp_typec_ports, link) + list_for_each_entry(route, &port->routes, port_link) + if (route->dcp->fixed_phy && route->dcp->hdmi_hpd) + dcp_fabric_hybrid_kick(route->dcp, 0); +} + +static struct apple_dcp *dcp_typec_port_hybrid(struct apple_dcp_typec_port *port) +{ + struct apple_dcp_typec_route *route; + + list_for_each_entry(route, &port->routes, port_link) + if (route->dcp->fixed_phy && route->dcp->hdmi_hpd) + return route->dcp; + return NULL; +} + +/* + * Who drives the hybrid of a desktop 26.x machine when an HDMI display and + * a Type-C display wider than one pipe both want it (typically one monitor + * on two inputs; it cannot tell which input it shows, so typec_hybrid_first + * says, and the desktop may switch it): + * - Type-C first: the Type-C display takes the hybrid and pairs, the HDMI + * display waits; it gets the hybrid back while the Type-C display fits + * one pipe (picture by picture) or is gone. + * - HDMI first: an HDMI display keeps the hybrid; a Type-C display moves + * off it, and takes it only while no HDMI display is there. + * A hand-over is an unplug and replug, so at most one per cooldown. + */ +static void dcp_hybrid_work(struct work_struct *work) +{ + struct apple_dcp *self = container_of(to_delayed_work(work), struct apple_dcp, + hybrid_wq); + unsigned long cooldown = msecs_to_jiffies(DCP_HYBRID_COOLDOWN_MS); + bool typec_first = dcp_typec_hybrid_first(); + struct apple_dcp_typec_port *port; + bool unyielded = false; + + guard(mutex)(&dcp_typec_fabric_lock); + if (dcp_hybrid_moved && time_before(jiffies, dcp_hybrid_moved + cooldown)) { + mod_delayed_work(system_freezable_wq, &self->hybrid_wq, + dcp_hybrid_moved + cooldown - jiffies); + return; + } + + /* A display that gave its pipes to HDMI takes one again when it may. */ + list_for_each_entry(port, &dcp_typec_ports, link) { + struct apple_dcp *hyb = dcp_typec_port_hybrid(port); + + if (!port->typec_yielded) + continue; + if (!typec_first && hyb && gpiod_get_value_cansleep(hyb->hdmi_hpd) > 0 && + hyb->connector && READ_ONCE(hyb->connector->connected) && + !READ_ONCE(hyb->typec_narrow)) + continue; + dev_info(self->dev, "the display on %pOF takes a pipe again\n", + port->connector_np); + port->typec_yielded = false; + unyielded = true; + } + if (unyielded) { + dcp_typec_route_waiting(); + dcp_hybrid_moved = jiffies ?: 1; + return; + } + + list_for_each_entry(port, &dcp_typec_ports, link) { + struct apple_dcp_typec_route *owner = port->owner, *route, *hybrid = NULL; + struct apple_dcp *hyb; + bool hdmi; + + if (!owner || owner->tunnel) + continue; + list_for_each_entry(route, &port->routes, port_link) { + if (route->dcp->fixed_phy && route->dcp->hdmi_hpd) { + hybrid = route; + break; + } + } + if (!hybrid) + continue; + hyb = hybrid->dcp; + hdmi = gpiod_get_value_cansleep(hyb->hdmi_hpd) > 0; + if (!typec_first) + WRITE_ONCE(hyb->hdmi_yielded, false); + + if (owner == hybrid) { + if (!hdmi || (typec_first && !READ_ONCE(hyb->typec_narrow))) + continue; + dev_info(hyb->dev, "%s: the hybrid goes back to the HDMI display\n", + typec_first ? "Type-C display fits one pipe" : "HDMI display first"); + WRITE_ONCE(hyb->hdmi_yielded, false); + if (dcp_typec_move_off_hybrid_locked(port, hyb)) + dcp_hybrid_moved = jiffies ?: 1; + return; + } + + /* + * HDMI first, and the HDMI display is wider than one pipe: the + * Type-C display on the other pipe gives it up and waits, and + * the HDMI display asks for both pipes again. + */ + if (!typec_first && hdmi && READ_ONCE(hyb->peer_starved) && + hyb->connector && READ_ONCE(hyb->connector->connected)) { + struct apple_dcp *dcp = owner->dcp; + + dev_info(hyb->dev, "the display on %pOF waits: the HDMI display takes both pipes\n", + port->connector_np); + if (dcp_dptx_disconnect_oob(to_platform_device(dcp->dev), 0)) + continue; + port->hpd = false; + if (dcp_typec_route_deactivate(owner)) + continue; + port->owner = NULL; + port->typec_yielded = true; + WRITE_ONCE(hyb->peer_starved, false); + if (!dcp_dptx_disconnect(hyb, 0)) + dcp_dptx_connect(hyb, 0); + dcp_hybrid_moved = jiffies ?: 1; + return; + } + + if (!READ_ONCE(owner->dcp->peer_starved) || hyb->active_typec_route || + dcp_typec_safe_held(hyb) || dcp_hybrid_fails >= DCP_HYBRID_MAX_FAILS || + (!typec_first && hdmi)) + continue; + if (typec_first && !READ_ONCE(hyb->hdmi_yielded)) { + WRITE_ONCE(hyb->hdmi_yielded, true); + dev_info(hyb->dev, "hybrid yielded to the Type-C display on %pOF\n", + port->connector_np); + /* Also a link the firmware keeps without a described display. */ + if ((hyb->connector && READ_ONCE(hyb->connector->connected)) || + hyb->dptxport[0].connected) { + dev_info(hyb->dev, "HDMI display waits: the hybrid goes to a Type-C display wider than one pipe\n"); + dcp_hdmi_yield_link(hyb); + /* The desktop turns the HDMI CRTC off first. */ + mod_delayed_work(system_freezable_wq, &self->hybrid_wq, + msecs_to_jiffies(DCP_HYBRID_SETTLE_MS)); + return; + } + } + if (dcp_typec_move_starved_locked()) + dcp_hybrid_fails = 0; + else if (++dcp_hybrid_fails >= DCP_HYBRID_MAX_FAILS) + dev_warn(hyb->dev, "hybrid not taken %u times, waiting for the next plug\n", + dcp_hybrid_fails); + dcp_hybrid_moved = jiffies ?: 1; + return; + } +} + static void dcp_hdmi_update_locked(struct apple_dcp *dcp) { unsigned long flags; diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index 48d47e057d109..4ffa59bcf9b68 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -298,6 +298,15 @@ struct apple_dcp { * peer above it: it needs the hybrid, which was taken when it came. */ bool peer_starved; + /* + * Desktop 26.x hybrid with apple,typec-hybrid-first: its HDMI display + * has given the pipeline to a Type-C display wider than one pipe and + * waits until that one fits one pipe again or leaves. + */ + bool hdmi_yielded; + /* The display on this pipeline runs a mode within one pipe. */ + bool typec_narrow; + struct delayed_work hybrid_wq; struct dcp_peer_tiling peer_tiling; bool peer_unbinding; /* Firmware display endpoint (its Disp_Index), apple,dcp-endpoint; diff --git a/drivers/gpu/drm/apple/dcp-peer-v26_6.inc b/drivers/gpu/drm/apple/dcp-peer-v26_6.inc index 94467fbf0bde9..b8038db010943 100644 --- a/drivers/gpu/drm/apple/dcp-peer-v26_6.inc +++ b/drivers/gpu/drm/apple/dcp-peer-v26_6.inc @@ -125,6 +125,23 @@ static int dcp_peer_call_oob(struct apple_dcp *dcp, const char *tag, * this the peer stays marked as an owner's peer after its port was taken by * another display, and swap_complete would discard that display's own swaps. */ +/* Drop @owner's reservation of @peer unless their pair runs; peer lock held. */ +static void dcp_peer_drop_locked(struct apple_dcp *owner, struct apple_dcp *peer) +{ + lockdep_assert_held(&dcp_peer_lock); + if (owner->peer_reserved != peer || owner->peer_tiling.vftg || + owner->peer_tiling.member || peer->peer_tiling.vftg || + peer->peer_tiling.member) + return; + memset(&owner->peer_tiling, 0, sizeof(owner->peer_tiling)); + memset(&peer->peer_tiling, 0, sizeof(peer->peer_tiling)); + peer->peer_owner = NULL; + owner->peer_reserved = NULL; + owner->peer_active = false; + dev_info(owner->dev, "peer %s no longer reserved: it drives a display of its own\n", + dev_name(peer->dev)); +} + void dcp_peer_release_26_6(struct apple_dcp *dcp) { guard(mutex)(&dcp_peer_lock); @@ -249,6 +266,16 @@ int dcp_peer_hints_26_6(struct apple_dcp *dcp, struct dptx_port *port, guard(mutex)(&dcp_peer_lock); if (dcp->peer_unbinding || dcp->crashed) return -ENODEV; + /* + * A reservation made before a pipe got a display of its own (a Type-C + * display moved there, or an HDMI display that came) is stale: the + * pipe is nobody's peer any more. A running pair keeps it. + */ + if (dcp->peer_owner && READ_ONCE(dcp->active_typec_route)) + dcp_peer_drop_locked(dcp->peer_owner, dcp); + if (dcp->peer_reserved && + (dcp_peer_reject_26_6(dcp, dcp->peer_reserved) & ~BIT(1))) + dcp_peer_drop_locked(dcp, dcp->peer_reserved); if (dcp->peer_owner) { memset(&r->primary_id, 0, 32); return 0; @@ -272,7 +299,8 @@ int dcp_peer_hints_26_6(struct apple_dcp *dcp, struct dptx_port *port, } if (!peer) { dev_info(dcp->dev, "no idle peer pipe for a %u px display\n", width); - WRITE_ONCE(dcp->peer_starved, !!READ_ONCE(dcp->active_typec_route)); + WRITE_ONCE(dcp->peer_starved, true); + dcp_fabric_hybrid_kick(dcp, 0); memset(&r->primary_id, 0, 32); return 0; } diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 28b6d03bbc22f..28274b11f43f3 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -2016,6 +2016,19 @@ static int dcp_desktop_dptx_disconnect_oob(struct apple_dcp *dcp, u32 port) * is reported by nothing else: the DP-to-HDMI converter keeps its DP link * up without a sink and the firmware sees no change. */ +/* The HDMI display gives its pipeline up: report it gone, release its link. */ +void dcp_hdmi_yield_link(struct apple_dcp *dcp) +{ + int tries = 5, ret; + + disconnected_hpd_event(dcp->connector); + /* A link the firmware keeps retrying is released between attempts. */ + while ((ret = dcp_dptx_disconnect(dcp, 0)) && --tries) + msleep(600); + if (ret) + dev_warn(dcp->dev, "HDMI link not released: %d\n", ret); +} + irqreturn_t dcp_desktop_dp2hdmi_hpd(struct apple_dcp *dcp) { struct dptx_port *dptx = &dcp->dptxport[0]; @@ -2026,6 +2039,11 @@ irqreturn_t dcp_desktop_dp2hdmi_hpd(struct apple_dcp *dcp) connected = gpiod_get_value_cansleep(dcp->hdmi_hpd); dev_dbg(dcp->dev, "DP2HDMI HPD irq, connected:%d after 500 ms\n", connected); if (!connected) { + /* + * A yield outlasts HDMI blinks (a monitor reconfiguring its + * inputs drops HPD on all of them): it ends with the Type-C + * display, not with HPD here. + */ if (!READ_ONCE(dcp->active_typec_route) && dcp->connector && READ_ONCE(dcp->connector->connected)) { dev_info(dcp->dev, "HDMI display unplugged\n"); @@ -2035,9 +2053,13 @@ irqreturn_t dcp_desktop_dp2hdmi_hpd(struct apple_dcp *dcp) } return IRQ_HANDLED; } - /* The pipeline drives a Type-C display: the HDMI display waits for it. */ - if (READ_ONCE(dcp->active_typec_route)) { + /* + * The pipeline drives a Type-C display, or is about to: the HDMI + * display waits for it, and gets it back once that one fits one pipe. + */ + if (READ_ONCE(dcp->active_typec_route) || READ_ONCE(dcp->hdmi_yielded)) { dev_info(dcp->dev, "HDMI display waits: its pipeline drives a Type-C display\n"); + dcp_fabric_hybrid_kick(dcp, 1000); return IRQ_HANDLED; } @@ -3473,9 +3495,9 @@ static int dcp_enable_dp2hdmi_hpd(struct apple_dcp *dcp) bool connected = gpiod_get_value_cansleep(dcp->hdmi_hpd); dev_info(dcp->dev, "%s: DP2HDMI HPD connected:%d\n", __func__, connected); - if (connected) + if (connected && !READ_ONCE(dcp->hdmi_yielded)) dcp_dptx_connect(dcp, 0); - else + else if (!connected) _dcp_poweroff(dcp); } @@ -3680,7 +3702,8 @@ void dcp_poweron(struct platform_device *pdev) bool connected = gpiod_get_value_cansleep(dcp->hdmi_hpd); dev_info(dcp->dev, "%s: DP2HDMI HPD connected:%d\n", __func__, connected); - if (connected) + /* A yielded hybrid is about to take a Type-C display. */ + if (connected && !READ_ONCE(dcp->hdmi_yielded)) dcp_dptx_connect(dcp, 0); } @@ -4461,6 +4484,7 @@ VISIBLE_IF_KUNIT void dcp_disable_typec_work(struct apple_dcp *dcp, bool release disable_delayed_work_sync(&dcp->hdmi_settle_wq); disable_delayed_work_sync(&dcp->hdmi_recheck_wq); disable_delayed_work_sync(&dcp->link_recovery_wq); + disable_delayed_work_sync(&dcp->hybrid_wq); dcp_hdmi_disable(dcp); } @@ -4475,6 +4499,7 @@ VISIBLE_IF_KUNIT void dcp_enable_typec_work(struct apple_dcp *dcp) enable_delayed_work(&dcp->hdmi_settle_wq); enable_delayed_work(&dcp->hdmi_recheck_wq); enable_delayed_work(&dcp->link_recovery_wq); + enable_delayed_work(&dcp->hybrid_wq); dcp_hdmi_enable(dcp); scoped_guard(mutex, &dcp->hpd_mutex) dcp->typec_work_enabled = true; diff --git a/drivers/gpu/drm/apple/dcp.h b/drivers/gpu/drm/apple/dcp.h index 9d39c728ccd78..9ad01a427f6a2 100644 --- a/drivers/gpu/drm/apple/dcp.h +++ b/drivers/gpu/drm/apple/dcp.h @@ -145,6 +145,8 @@ bool apple_t8122_25g83_board(void); void dcp_typec_hybrid_first_dt(void); void dcp_fabric_hdmi_unplugged(struct apple_dcp *dcp); +void dcp_fabric_hybrid_kick(struct apple_dcp *dcp, unsigned int delay_ms); +void dcp_hdmi_yield_link(struct apple_dcp *dcp); void dcp_link_recovery_schedule(struct apple_dcp *dcp); /* Desktop 26.6 dual-pipe peer (dcp-peer-v26_6.inc, included from dcp.c) */ diff --git a/drivers/gpu/drm/apple/dcp_external_announce_test.c b/drivers/gpu/drm/apple/dcp_external_announce_test.c index 021293c10ba1d..fcb9a7125b51b 100644 --- a/drivers/gpu/drm/apple/dcp_external_announce_test.c +++ b/drivers/gpu/drm/apple/dcp_external_announce_test.c @@ -112,6 +112,7 @@ static struct announce_test *announce_test_init(struct kunit *test) INIT_DELAYED_WORK(&dcp->hdmi_settle_wq, announce_test_noop); INIT_DELAYED_WORK(&dcp->hdmi_recheck_wq, announce_test_noop); INIT_DELAYED_WORK(&dcp->link_recovery_wq, announce_test_noop); + INIT_DELAYED_WORK(&dcp->hybrid_wq, announce_test_noop); INIT_DELAYED_WORK(&dcp->bl_fallback_wq, announce_test_noop); INIT_DELAYED_WORK(&dcp->backlight.fade_work, announce_test_noop); INIT_WORK(&dcp->external_stop_work, announce_test_noop); diff --git a/drivers/gpu/drm/apple/iomfb_template.c b/drivers/gpu/drm/apple/iomfb_template.c index d6334e88d3ed0..bdb78c999eab2 100644 --- a/drivers/gpu/drm/apple/iomfb_template.c +++ b/drivers/gpu/drm/apple/iomfb_template.c @@ -3010,6 +3010,15 @@ int DCP_FW_NAME(iomfb_modeset)(struct apple_dcp *dcp, /* VRR modes need swap timestamps, see iomfb_flush(). */ dcp->use_timestamps = mode->vrr; + /* A display within one pipe may let the other input have a pipe again. */ + { + bool narrow = crtc_state->mode.hdisplay <= DCP_DESKTOP_PIPE_MAX_WIDTH; + + WRITE_ONCE(dcp->typec_narrow, narrow); + if (narrow) + dcp_fabric_hybrid_kick(dcp, 1000); + } + /* * A mode wider than one pipe goes to both pipes of a reserved pair, * with adaptive sync set the same on both. The pair takes the mode's From becbe407a88b6abb702ed4a9896e65ca6e1de38d Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Tue, 27 Oct 2026 11:41:45 -0400 Subject: [PATCH 370/388] arm64: dts: apple: t8132-j713: Add SMC power, current and voltage sensors The J713 hwmon node lists only four temperatures, so total system power and the input rails that the other Apple laptops report are missing. Add the power, current and voltage keys that this SMC provides, with the labels hwmon-common.dtsi and hwmon-laptop.dtsi use elsewhere: PSTR, PDTR, ID0R, VD0R and SBAV. The J713 has no PMVR; its 3.8 V rail is PMVC (VMVC reads 3.81 V), so that key carries the "3.8 V Rail Power" label. Also add Ib8f, the display supply current. On the J713 it reads about 0.26 A at brightness 225 of 500, 0.09 A with the backlight at 0 and 0.05 A once the panel is powered off, so it measures the panel and backlight load that no other key reports. All keys are readable 4-byte floats on this machine. Node names are in alphabetical order, and no temperature channel moves. Signed-off-by: Erik Bourget --- arch/arm64/boot/dts/apple/t8132-j713.dts | 36 ++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/arch/arm64/boot/dts/apple/t8132-j713.dts b/arch/arm64/boot/dts/apple/t8132-j713.dts index d878b72fd39ba..6fcbd22e9bd2e 100644 --- a/arch/arm64/boot/dts/apple/t8132-j713.dts +++ b/arch/arm64/boot/dts/apple/t8132-j713.dts @@ -325,6 +325,32 @@ hwmon { compatible = "apple,smc-hwmon"; + current-ID0R { + apple,key-id = "ID0R"; + label = "AC Input Current"; + }; + + /* Panel and backlight supply; tracks brightness, idles when blanked. */ + current-Ib8f { + apple,key-id = "Ib8f"; + label = "Display Supply Current"; + }; + + power-PDTR { + apple,key-id = "PDTR"; + label = "AC Input Power"; + }; + + power-PMVC { + apple,key-id = "PMVC"; + label = "3.8 V Rail Power"; + }; + + power-PSTR { + apple,key-id = "PSTR"; + label = "Total System Power"; + }; + temperature-TB0T { apple,key-id = "TB0T"; label = "Battery Hotspot"; @@ -344,6 +370,16 @@ apple,key-id = "TW0P"; label = "WiFi/BT Module Temp"; }; + + voltage-SBAV { + apple,key-id = "SBAV"; + label = "Battery Voltage"; + }; + + voltage-VD0R { + apple,key-id = "VD0R"; + label = "Charger Input Voltage"; + }; }; rtc { From 50d89e75eb1804a9466cb64c4b5c3e21ba176bcb Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Tue, 27 Oct 2026 15:27:31 -0400 Subject: [PATCH 371/388] arm64: dts: apple: t8132: Add the ISP_SYS power domain The ISP_SYS power state register at PMGR offset 0x530 is the only one in the PMGR block without a node. Describe it so boards can manage the domain. Leave it disabled by default, as t8112 does: a board enables it once it has been checked on that hardware. Signed-off-by: Erik Bourget --- arch/arm64/boot/dts/apple/t8132-pmgr.dtsi | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/arch/arm64/boot/dts/apple/t8132-pmgr.dtsi b/arch/arm64/boot/dts/apple/t8132-pmgr.dtsi index 6fbf09e8f9810..5e31d6ce69e15 100644 --- a/arch/arm64/boot/dts/apple/t8132-pmgr.dtsi +++ b/arch/arm64/boot/dts/apple/t8132-pmgr.dtsi @@ -790,6 +790,15 @@ status = "disabled"; /* Locked, don't touch it */ }; + ps_isp_sys: power-controller@530 { + compatible = "apple,t8132-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x530 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "isp_sys"; + status = "disabled"; + }; + ps_ans: power-controller@538 { compatible = "apple,t8132-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; reg = <0x538 4>; From ea08c2aaa246511de6afa3ab5506927b00277caa Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Tue, 27 Oct 2026 15:27:31 -0400 Subject: [PATCH 372/388] arm64: dts: apple: t8132-j713: Power down the unused ISP Nothing on J713 uses the ISP yet, but it is left powered at boot. Enable the ISP_SYS domain in the board policy so that genpd turns it off as an unused domain; a future camera driver will power it up through the normal path. On battery with the display blanked, powering ISP_SYS off lowers the 3.8 V rail by about 65 mW (three off/on pairs: +69, +61, +65 mW). The domain stays off across s2idle and the display is unaffected. The T8132 platform handoff builds its embedded J713 policy from this file and disables any PMGR domain the policy does not list, so the kernel and DTB must come from the same tree for this to take effect. Signed-off-by: Erik Bourget --- arch/arm64/boot/dts/apple/t8132-j713-pmgr.dtsi | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/arch/arm64/boot/dts/apple/t8132-j713-pmgr.dtsi b/arch/arm64/boot/dts/apple/t8132-j713-pmgr.dtsi index 5606ce4d2d3e4..2cad6906ce0ec 100644 --- a/arch/arm64/boot/dts/apple/t8132-j713-pmgr.dtsi +++ b/arch/arm64/boot/dts/apple/t8132-j713-pmgr.dtsi @@ -378,6 +378,11 @@ status = "disabled"; }; +/* No camera driver yet; genpd powers the ISP down as an unused domain. */ +&ps_isp_sys { + status = "okay"; +}; + &ps_ans { status = "okay"; }; From d71c148735aef8d7a2d51f74528037c5a54b780c Mon Sep 17 00:00:00 2001 From: Jonathan Goyvaerts Date: Tue, 27 Oct 2026 17:10:11 -0300 Subject: [PATCH 373/388] arm64: dts: apple: t6040: honour the PMP's memory votes On T6040 only the CLPC (agent 1) and the AP (agent 3) are enabled in the PMC, so the DRAM controller (DCS) and the fabric are raised only by CPU load. GPU work with the CPUs idle runs at a low DCS state. On J616s with the PMP running and the CPUs otherwise idle: CPUs idle one CPU spinning 2x MSAA R11G11B10 1440p pass 1100-1220 285-305 us/pass 16 MiB compute copy, 8 ms gaps 19.4-19.7 29.3-31.2 GB/s 256 MiB compute copies 167-213 215-232 GB/s The core and cluster of the spinning CPU make no difference. CPU DRAM latency, from 2 ms bursts too short for the CLPC, shows the DCS: 301-309 ns with the GPU idle, 423-454 ns under GPU-only load, and 133-192 ns when a spinning core holds the CLPC floor. GPU load alone never raises it. On T8140 the PMP (agent 0) votes the DCS and fabric for the GPU and the other memory clients. The PMP runs from boot on T6040 too, but its agent stayed off because iBoot leaves its floor at the top DCS state. That only matters while the PMP does not run. Enable agent 0 and make the PMC depend on the PMP through apple,pmp, as on T8132. The PMC then probes only after the PMP driver has bound, which happens once the firmware is running, and the device link unbinds the PMC, which puts back the boot enables, before the PMP. If the PMP never binds, the PMC and the CLPC stay unprobed too, so busy CPUs no longer raise the DCS either; T8132 makes the same trade. With this change and the CPUs idle, the same pass takes 300-311 us, the 16 MiB copies reach 97-102 GB/s and the 256 MiB copies 232 GB/s. CPU DRAM latency under the pass drops to 187-194 ns and returns to 286-319 ns within seconds once the GPU idles. Signed-off-by: Jonathan Goyvaerts Assisted-by: LLM --- arch/arm64/boot/dts/apple/t6040.dtsi | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/arch/arm64/boot/dts/apple/t6040.dtsi b/arch/arm64/boot/dts/apple/t6040.dtsi index 213723f3c34b2..7a4a2ff871b32 100644 --- a/arch/arm64/boot/dts/apple/t6040.dtsi +++ b/arch/arm64/boot/dts/apple/t6040.dtsi @@ -1005,9 +1005,13 @@ * only hands to its firmware, so the two do not conflict; "rails" * comes first so that this node's unit address differs from * pmgr_dcp's. - * Static voters are CLPC (1) and AP (3). The PMP's agent (0) stays - * off while the PMP does not run: iBoot leaves its floor at the top - * DCS state. + * Static voters are the PMP (0), CLPC (1) and AP (3). The PMP votes + * the DCS and fabric for memory clients other than the CPUs, the GPU + * among them; without its agent, GPU work runs at a low DCS + * state unless a CPU is busy. iBoot leaves the PMP's floor at the top + * DCS state, so its agent must stay off while the PMP does not run: + * apple,pmp holds off the PMC until the PMP driver has bound, and + * unbinds the PMC first. */ pmc: power-management@5048aa000 { compatible = "apple,t6041-pmc"; @@ -1017,7 +1021,8 @@ nonposted-mmio; #address-cells = <1>; #size-cells = <0>; - apple,static-voters = <1 3>; + apple,pmp = <&pmp>; + apple,static-voters = <0 1 3>; status = "disabled"; clpc: performance-controller@1 { From dc2fae850d0bc6ba7594d88d775c99874c708212 Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Tue, 27 Oct 2026 17:13:42 -0400 Subject: [PATCH 374/388] arm64: dts: apple: t8132: Add the CPU energy model Add opp-microwatt to every OPP of both T8132 (M4) CPU clusters, so the OPP core registers an energy model and the scheduler can use energy-aware placement. Without it, EAS stays off on T8132 even though the cpufreq driver already registers the model from the OPP table. The values were measured on a MacBook Air (J713) with the method used for T8140: one busy core per cluster at each operating point, pinned with scaling_min/max_freq, read from the SMC core-rail power meters. - P cluster: PP0b with one busy core. The rail voltage steps with the P-state (584 mV at 912 MHz) and no other rail rises with the load. Each point is the mean of three runs (within 2%), with the top points measured from a die below 55 C. - E cluster: PPbb idle plus a sixth of the rise with all six cores busy, as PPbb does not register a single busy core. Below 1476 MHz it does not register six either; the 900 and 1080 MHz values use the measured rail voltage and the cluster's dynamic coefficient fitted from 1476-2892 MHz (72 mW per GHz V^2, within 6% at each measured point). - 4416 and 4464 MHz are turbo states, measured with boost enabled. With one busy core, 4044 and 4104 MHz run no faster than 3984 MHz, and 3876 MHz draws more than 3936 MHz. The energy model marks these states inefficient and cpufreq skips them. On J713 with this model, a medium background load (four threads at 30% duty) stays on the E cluster with EAS, instead of reaching the P cluster 3-9% of the time without it, and uses 20-57 mW less system power. Signed-off-by: Erik Bourget --- arch/arm64/boot/dts/apple/t8132.dtsi | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/arch/arm64/boot/dts/apple/t8132.dtsi b/arch/arm64/boot/dts/apple/t8132.dtsi index c82816625efd0..c5b1189d344e5 100644 --- a/arch/arm64/boot/dts/apple/t8132.dtsi +++ b/arch/arm64/boot/dts/apple/t8132.dtsi @@ -221,34 +221,42 @@ opp-900000000 { opp-hz = /bits/ 64 <900000000>; opp-level = <1>; + opp-microwatt = <31100>; }; opp-1080000000 { opp-hz = /bits/ 64 <1080000000>; opp-level = <2>; + opp-microwatt = <37800>; }; opp-1476000000 { opp-hz = /bits/ 64 <1476000000>; opp-level = <3>; + opp-microwatt = <54000>; }; opp-1860000000 { opp-hz = /bits/ 64 <1860000000>; opp-level = <4>; + opp-microwatt = <81500>; }; opp-2172000000 { opp-hz = /bits/ 64 <2172000000>; opp-level = <5>; + opp-microwatt = <110500>; }; opp-2496000000 { opp-hz = /bits/ 64 <2496000000>; opp-level = <6>; + opp-microwatt = <155300>; }; opp-2808000000 { opp-hz = /bits/ 64 <2808000000>; opp-level = <7>; + opp-microwatt = <192800>; }; opp-2892000000 { opp-hz = /bits/ 64 <2892000000>; opp-level = <8>; + opp-microwatt = <193800>; }; }; @@ -259,83 +267,103 @@ opp-912000000 { opp-hz = /bits/ 64 <912000000>; opp-level = <1>; + opp-microwatt = <289000>; }; opp-1260000000 { opp-hz = /bits/ 64 <1260000000>; opp-level = <2>; + opp-microwatt = <396000>; }; opp-1512000000 { opp-hz = /bits/ 64 <1512000000>; opp-level = <3>; + opp-microwatt = <510000>; }; opp-1800000000 { opp-hz = /bits/ 64 <1800000000>; opp-level = <4>; + opp-microwatt = <632000>; }; opp-2088000000 { opp-hz = /bits/ 64 <2088000000>; opp-level = <5>; + opp-microwatt = <818000>; }; opp-2352000000 { opp-hz = /bits/ 64 <2352000000>; opp-level = <6>; + opp-microwatt = <1014000>; }; opp-2616000000 { opp-hz = /bits/ 64 <2616000000>; opp-level = <7>; + opp-microwatt = <1188000>; }; opp-2868000000 { opp-hz = /bits/ 64 <2868000000>; opp-level = <8>; + opp-microwatt = <1456000>; }; opp-3096000000 { opp-hz = /bits/ 64 <3096000000>; opp-level = <9>; + opp-microwatt = <1733000>; }; opp-3300000000 { opp-hz = /bits/ 64 <3300000000>; opp-level = <10>; + opp-microwatt = <2031000>; }; opp-3468000000 { opp-hz = /bits/ 64 <3468000000>; opp-level = <11>; + opp-microwatt = <2337000>; }; opp-3624000000 { opp-hz = /bits/ 64 <3624000000>; opp-level = <12>; + opp-microwatt = <2485000>; }; opp-3756000000 { opp-hz = /bits/ 64 <3756000000>; opp-level = <13>; + opp-microwatt = <2763000>; }; opp-3876000000 { opp-hz = /bits/ 64 <3876000000>; opp-level = <14>; + opp-microwatt = <3192000>; }; opp-3936000000 { opp-hz = /bits/ 64 <3936000000>; opp-level = <15>; + opp-microwatt = <3077000>; }; opp-3984000000 { opp-hz = /bits/ 64 <3984000000>; opp-level = <16>; + opp-microwatt = <3067000>; }; opp-4044000000 { opp-hz = /bits/ 64 <4044000000>; opp-level = <17>; + opp-microwatt = <3062000>; }; opp-4104000000 { opp-hz = /bits/ 64 <4104000000>; opp-level = <18>; + opp-microwatt = <3062000>; }; opp-4416000000 { opp-hz = /bits/ 64 <4416000000>; opp-level = <19>; + opp-microwatt = <5389000>; turbo-mode; }; opp-4464000000 { opp-hz = /bits/ 64 <4464000000>; opp-level = <20>; + opp-microwatt = <5006000>; turbo-mode; }; }; From 3c92e5329dd15b86c96429218a1fa0b252d85194 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Wed, 28 Oct 2026 08:45:53 +1100 Subject: [PATCH 375/388] drm/apple: clear the desktop 26.6 peer allocation as one field group dcp_peer_hints_26_6() clears the 32-byte allocation block of the display hints (primary and peer endpoint IDs, extra pipe count, flags and the reserved bytes) with memset(&r->primary_id, 0, 32). FORTIFY_SOURCE sees a write of 32 bytes into the 4-byte primary_id field and fails the build with -Werror (__write_overflow_field), so dcp.o does not compile with the Fedora-based release configuration. Group the allocation fields with struct_group_attr(), keeping the packed layout, and clear the group by its size. The offsets are unchanged and a new static_assert pins the group at 32 bytes. Fixes: e3e7131681e2 ("drm/apple: drive a display wider than one pipe with two desktop 26.6 pipes") Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/dcp-peer-v26_6.h | 11 ++++++++--- drivers/gpu/drm/apple/dcp-peer-v26_6.inc | 6 +++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp-peer-v26_6.h b/drivers/gpu/drm/apple/dcp-peer-v26_6.h index df83045cfa73e..3b88d5e040303 100644 --- a/drivers/gpu/drm/apple/dcp-peer-v26_6.h +++ b/drivers/gpu/drm/apple/dcp-peer-v26_6.h @@ -4,6 +4,7 @@ #include #include +#include #include struct dcp_peer_tiling { @@ -78,9 +79,12 @@ static inline bool dcp_peer_tiling_set(struct dcp_peer_tiling *own, struct dcp_display_hints { u8 hints[0x6c]; u8 padding[4]; - __le32 primary_id, peer_id; - u8 extra_pipes, flags; - u8 allocation_reserved[22]; + /* The allocation the AP fills in, or clears when it allocates no peer. */ + struct_group_attr(allocation, __packed, + __le32 primary_id, peer_id; + u8 extra_pipes, flags; + u8 allocation_reserved[22]; + ); __le32 status; u8 tail[12]; } __packed; @@ -88,6 +92,7 @@ struct dcp_display_hints { static_assert(sizeof(struct dcp_display_hints) == 160); static_assert(offsetof(struct dcp_display_hints, primary_id) == 0x70); static_assert(offsetof(struct dcp_display_hints, status) == 0x90); +static_assert(sizeof_field(struct dcp_display_hints, allocation) == 32); /* The widest mode one desktop 26.6 DISPEXT pipe drives (firmware: "max width: 6720"). */ #define DCP_DESKTOP_PIPE_MAX_WIDTH 6720 diff --git a/drivers/gpu/drm/apple/dcp-peer-v26_6.inc b/drivers/gpu/drm/apple/dcp-peer-v26_6.inc index b8038db010943..29c4b6e697ebc 100644 --- a/drivers/gpu/drm/apple/dcp-peer-v26_6.inc +++ b/drivers/gpu/drm/apple/dcp-peer-v26_6.inc @@ -259,7 +259,7 @@ int dcp_peer_hints_26_6(struct apple_dcp *dcp, struct dptx_port *port, width = get_unaligned_le32(r->hints + 0x38); if (!(r->hints[0] & 1) || !width || width <= DCP_DESKTOP_PIPE_MAX_WIDTH) { - memset(&r->primary_id, 0, 32); + memset(&r->allocation, 0, sizeof(r->allocation)); return 0; } @@ -277,7 +277,7 @@ int dcp_peer_hints_26_6(struct apple_dcp *dcp, struct dptx_port *port, (dcp_peer_reject_26_6(dcp, dcp->peer_reserved) & ~BIT(1))) dcp_peer_drop_locked(dcp, dcp->peer_reserved); if (dcp->peer_owner) { - memset(&r->primary_id, 0, 32); + memset(&r->allocation, 0, sizeof(r->allocation)); return 0; } peer = dcp->peer_reserved; @@ -301,7 +301,7 @@ int dcp_peer_hints_26_6(struct apple_dcp *dcp, struct dptx_port *port, dev_info(dcp->dev, "no idle peer pipe for a %u px display\n", width); WRITE_ONCE(dcp->peer_starved, true); dcp_fabric_hybrid_kick(dcp, 0); - memset(&r->primary_id, 0, 32); + memset(&r->allocation, 0, sizeof(r->allocation)); return 0; } WRITE_ONCE(dcp->peer_starved, false); From d50f12268b0d68a03ddb8a3da464e52a246ac83e Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Wed, 28 Oct 2026 08:24:31 +1100 Subject: [PATCH 376/388] selftests: apple-dcp: follow adaptive sync on desktop 26.6 Desktop 26.6 now takes adaptive sync with a mode set. 562eb4c4087d removed dcp_mode_vrr_supported(), which the publication fixture extracted unconditionally. 6477da65cee5 made dcp_crtc_atomic_check() turn a VRR_ENABLED change on that firmware into a mode set, through drm_atomic_get_old_crtc_state() and crtc_state->mode_changed, which the VRR admission fixture did not model. So publication-hpd failed to build its unit and vrr-rejection did not compile. Extract the VRR filter only where the source still has it. Give the VRR fixture the desktop firmware value, mode_changed and an old-state lookup that counts its calls. The existing 72 admission cases run unchanged and now also check that no other firmware looks at the old state or forces a mode set. Where the source has the desktop path, 12 more cases pin it: an enabled CRTC whose VRR_ENABLED differs from its old state gets mode_changed, and nothing else does (a disabled CRTC, an unchanged VRR_ENABLED, no old state). Fixes: 562eb4c4087d ("drm/apple: enable adaptive sync on desktop 26.6 external displays") Fixes: 6477da65cee5 ("drm/apple: enable adaptive sync on the desktop 26.6 internal panel") Assisted-by: LLM Signed-off-by: Ryan Murray --- .../drivers/gpu/apple-dcp/lifecycle.py | 47 +++++++++++++++++-- .../selftests/drivers/gpu/apple-dcp/rate.py | 6 ++- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py index bc2e42a4a799f..ede39666c8413 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py @@ -1672,7 +1672,8 @@ def vrr(root): struct apple_connector { bool connected; }; /* The native T6030 14.7 backend is never selected in these scenarios. */ enum dcp_firmware_version { DCP_FIRMWARE_UNKNOWN, DCP_FIRMWARE_V_12_3, DCP_FIRMWARE_V_13_5, - DCP_FIRMWARE_V_14_7, DCP_FIRMWARE_V_14_7_TEMPLATE, DCP_FIRMWARE_V_26_6 }; + DCP_FIRMWARE_V_14_7, DCP_FIRMWARE_V_14_7_TEMPLATE, DCP_FIRMWARE_V_26_6, + DCP_FIRMWARE_V_26_6_DESKTOP }; struct apple_dcp { bool crashed, external, external_native, quiescing; unsigned int nr_typec_routes; @@ -1685,14 +1686,19 @@ def vrr(root): struct platform_device { struct apple_dcp *data; }; struct drm_crtc { struct platform_device *dcp; void *primary; }; struct drm_display_mode { int unused; }; -struct drm_crtc_state { bool enable, vrr_enabled, modeset, color_mgmt_changed; struct drm_display_mode mode; void *ctm, *degamma_lut, *gamma_lut; unsigned plane_mask; }; +struct drm_crtc_state { bool enable, vrr_enabled, modeset, mode_changed, color_mgmt_changed; struct drm_display_mode mode; void *ctm, *degamma_lut, *gamma_lut; unsigned plane_mask; }; /* mainline 7.2 renamed struct drm_atomic_state to struct drm_atomic_commit */ -struct drm_atomic_commit { struct drm_crtc_state crtc; }; +struct drm_atomic_commit { struct drm_crtc_state crtc, old_crtc; bool no_old_crtc; }; #define to_apple_crtc(x) (x) #define platform_get_drvdata(x) ((x)->data) static struct drm_crtc_state *drm_atomic_get_new_crtc_state( struct drm_atomic_commit *s, struct drm_crtc *c) { (void)c; return &s->crtc; } +/* Only the desktop 26.6 firmware compares against the old CRTC state. */ +static unsigned int old_state_lookups; +static struct drm_crtc_state * __attribute__((unused)) drm_atomic_get_old_crtc_state( + struct drm_atomic_commit *s, struct drm_crtc *c) +{ (void)c; old_state_lookups++; return s->no_old_crtc ? NULL : &s->old_crtc; } static bool drm_atomic_crtc_needs_modeset(struct drm_crtc_state *s) { return s->modeset; } static int __attribute__((unused)) iomfb_v14_7_atomic_check(struct apple_dcp *d, @@ -1750,14 +1756,45 @@ def vrr(root): } cases++; } - dcp.crashed = true; + /* No other firmware turns a VRR change into a mode set. */ + assert(!old_state_lookups && !state.crtc.mode_changed); +DESKTOP_VRR dcp.crashed = true; state.crtc.vrr_enabled = false; assert(dcp_crtc_atomic_check(&crtc, &state) == -EINVAL); printf("PASS: %u production atomic VRR admission cases and crashed-device rejection\n", cases); return 0; } """ - return prefix + check + suffix + desktop = '' + if 'DCP_FIRMWARE_V_26_6_DESKTOP' in check: + # Desktop 26.6 applies adaptive sync with a mode set: a VRR_ENABLED + # change of an enabled CRTC must become one, nothing else may. + desktop = r""" dcp.fw_compat = DCP_FIRMWARE_V_26_6_DESKTOP; + dcp.hw.iomfb_method_profile = 0; + dcp.mode_state.valid = true; + dcp.connector = &connector; + connector.connected = true; + state.crtc.modeset = false; + for (unsigned int enable = 0; enable < 2; enable++) + for (unsigned int old_vrr = 0; old_vrr < 3; old_vrr++) + for (unsigned int vrr = 0; vrr < 2; vrr++) { + state.crtc.enable = enable; + state.crtc.vrr_enabled = vrr; + state.crtc.mode_changed = false; + state.no_old_crtc = old_vrr == 2; + state.old_crtc.vrr_enabled = old_vrr == 1; + assert(dcp_crtc_atomic_check(&crtc, &state) == 0); + bool expected = enable && old_vrr != 2 && (old_vrr == 1) != vrr; + if (state.crtc.mode_changed != expected) { + fprintf(stderr, "desktop enable=%u old_vrr=%u vrr=%u: mode_changed=%d expected %d\n", + enable, old_vrr, vrr, state.crtc.mode_changed, expected); + return 1; + } + cases++; + } + dcp.fw_compat = DCP_FIRMWARE_UNKNOWN; +""" + return prefix + check + suffix.replace('DESKTOP_VRR', desktop, 1) def power_reply(root): diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py b/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py index 7bd087c364c52..72b281ab2d710 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py @@ -677,7 +677,7 @@ def publication(root): #define DCP_FIRMWARE_V_14_7 (DCP_FIRMWARE_H17P + 1) #define DCP_FIRMWARE_V_14_7_TEMPLATE (DCP_FIRMWARE_H17P + 2) #endif -/* Nor the desktop 26.6 firmware, the one that withholds VRR modes. */ +/* Nor the desktop 26.6 firmware. */ #define DCP_FIRMWARE_V_26_6_DESKTOP (DCP_FIRMWARE_H17P + 3) '''+firmware_lineage(root)+internal_inline(root,'dcp_fw_is_14_7')+r'''/* Off T6030 machines the direct crossbar output is not clocked separately. */ static int __attribute__((unused)) dcp_direct_crossbar_link(struct apple_dcp *d,bool up) { (void)d;(void)up;return 0; } @@ -700,7 +700,9 @@ def publication(root): + ''.join(apple_function(root, name) for name in ('dcp_queue_hotplug', 'dcp_modes_for_connector', 'apple_connector_invalidate_edid', 'apple_connector_edid_set_live')) + mode_helpers) - publication = block(iomfb,'static void dcp_clear_connector_edid(')+'\n'+block(iomfb,'static void dcp_consume_edid_invalidation(')+mode_helpers+block(iomfb,'static bool dcp_mode_vrr_supported(')+'\n'+block(iomfb,'int dcp_get_modes(')+block(iomfb,re.search(r'^[^\n;(]*\blookup_mode\(',iomfb,re.M).start()) + # The desktop 26.6 VRR filter is gone once that firmware takes adaptive sync. + vrr_filter = block(iomfb,'static bool dcp_mode_vrr_supported(')+'\n' if 'static bool dcp_mode_vrr_supported(' in iomfb else '' + publication = block(iomfb,'static void dcp_clear_connector_edid(')+'\n'+block(iomfb,'static void dcp_consume_edid_invalidation(')+mode_helpers+vrr_filter+block(iomfb,'int dcp_get_modes(')+block(iomfb,re.search(r'^[^\n;(]*\blookup_mode\(',iomfb,re.M).start()) # Drive malformed replacements through the same chunk/end ingress as firmware. # The dictionary cache is a boundary substitute and takes ownership of the blob. receipt = r''' From e14d7ec5ec1572b83ec13ebb335fc340449cdd5c Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Wed, 28 Oct 2026 08:24:31 +1100 Subject: [PATCH 377/388] selftests: apple-dcp: follow the hybrid hand-over in the route fixtures The actual-DCP route fixtures compile the real dcp_typec_route_set(), dcp_typec_route_deactivate() and dcp_typec_route_available(). The J616s series added hybrid hand-over state to them. Deactivation now clears peer_starved and typec_narrow, releases a desktop 26.6 peer and restores the fixed output through its own fixed_xbar. A port leaving DP clears typec_yielded and dcp_hybrid_fails and hands every yielded hybrid on the port's routes back to HDMI. Availability asks dcp_typec_hybrid_first(). The mock struct apple_dcp and port had none of this, so seven units no longer compiled. Add the fields and a counter. Extract the real dcp_typec_hybrid_first() over the production defaults (module parameter -1, no DT opt-in), so the policy is as before. Stub the peer release to assert, since no scenario is on desktop 26.6. Let the single-port list walk yield the port's one route when it walks routes. Probe now records the fixed output's crossbar in fixed_xbar as well as xbar (7b990950a349), and deactivation restores the fixed route through fixed_xbar. The mux-release-stages fixed-restore case set only xbar, so set both as probe does. Its assertions are unchanged: the restore is retried once after its failure and the deselect stays balanced. Fixes: 7b990950a349 ("drm/apple: fabric: keep the hybrid pipeline's HDMI crossbar apart from its Type-C route's") Fixes: 0d43a7f471ac ("drm/apple: fabric: hand the hybrid between an HDMI and a two-pipe Type-C display") Assisted-by: LLM Signed-off-by: Ryan Murray --- .../drivers/gpu/apple-dcp/accounting.py | 2 +- .../drivers/gpu/apple-dcp/transaction.py | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/accounting.py b/tools/testing/selftests/drivers/gpu/apple-dcp/accounting.py index ce2791cc3a379..9c74ec637be24 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/accounting.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/accounting.py @@ -103,7 +103,7 @@ def resources(ROOT): puts("PASS successful DCP finalizer is not repeated after a later provider failure");return 0; } if(argv[1][0]=='h')hw_failures=1; - if(argv[1][0]=='f') {display.fixed_connector_type=2;display.hdmi_hpd=(void *)1;gpio_level=1;display.xbar=&fixed_xbar;fixed_restore_failures=1;} + if(argv[1][0]=='f') {display.fixed_connector_type=2;display.hdmi_hpd=(void *)1;gpio_level=1;display.xbar=display.fixed_xbar=&fixed_xbar;fixed_restore_failures=1;} assert(typec_mux_set(&mux,&safe)==-EIO&&xbar.lock==1&&port.owner==&route&&port.safe_pending&&!port.safe_finish_ready); bool selected_after_error=route.selected; int ret=typec_mux_set(&mux,&safe); diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/transaction.py b/tools/testing/selftests/drivers/gpu/apple-dcp/transaction.py index 5dea64a19054e..6c40adf5d3a85 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/transaction.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/transaction.py @@ -40,13 +40,21 @@ def check(name, unit, cases=((),), negative=False, flags=()): bool external_power_attempted,dptx_tunnel,tb_clock_ok,fixed_route_selected; int tb_lock; unsigned typec_generation,dptx_dfp_port,dptx_phy,fixed_dptx_phy,fixed_mux_index; void *tb_dpin_set_active,*tb_dpin_ctx,*phy,*fixed_phy; - struct mux_control *xbar; + struct mux_control *xbar,*fixed_xbar; + /* desktop 26.x hybrid hand-over and dual-pipe state; fw_compat is never desktop here */ + bool peer_starved,typec_narrow,hdmi_yielded; struct apple_dcp_typec_route *active_typec_route,*typec_routes; struct connector *connector,*fixed_connector;int connector_type,fixed_connector_type; struct mux_control *direct_xbar_up; bool external_native, ext_dead; int fw_compat; unsigned long long tb_generation; int modes_lock; unsigned long long modes_generation; bool modes_admitted,modes_provisional; struct { int iomfb_method_profile; } hw; ''',1) + # The hybrid hand-over walks a port's routes; the only route is this port's. + actual=actual.replace('struct apple_dcp_typec_port { int link;', 'struct apple_dcp_typec_port { int link,routes; bool typec_yielded;',1) + actual=actual.replace('#define list_for_each_entry(p,h,m) for((void)(h),(p)=sole_port();(p);(p)=NULL)', + '#define list_for_each_entry(p,h,m) for((void)(h),(p)=_Generic((p),struct apple_dcp_typec_route *:sole_route(),default:sole_port());(p);(p)=NULL)',1) + actual=actual.replace('static struct apple_dcp_typec_port *sole_port(void) { return &port; }', + 'static struct apple_dcp_typec_port *sole_port(void) { return &port; }\nstatic struct apple_dcp_typec_route * __attribute__((unused)) sole_route(void) { return &route; }',1) actual=actual.replace('bool applied_valid,safe_pending,dp_wanted,dp_hpd,hpd;', 'struct typec_altmode *applied_alt; unsigned long applied_mode; unsigned applied_status,applied_conf; bool applied_valid,safe_pending,dp_wanted,dp_hpd,hpd;',1) actual=actual.replace('void *xbar; bool deactivate_xbar_done; int deactivate_error; bool selected,tunnel;', 'struct mux_control *xbar,*active_xbar; void *phy; unsigned dptx_phy,tunnel_dpin,mux_index;unsigned long long tunnel_generation;bool deactivate_xbar_done; int deactivate_error;bool xbar_up,selected,tunnel;',1) extra=r''' @@ -70,6 +78,10 @@ def check(name, unit, cases=((),), negative=False, flags=()): static int mux_control_deselect(struct mux_control *m) { (void)m;event('C');xbar_calls++;return xbar_error; } static int mux_control_select(struct mux_control *m,unsigned n) { (void)m;(void)n;return 0; } static void dpavservep_detach(struct apple_dcp *d) { (void)d; } + /* Desktop 26.6 dual-pipe peers: no scenario here is on that firmware. */ + static void __attribute__((unused)) dcp_peer_release_26_6(struct apple_dcp *d) { (void)d;assert(!"desktop 26.6 peer release"); } + /* failed takes of the hybrid, reset when a Type-C display leaves DP */ + static unsigned int __attribute__((unused)) dcp_hybrid_fails; static void dcp_firmware_metadata_connection(struct apple_dcp *d,unsigned n,bool c) { (void)d;(void)n;(void)c; } static bool dcp_typec_keep_order(void) { return false; } static struct apple_dcp_typec_route * __attribute__((unused)) dcp_typec_lowest_free(struct apple_dcp_typec_port *p) { (void)p;return &route; } @@ -115,6 +127,11 @@ def check(name, unit, cases=((),), negative=False, flags=()): '''+available # Route-follow: the fixed output, or a failed T6030 external processor, keeps the route busy. available+=apple_function(ROOT,'dcp_typec_route_busy') + # Hybrid-first policy: the real helper over the module parameter's and the + # device tree's defaults (-1: from the DT; no apple,typec-hybrid-first here). + hybrid_first=apple_function(ROOT,'dcp_typec_hybrid_first') + if hybrid_first: + available='static int typec_hybrid_first=-1;\n static bool typec_hybrid_first_dt;\n '+hybrid_first+available if available: actual=actual.replace('struct apple_dcp { int external_lock,external_error;','struct apple_dcp { int external_lock,external_error;\n struct dcp_fabric_presence hdmi_presence; int hdmi_presence_lock; unsigned nr_typec_routes;',1) available='''#define atomic_read(p) (*(p)) From 8f687be747eb1dd1a5ae28032c49042b5d65dc8c Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Tue, 27 Oct 2026 19:42:49 -0400 Subject: [PATCH 378/388] bus: simple-pm-bus: Add apple,t8132-pmgr The T8132 PMGR nodes are "apple,t8132-pmgr", "apple,t8103-pmgr", "syscon", "simple-mfd". Their best match here is "apple,t8103-pmgr" (ONLY_BUS), which is not their first compatible, so probe returns -ENODEV and both nodes stay unbound. The power domains in the mini PMGR take parents from the main PMGR, and fw_devlink keeps waiting for the unbound mini PMGR node as a consumer of those parents. On J713 this holds back the ATC common domains: sync_state() pending due to 388280000.power-management genpd keeps a domain on until its provider's sync_state(), so these never power down. Bind the T8132 PMGRs as plain buses, like the other Apple PMGRs. With sync_state() reached, genpd may switch off any unused domain the board does not keep on. On J713 the two ATC common domains were the only ones pending; the other T8132 boards were not tested. Signed-off-by: Erik Bourget --- drivers/bus/simple-pm-bus.c | 1 + 1 file changed, 1 insertion(+) diff --git a/drivers/bus/simple-pm-bus.c b/drivers/bus/simple-pm-bus.c index 6d351ca51aa4e..9a8b654fe021f 100644 --- a/drivers/bus/simple-pm-bus.c +++ b/drivers/bus/simple-pm-bus.c @@ -149,6 +149,7 @@ static const struct of_device_id simple_pm_bus_of_match[] = { { .compatible = "apple,t8015-pmgr", .data = ONLY_BUS }, { .compatible = "apple,t8103-pmgr", .data = ONLY_BUS }, { .compatible = "apple,t8112-pmgr", .data = ONLY_BUS }, + { .compatible = "apple,t8132-pmgr", .data = ONLY_BUS }, { .compatible = "apple,t6000-pmgr", .data = ONLY_BUS }, { .compatible = "apple,t6020-pmgr", .data = ONLY_BUS },\ { .compatible = "fsl,ls1021a-scfg", }, From 1c79b032094946a2b3a9207d0e8d0cebf9bc7837 Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Tue, 27 Oct 2026 20:04:49 -0400 Subject: [PATCH 379/388] phy: apple: atc: Let an idle T8132 PHY runtime-suspend A J713 (MacBook Air M4) Type-C port keeps its ATC power domains on with nothing plugged in. For the PHY's domain to drop, the PHY must runtime- suspend, and its domain has to be the controller's ATCn_USB: that domain also holds the PIPE handler, which the PHY writes to drive the USB controller's reset and to route the PIPE, also while the Type-C mux changes mode before the controller has resumed. A running controller keeps the shared domain on by itself. On T8132, once the PHY follows its PM domain with the genpd notifier, enable runtime PM before the PHYs are created, so that they follow it too. Power the device for each operation that can reach the hardware: the USB2 and USB3 PHY ops, the controller's reset, the Type-C mux, and the deferred SuperSpeed retry. The orientation switch only records the orientation, and T8132 creates no DisplayPort PHY. Shutdown touches the hardware only for an owned common epoch, which holds the device on. Hold a reference while a power-down would lose state: a mode or a host, a PIPE away from its parked dummy backend or between backends, and the common-power epoch of a USB3 admission, which only its own stop may end. The rest is the baseline the restore below sets up. With nothing held, the PHY autosuspends after 2 s, which spans the burst of mux, orientation and reset calls of one plug. Unbind drops a hold still taken. The domain then switches off with the controller's and resets the block, so also restore the PHY state after that on T8132, as on T8122. The T8132 baseline is the one its probe sets up: the common block is acquired only on USB3 admission, so it is not powered down, and the PIPE is set up as at probe, with the dummy PIPE clock the USB2 controller needs. System resume leaves a runtime-suspended PHY to its next operation, as its domain may switch off again once resume completes. Signed-off-by: Erik Bourget --- drivers/phy/apple/atc-test.c | 158 +++++++++++++++++++++ drivers/phy/apple/atc.c | 264 ++++++++++++++++++++++++++++++++--- 2 files changed, 400 insertions(+), 22 deletions(-) diff --git a/drivers/phy/apple/atc-test.c b/drivers/phy/apple/atc-test.c index c832f2b734f01..366b4b86f115e 100644 --- a/drivers/phy/apple/atc-test.c +++ b/drivers/phy/apple/atc-test.c @@ -1008,6 +1008,149 @@ static void atcphy_test_usb2_init_after_safe(struct kunit *test) KUNIT_EXPECT_EQ(test, atcphy->mode, APPLE_ATCPHY_MODE_OFF); } +/* An idle T8132 PHY may let its domain drop; anything it still runs holds it. */ +static void atcphy_test_t8132_rpm_busy(struct kunit *test) +{ + struct apple_atcphy *atcphy = atcphy_test_alloc(test, false); + + atcphy->hw = &atcphy_hw_t8132; + atcphy->mode = APPLE_ATCPHY_MODE_OFF; + atcphy->pipe_state = ATCPHY_PIPEHANDLER_STATE_DUMMY; + KUNIT_EXPECT_FALSE(test, atcphy_rpm_busy(atcphy)); + + atcphy->mode = APPLE_ATCPHY_MODE_USB2; + KUNIT_EXPECT_TRUE(test, atcphy_rpm_busy(atcphy)); + atcphy->mode = APPLE_ATCPHY_MODE_OFF; + + atcphy->host_active = true; + KUNIT_EXPECT_TRUE(test, atcphy_rpm_busy(atcphy)); + atcphy->host_active = false; + + atcphy->pipe_pending = true; + KUNIT_EXPECT_TRUE(test, atcphy_rpm_busy(atcphy)); + atcphy->pipe_pending = false; + + atcphy->pipe_state = ATCPHY_PIPEHANDLER_STATE_USB3; + KUNIT_EXPECT_TRUE(test, atcphy_rpm_busy(atcphy)); + atcphy->pipe_state = ATCPHY_PIPEHANDLER_STATE_DUMMY; + + /* A failed USB3 admission keeps its common-power epoch */ + atcphy->t8132_common_owned = true; + KUNIT_EXPECT_TRUE(test, atcphy_rpm_busy(atcphy)); +} + +/* The hold follows the busy state, with one reference at most. */ +static void atcphy_test_t8132_rpm_hold(struct kunit *test) +{ + struct apple_atcphy *atcphy = atcphy_test_alloc_powered_off(test); + + atcphy->hw = &atcphy_hw_t8132; + atcphy->idle_rpm = true; + atcphy->mode = APPLE_ATCPHY_MODE_USB2; + atcphy_rpm_sync(atcphy); + KUNIT_EXPECT_TRUE(test, atcphy->rpm_held); + KUNIT_EXPECT_EQ(test, atomic_read(&atcphy->dev->power.usage_count), 1); + atcphy_rpm_sync(atcphy); + KUNIT_EXPECT_EQ(test, atomic_read(&atcphy->dev->power.usage_count), 1); + + atcphy->mode = APPLE_ATCPHY_MODE_OFF; + atcphy_rpm_sync(atcphy); + KUNIT_EXPECT_FALSE(test, atcphy->rpm_held); + KUNIT_EXPECT_EQ(test, atomic_read(&atcphy->dev->power.usage_count), 0); + + /* Unbind drops a hold that is still taken */ + atcphy->mode = APPLE_ATCPHY_MODE_USB2; + atcphy_rpm_sync(atcphy); + atcphy_rpm_release(atcphy); + KUNIT_EXPECT_FALSE(test, atcphy->rpm_held); + KUNIT_EXPECT_EQ(test, atomic_read(&atcphy->dev->power.usage_count), 0); +} + +/* Record the mode a restore asks for, without touching MMIO. */ +static int atcphy_test_configure_record(struct apple_atcphy *atcphy, enum atcphy_mode mode) +{ + struct kunit *test = kunit_get_current_test(); + struct atcphy_test_configure *config = test->priv; + + lockdep_assert_held(&atcphy->lock); + config->calls++; + if (mode != APPLE_ATCPHY_MODE_OFF) + config->lane_requests++; + atcphy->mode = mode; + return 0; +} + +/* A T8132 PHY whose domain was off: zeroed memory stands in for the reset block. */ +static struct apple_atcphy *atcphy_test_alloc_t8132_pd_off(struct kunit *test, + enum atcphy_mode mode) +{ + struct apple_atcphy *atcphy = atcphy_test_alloc_powered_off(test); + void *core = kunit_kzalloc(test, ATCPHY_MISC + 4, GFP_KERNEL); + void *pipehandler = kunit_kzalloc(test, PIPEHANDLER_T8132_USB2_CONFIG + 4, GFP_KERNEL); + void *usb2phy = kunit_kzalloc(test, USB2PHY_MISCTUNE + 4, GFP_KERNEL); + + KUNIT_ASSERT_NOT_NULL(test, core); + KUNIT_ASSERT_NOT_NULL(test, pipehandler); + KUNIT_ASSERT_NOT_NULL(test, usb2phy); + atcphy->regs.core = (__force void __iomem *)core; + atcphy->regs.pipehandler = (__force void __iomem *)pipehandler; + atcphy->regs.usb2phy = (__force void __iomem *)usb2phy; + atcphy->hw = &atcphy_hw_t8132; + atcphy->mode = mode; + atcphy->pd_was_off = true; + + return atcphy; +} + +/* The probe baseline comes back: controller in reset, USB2 off, dummy PIPE clock on. */ +static void atcphy_test_t8132_restore_idle(struct kunit *test) +{ + struct apple_atcphy *atcphy = atcphy_test_alloc_t8132_pd_off(test, APPLE_ATCPHY_MODE_OFF); + struct atcphy_test_configure config = {}; + u32 aon; + + test->priv = &config; + kunit_activate_static_stub(test, atcphy_configure, atcphy_test_configure_record); + scoped_guard(mutex, &atcphy->lock) + atcphy_restore_after_pd_off(atcphy); + + KUNIT_EXPECT_FALSE(test, atcphy->pd_was_off); + KUNIT_EXPECT_EQ(test, config.calls, 0U); + KUNIT_EXPECT_EQ(test, atcphy->mode, APPLE_ATCPHY_MODE_OFF); + KUNIT_EXPECT_EQ(test, atcphy->pipe_state, ATCPHY_PIPEHANDLER_STATE_DUMMY); + aon = readl(atcphy->regs.pipehandler + PIPEHANDLER_AON_GEN); + KUNIT_EXPECT_FALSE(test, aon & PIPEHANDLER_AON_GEN_DWC3_RESET_N); + KUNIT_EXPECT_TRUE(test, aon & PIPEHANDLER_AON_GEN_DWC3_FORCE_CLAMP_EN); + KUNIT_EXPECT_TRUE(test, readl(atcphy->regs.pipehandler + PIPEHANDLER_T8132_USB2_CONFIG) & + PIPEHANDLER_T8132_USB2_CONFIG_EN); + KUNIT_EXPECT_TRUE(test, readl(atcphy->regs.usb2phy + USB2PHY_CTL) & USB2PHY_CTL_SIDDQ); + /* As at probe, the common block is left for a USB3 admission to acquire. */ + KUNIT_EXPECT_EQ(test, readl(atcphy->regs.core + ATCPHY_POWER_CTRL), 0U); + + /* Nothing to do once the state is back. */ + scoped_guard(mutex, &atcphy->lock) + atcphy_restore_after_pd_off(atcphy); + KUNIT_EXPECT_EQ(test, config.calls, 0U); +} + +/* After the baseline, the mode the Type-C mux selected is requested again. */ +static void atcphy_test_t8132_restore_mode(struct kunit *test) +{ + struct apple_atcphy *atcphy = atcphy_test_alloc_t8132_pd_off(test, APPLE_ATCPHY_MODE_USB2); + struct atcphy_test_configure config = {}; + + test->priv = &config; + kunit_activate_static_stub(test, atcphy_configure, atcphy_test_configure_record); + scoped_guard(mutex, &atcphy->lock) + atcphy_restore_after_pd_off(atcphy); + + KUNIT_EXPECT_EQ(test, config.calls, 1U); + KUNIT_EXPECT_EQ(test, config.lane_requests, 1U); + KUNIT_EXPECT_EQ(test, atcphy->mode, APPLE_ATCPHY_MODE_USB2); + KUNIT_EXPECT_FALSE(test, readl(atcphy->regs.pipehandler + PIPEHANDLER_AON_GEN) & + PIPEHANDLER_AON_GEN_DWC3_RESET_N); +} + static void atcphy_test_t8103_usb3_machine_scope(struct kunit *test) { static const struct { @@ -1084,3 +1227,18 @@ static struct kunit_suite atcphy_mux_test_suite = { }; kunit_test_suite(atcphy_mux_test_suite); + +static struct kunit_case atcphy_t8132_pm_test_cases[] = { + KUNIT_CASE(atcphy_test_t8132_rpm_busy), + KUNIT_CASE(atcphy_test_t8132_rpm_hold), + KUNIT_CASE(atcphy_test_t8132_restore_idle), + KUNIT_CASE(atcphy_test_t8132_restore_mode), + {} +}; + +static struct kunit_suite atcphy_t8132_pm_test_suite = { + .name = "phy-apple-atc-t8132-pm", + .test_cases = atcphy_t8132_pm_test_cases, +}; + +kunit_test_suite(atcphy_t8132_pm_test_suite); diff --git a/drivers/phy/apple/atc.c b/drivers/phy/apple/atc.c index 11c5834c76763..84cef035e0865 100644 --- a/drivers/phy/apple/atc.c +++ b/drivers/phy/apple/atc.c @@ -55,6 +55,9 @@ #include "atc-t8122-dp.h" #include "atc-tunnel.h" +/* Runtime-suspend delay, which covers the mux, orientation and reset calls of a plug */ +#define ATCPHY_AUTOSUSPEND_MS 2000 + #define AUSPLL_FSM_CTRL 0x1014 #define AUSPLL_APB_CMD_OVERRIDE 0x2000 @@ -657,8 +660,13 @@ struct apple_atcphy; * it; the lock handshake that starts the next PIPE change is * only acknowledged while it runs * @restore_after_pd_off: The PHY's power domain may be switched off in system - * sleep, which resets the block; bring the tracked - * state back once the domain is on again + * sleep, or at runtime with @idle_power_off, which + * resets the block; bring the tracked state back once + * the domain is on again + * @idle_power_off: Let an idle PHY runtime-suspend, so that its power domain + * can also drop while the system runs, see atcphy_rpm_get(). + * Only for PHYs without DisplayPort, whose phy ops and tunnel + * calls take no runtime PM reference */ struct atcphy_hw { void (*configure_post_tunable)(struct apple_atcphy *, enum atcphy_mode); @@ -677,6 +685,7 @@ struct atcphy_hw { bool park_pipe_unlocked; bool park_dummy_phy; bool restore_after_pd_off; + bool idle_power_off; }; enum atcphy_t8132_bank { @@ -759,6 +768,9 @@ enum atcphy_t8132_bank { * @pd_nb: Power domain notifier, registered with restore_after_pd_off * @pd_was_off: The power domain was switched off since the state was last * brought back; set from the notifier, cleared under @lock + * @idle_rpm: An idle PHY runtime-suspends, see atcphy_rpm_get() + * @rpm_held: A runtime PM reference is held while busy, see atcphy_rpm_busy(); + * under @lock * @regs: Memory-mapped registers * @regs.core: Core registers * @regs.axi2af: AXI to Apple Fabric interface registers @@ -840,6 +852,8 @@ struct apple_atcphy { struct notifier_block pd_nb; bool pd_was_off; + bool idle_rpm; + bool rpm_held; bool tunnel_dual_stream; bool tunnel_routes_present; @@ -3945,6 +3959,73 @@ static int atcphy_configure(struct apple_atcphy *atcphy, enum atcphy_mode mode) static void _atcphy_dwc3_reset_assert(struct apple_atcphy *atcphy); +/* + * On T8132 an idle PHY lets its power domain drop. That domain is the USB + * controller's, which also holds the PIPE handler: the PHY writes it to drive + * the controller's reset and to route the PIPE, also while the Type-C mux + * changes mode before the controller has resumed. So every operation that can + * reach the hardware powers the device first, and a reference stays held while + * the PHY runs anything, see atcphy_rpm_busy(). A running controller keeps the + * shared domain on by itself. + */ +static int atcphy_rpm_get(struct apple_atcphy *atcphy) +{ + if (!atcphy->idle_rpm) + return 0; + return pm_runtime_resume_and_get(atcphy->dev); +} + +/* + * What a power-down would lose: a mode or a host, a PIPE away from its parked + * dummy backend or between backends, or on T8132 a common-power epoch, which + * only its own stop may end. Without these the PHY is in the baseline that + * atcphy_restore_after_pd_off() sets up again. + */ +static bool atcphy_rpm_busy(const struct apple_atcphy *atcphy) +{ + return atcphy->mode != APPLE_ATCPHY_MODE_OFF || atcphy->host_active || + atcphy->pipe_pending || atcphy->pipe_state != ATCPHY_PIPEHANDLER_STATE_DUMMY || + atcphy->t8132_common_owned; +} + +/* Take or drop the reference held while busy; the device must be on. */ +static void atcphy_rpm_sync(struct apple_atcphy *atcphy) +{ + bool busy; + + if (!atcphy->idle_rpm) + return; + guard(mutex)(&atcphy->lock); + busy = atcphy_rpm_busy(atcphy); + if (busy == atcphy->rpm_held) + return; + if (busy) + pm_runtime_get_noresume(atcphy->dev); + else + pm_runtime_put_noidle(atcphy->dev); + atcphy->rpm_held = busy; + dev_dbg(atcphy->dev, "%s the power domain\n", busy ? "holding" : "releasing"); +} + +static void atcphy_rpm_put(struct apple_atcphy *atcphy) +{ + if (!atcphy->idle_rpm) + return; + atcphy_rpm_sync(atcphy); + pm_runtime_put_autosuspend(atcphy->dev); +} + +/* Drop the hold of an unbound PHY or a failed probe, before runtime PM is disabled. */ +static void atcphy_rpm_release(void *data) +{ + struct apple_atcphy *atcphy = data; + + guard(mutex)(&atcphy->lock); + if (atcphy->rpm_held) + pm_runtime_put_noidle(atcphy->dev); + atcphy->rpm_held = false; +} + /* * A T8122 PHY loses its register state whenever its power domain is switched * off. In system sleep that happens to a domain that no running device holds: @@ -3965,6 +4046,9 @@ static void _atcphy_dwc3_reset_assert(struct apple_atcphy *atcphy); * resume callback, whichever comes first, so the order in which the Type-C * controller, the USB controller and the display controller resume does not * matter. + * + * On T8132 the domain also switches off at runtime, once an idle PHY and its + * USB controller have both suspended, see atcphy_rpm_get(). */ static void atcphy_restore_after_pd_off(struct apple_atcphy *atcphy) { @@ -3990,20 +4074,29 @@ static void atcphy_restore_after_pd_off(struct apple_atcphy *atcphy) atcphy->host_active = false; atcphy_usb2_power_off(atcphy); } - ret = atcphy_power_off(atcphy); - if (ret) - dev_warn(atcphy->dev, "PHY did not power down after its domain was off: %d\n", - ret); + /* As at probe, M4 acquires its common bank only on USB3 admission. */ + if (!atcphy->hw->usb2_only) { + ret = atcphy_power_off(atcphy); + if (ret) + dev_warn(atcphy->dev, + "PHY did not power down after its domain was off: %d\n", ret); + } atcphy->mode = APPLE_ATCPHY_MODE_OFF; if (atcphy->dp_only) { dev_dbg(atcphy->dev, "power domain was off, restoring mode %d\n", mode); } else { - ret = atcphy_configure_pipehandler_dummy(atcphy, false); - if (ret) - dev_warn(atcphy->dev, "Failed to park the PIPE after its domain was off: %d\n", - ret); - atcphy->pipe_state = ATCPHY_PIPEHANDLER_STATE_DUMMY; + if (atcphy->hw->usb2_only) { + /* As at probe, with the dummy PIPE clock the USB2 controller needs */ + atcphy_setup_pipehandler(atcphy); + } else { + ret = atcphy_configure_pipehandler_dummy(atcphy, false); + if (ret) + dev_warn(atcphy->dev, + "Failed to park the PIPE after its domain was off: %d\n", + ret); + atcphy->pipe_state = ATCPHY_PIPEHANDLER_STATE_DUMMY; + } dev_dbg(atcphy->dev, "power domain was off, restoring mode %d: PIPE mux=%08x lock=%08x/%08x aon=%08x nonsel=%08x\n", mode, readl(atcphy->regs.pipehandler + PIPEHANDLER_MUX_CTRL), @@ -4095,10 +4188,40 @@ static int atcphy_usb2_init(struct phy *phy) return 0; } +/* + * The phy core also powers the device for init and power_off; these wrappers + * also take or drop the hold for the state an operation leaves behind. + */ +static int atcphy_rpm_usb2_init(struct phy *phy) +{ + struct apple_atcphy *atcphy = phy_get_drvdata(phy); + int ret; + + ret = atcphy_rpm_get(atcphy); + if (ret) + return ret; + ret = atcphy_usb2_init(phy); + atcphy_rpm_put(atcphy); + return ret; +} + +static int atcphy_rpm_usb2_set_mode(struct phy *phy, enum phy_mode mode, int submode) +{ + struct apple_atcphy *atcphy = phy_get_drvdata(phy); + int ret; + + ret = atcphy_rpm_get(atcphy); + if (ret) + return ret; + ret = atcphy_usb2_set_mode(phy, mode, submode); + atcphy_rpm_put(atcphy); + return ret; +} + static const struct phy_ops apple_atc_usb2_phy_ops = { .owner = THIS_MODULE, - .init = atcphy_usb2_init, - .set_mode = atcphy_usb2_set_mode, + .init = atcphy_rpm_usb2_init, + .set_mode = atcphy_rpm_usb2_set_mode, }; static int atcphy_usb3_power_off(struct phy *phy) @@ -4257,11 +4380,37 @@ static int atcphy_usb3_exit(struct phy *phy) return 0; } +static int atcphy_rpm_usb3_power_off(struct phy *phy) +{ + struct apple_atcphy *atcphy = phy_get_drvdata(phy); + int ret; + + ret = atcphy_rpm_get(atcphy); + if (ret) + return ret; + ret = atcphy_usb3_power_off(phy); + atcphy_rpm_put(atcphy); + return ret; +} + +static int atcphy_rpm_usb3_set_mode(struct phy *phy, enum phy_mode mode, int submode) +{ + struct apple_atcphy *atcphy = phy_get_drvdata(phy); + int ret; + + ret = atcphy_rpm_get(atcphy); + if (ret) + return ret; + ret = atcphy_usb3_set_mode(phy, mode, submode); + atcphy_rpm_put(atcphy); + return ret; +} + static const struct phy_ops apple_atc_usb3_phy_ops = { .owner = THIS_MODULE, .exit = atcphy_usb3_exit, - .power_off = atcphy_usb3_power_off, - .set_mode = atcphy_usb3_set_mode, + .power_off = atcphy_rpm_usb3_power_off, + .set_mode = atcphy_rpm_usb3_set_mode, }; static int atcphy_dpphy_set_mode(struct phy *phy, enum phy_mode mode, int submode) @@ -4623,9 +4772,35 @@ static int atcphy_dwc3_reset_deassert(struct reset_controller_dev *rcdev, unsign return 0; } +static int atcphy_rpm_dwc3_reset_assert(struct reset_controller_dev *rcdev, unsigned long id) +{ + struct apple_atcphy *atcphy = container_of(rcdev, struct apple_atcphy, rcdev); + int ret; + + ret = atcphy_rpm_get(atcphy); + if (ret) + return ret; + ret = atcphy_dwc3_reset_assert(rcdev, id); + atcphy_rpm_put(atcphy); + return ret; +} + +static int atcphy_rpm_dwc3_reset_deassert(struct reset_controller_dev *rcdev, unsigned long id) +{ + struct apple_atcphy *atcphy = container_of(rcdev, struct apple_atcphy, rcdev); + int ret; + + ret = atcphy_rpm_get(atcphy); + if (ret) + return ret; + ret = atcphy_dwc3_reset_deassert(rcdev, id); + atcphy_rpm_put(atcphy); + return ret; +} + static const struct reset_control_ops atcphy_dwc3_reset_ops = { - .assert = atcphy_dwc3_reset_assert, - .deassert = atcphy_dwc3_reset_deassert, + .assert = atcphy_rpm_dwc3_reset_assert, + .deassert = atcphy_rpm_dwc3_reset_deassert, }; static int atcphy_reset_xlate(struct reset_controller_dev *rcdev, @@ -4973,11 +5148,17 @@ static void atcphy_usb3_port_work(struct work_struct *work) struct atcphy_usb3_pm pm = {}; int ret; + ret = atcphy_rpm_get(atcphy); + if (ret) { + atcphy_usb3_port_retry(atcphy, false, ret); + return; + } ret = atcphy_usb3_pm_get(atcphy, &pm); if (!ret) { ret = atcphy_usb3_port_restore(atcphy, &pm); atcphy_usb3_pm_put(&pm); } + atcphy_rpm_put(atcphy); if (ret) atcphy_usb3_port_retry(atcphy, false, ret); } @@ -5347,7 +5528,15 @@ static int atcphy_mux_update(struct apple_atcphy *atcphy, const struct typec_mux static int atcphy_mux_set(struct typec_mux_dev *mux, struct typec_mux_state *state) { - return atcphy_mux_update(typec_mux_get_drvdata(mux), state); + struct apple_atcphy *atcphy = typec_mux_get_drvdata(mux); + int ret; + + ret = atcphy_rpm_get(atcphy); + if (ret) + return ret; + ret = atcphy_mux_update(atcphy, state); + atcphy_rpm_put(atcphy); + return ret; } static void atcphy_typec_mux_unregister(void *data) @@ -5850,12 +6039,13 @@ static void atcphy_pd_notifier_remove(void *data) dev_pm_genpd_remove_notifier(data); } -static void atcphy_probe_pd_notifier(struct apple_atcphy *atcphy) +/* Returns whether the PHY follows its power domain. */ +static bool atcphy_probe_pd_notifier(struct apple_atcphy *atcphy) { int ret; if (!atcphy->hw->restore_after_pd_off) - return; + return false; atcphy->pd_nb.notifier_call = atcphy_pd_notify; ret = dev_pm_genpd_add_notifier(atcphy->dev, &atcphy->pd_nb); @@ -5869,6 +6059,7 @@ static void atcphy_probe_pd_notifier(struct apple_atcphy *atcphy) dev_warn(atcphy->dev, "Cannot follow the PM domain (%d), PHY state is not restored after it was off\n", ret); + return !ret; } /* The T8103 PHY compatible is also a fallback on unqualified M1/M2 variants. */ @@ -5883,6 +6074,7 @@ static int atcphy_probe(struct platform_device *pdev) { struct apple_atcphy *atcphy; struct device *dev = &pdev->dev; + bool followed; int ret; atcphy = devm_kzalloc(&pdev->dev, sizeof(*atcphy), GFP_KERNEL); @@ -5914,9 +6106,29 @@ static int atcphy_probe(struct platform_device *pdev) atcphy->pipe_state = ATCPHY_PIPEHANDLER_STATE_DUMMY; atcphy->fixed_usb2 = atcphy_usb2_behind_fixed_hub(atcphy); atcphy->typec_mode = APPLE_ATCPHY_MODE_USB2; - atcphy_probe_pd_notifier(atcphy); + followed = atcphy_probe_pd_notifier(atcphy); - return atcphy_probe_finalize(atcphy); + /* + * Only a PHY that follows its domain may let it drop. Before the PHYs are + * created, so that they follow runtime PM too. + */ + if (atcphy->hw->idle_power_off && followed) { + pm_runtime_set_autosuspend_delay(dev, ATCPHY_AUTOSUSPEND_MS); + pm_runtime_use_autosuspend(dev); + ret = devm_pm_runtime_set_active_enabled(dev); + if (ret) + return ret; + ret = devm_add_action_or_reset(dev, atcphy_rpm_release, atcphy); + if (ret) + return ret; + atcphy->idle_rpm = true; + } + + ret = atcphy_probe_finalize(atcphy); + if (ret) + return ret; + atcphy_rpm_sync(atcphy); + return 0; } static int atcphy_suspend(struct device *dev) @@ -5947,6 +6159,12 @@ static int atcphy_resume(struct device *dev) guard(mutex)(&atcphy->lock); atcphy->t8132_suspended = false; + /* + * A suspended PHY holds nothing, and its domain may drop again once system + * resume completes: leave the restore to its next operation. + */ + if (atcphy->idle_rpm && pm_runtime_status_suspended(dev)) + return 0; atcphy_restore_after_pd_off(atcphy); return 0; @@ -6006,6 +6224,8 @@ static const struct atcphy_hw atcphy_hw_t8132 = { .usb2phy_sig_host = USB2PHY_SIG_HOST_T8132, .pipe_mux_clk = PIPEHANDLER_MUX_CTRL_CLK_T8103, .pipe_mux_data = PIPEHANDLER_MUX_CTRL_DATA_T8103, + .restore_after_pd_off = true, + .idle_power_off = true, }; static const struct atcphy_hw atcphy_hw_t8140 = { From 4cacc48c78a9674a8160e2a06f0518f2d9f41bb5 Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Tue, 27 Oct 2026 20:04:49 -0400 Subject: [PATCH 380/388] usb: dwc3: apple: Let an idle T8132 controller runtime-suspend With no cable the controller's core is down, but on T8132 the controller stays runtime-active, which would keep its power domain, its DARTs and, through the PHY device link, the PHY powered. Allow runtime PM there as on T6040, so that the glue's reference alone decides. A port that has not seen a cable since boot has no core probe yet, and runtime PM stays disabled until dwc3_core_probe() enables it. Until then, on T8132, let the glue own runtime PM so that such a port can suspend too, and hand it over powered, before the first reset deassert. If that first core probe fails, runtime PM stays with the core and the controller powered, as before. Signed-off-by: Erik Bourget --- drivers/usb/dwc3/dwc3-apple.c | 51 ++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/drivers/usb/dwc3/dwc3-apple.c b/drivers/usb/dwc3/dwc3-apple.c index 998b6cffe6297..c11d299840d80 100644 --- a/drivers/usb/dwc3/dwc3-apple.c +++ b/drivers/usb/dwc3/dwc3-apple.c @@ -90,10 +90,11 @@ * dwc3_apple_init(). In host mode xHCI then keeps running in suspend-to-idle when a USB device * may wake the system, as a halted xHC could not signal the wakeup. * - * Runtime PM (T6040 only, see dwc3_apple_core_probe()): the glue holds a reference from the - * reset deassert to the end of the shutdown, so with no cable the controller can runtime-suspend - * and its power domain drop, its DARTs (which save and restore their state) and, through the - * device link of its phys, the PHY with it. + * Runtime PM (T6040 and T8132, see dwc3_apple_core_probe()): the glue holds a reference from + * the reset deassert to the end of the shutdown, so with no cable the controller can + * runtime-suspend and its power domain drop, its DARTs (which save and restore their state) and, + * through the device link of its phys, the PHY with it. On T8132 a port that has not seen a cable + * yet can suspend as well, with runtime PM held by the glue until the first core probe. * * enum dwc3_apple_state is defined in dwc3-apple-state.h. */ @@ -123,6 +124,7 @@ * @usb3_power_off_pending: Core-owned USB3 references still to release * @idle_rpm: Let the controller runtime-suspend while the core is down * @rpm_held: The glue holds a runtime PM reference for the running core + * @pre_core_rpm: Runtime PM is the glue's own until the first core bring-up * @drd_cfg: The controller's configuration block (optional, for its tunable) * @tunables: Controller tunables from the device tree, see dwc3_apple_tunable_list */ @@ -151,6 +153,7 @@ struct dwc3_apple { unsigned long usb3_power_off_pending; bool idle_rpm; bool rpm_held; + bool pre_core_rpm; void __iomem *drd_cfg; struct apple_tunable *tunables[DWC3_APPLE_NR_TUNABLES]; @@ -524,8 +527,21 @@ static int dwc3_apple_core_start(struct dwc3_apple *appledwc, enum dwc3_apple_st } /* - * Before the first core probe runtime PM is not enabled yet and the controller stays - * powered; dwc3_apple_core_probe() takes the reference then. + * Power an idle port's controller up for its first core probe, then leave runtime PM + * to dwc3_core_probe(), which expects it disabled with the device active. + */ + if (appledwc->pre_core_rpm) { + ret = pm_runtime_resume_and_get(appledwc->dev); + if (ret) + return ret; + pm_runtime_disable(appledwc->dev); + pm_runtime_put_noidle(appledwc->dev); + appledwc->pre_core_rpm = false; + } + + /* + * Before the first core probe the controller is powered, with runtime PM off or handed + * over above; dwc3_apple_core_probe() takes the reference then. */ if (appledwc->state != DWC3_APPLE_PROBE_PENDING && !appledwc->rpm_held) { ret = pm_runtime_resume_and_get(appledwc->dev); @@ -839,8 +855,9 @@ static int dwc3_apple_probe(struct platform_device *pdev) appledwc->dev = &pdev->dev; appledwc->fixed_hub = dwc3_apple_has_fixed_hub(dev); appledwc->host_only = dwc3_apple_host_only(dev); - /* Tested on T6040 only, where the PHY driver and the DARTs follow runtime PM. */ - appledwc->idle_rpm = of_device_is_compatible(dev->of_node, "apple,t6040-dwc3"); + /* The PHY driver and the DARTs follow runtime PM on these SoCs. */ + appledwc->idle_rpm = of_device_is_compatible(dev->of_node, "apple,t6040-dwc3") || + of_device_is_compatible(dev->of_node, "apple,t8132-dwc3"); mutex_init(&appledwc->lock); /* A never-connected port has no generic core probe to publish this. */ appledwc->dwc.dev = dev; @@ -906,9 +923,23 @@ static int dwc3_apple_probe(struct platform_device *pdev) * details. */ appledwc->state = DWC3_APPLE_PROBE_PENDING; + /* + * Until the first cable there is no core to keep powered: let the controller + * runtime-suspend, and its DARTs with it, so that its power domain can drop. + * Tested on T8132 only. + */ + if (appledwc->idle_rpm && !appledwc->fixed_hub && + of_device_is_compatible(dev->of_node, "apple,t8132-dwc3")) { + pm_runtime_set_active(dev); + pm_runtime_enable(dev); + appledwc->pre_core_rpm = true; + } ret = dwc3_apple_setup_role_switch(appledwc); - if (ret) + if (ret) { + if (appledwc->pre_core_rpm) + pm_runtime_disable(dev); return dev_err_probe(&pdev->dev, ret, "Failed to setup role switch\n"); + } if (!appledwc->fixed_hub) return 0; @@ -951,6 +982,8 @@ static void dwc3_apple_remove(struct platform_device *pdev) dev_err(appledwc->dev, "Removal left pending PHY/reset cleanup\n"); if (appledwc->state != DWC3_APPLE_PROBE_PENDING) dwc3_core_remove_no_shutdown(&appledwc->dwc); + if (appledwc->pre_core_rpm) + pm_runtime_disable(appledwc->dev); } /* Stop the consumer and release its PHY epoch before the supplier shutdown. */ From cf4eb7c2e357b7a4002b7650d256431df6559519 Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Tue, 27 Oct 2026 20:04:50 -0400 Subject: [PATCH 381/388] arm64: dts: apple: t8132: Let the idle J713 USB-C ports power down Place the USB controllers, their DARTs and the Type-C PHYs in their ATCn_USB domains, as on T8103, and let the J713 policy manage ATCn_USB and ATCn_COMMON: an idle port then switches both off. The ATCn_USB_AON parents stay on, as handed over (apple,inherited-on): they are left out of this change. The SoC and board parts go together: a consumer of a domain the J713 policy disables never probes. The PHY shares the controller's domain because it writes the PIPE handler in it, and the drivers must power the domain down only while both are idle, so a DTB with this change needs a kernel with the PHY and controller runtime PM changes. The other T8132 boards keep these nodes disabled. In three paired runs on a J713 on battery, with the display at 150 nits, both idle ports drew 90 and 130 mW less than with their domains held on; the first pair, 218 mW, included drift. Signed-off-by: Erik Bourget --- arch/arm64/boot/dts/apple/t8132-j713-pmgr.dtsi | 13 +++++++------ arch/arm64/boot/dts/apple/t8132-j713.dts | 1 - arch/arm64/boot/dts/apple/t8132.dtsi | 8 ++++++++ 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/arch/arm64/boot/dts/apple/t8132-j713-pmgr.dtsi b/arch/arm64/boot/dts/apple/t8132-j713-pmgr.dtsi index 2cad6906ce0ec..2e329cecdbbfe 100644 --- a/arch/arm64/boot/dts/apple/t8132-j713-pmgr.dtsi +++ b/arch/arm64/boot/dts/apple/t8132-j713-pmgr.dtsi @@ -190,7 +190,7 @@ }; &ps_atc0_common { - status = "disabled"; + status = "okay"; }; &ps_spi2 { @@ -279,7 +279,6 @@ &ps_atc1_common { status = "okay"; - apple,inherited-on; }; &ps_atc1_pcie { @@ -506,19 +505,21 @@ }; &ps_atc0_usb_aon { - status = "disabled"; + status = "okay"; + apple,inherited-on; }; &ps_atc1_usb_aon { - status = "disabled"; + status = "okay"; + apple,inherited-on; }; &ps_atc0_usb { - status = "disabled"; + status = "okay"; }; &ps_atc1_usb { - status = "disabled"; + status = "okay"; }; &ps_atc2_usb_aon { diff --git a/arch/arm64/boot/dts/apple/t8132-j713.dts b/arch/arm64/boot/dts/apple/t8132-j713.dts index 6fcbd22e9bd2e..da55888e6d5e4 100644 --- a/arch/arm64/boot/dts/apple/t8132-j713.dts +++ b/arch/arm64/boot/dts/apple/t8132-j713.dts @@ -232,7 +232,6 @@ <0x4 0x0b050000 0x0 0x4000>; reg-names = "usb2phy", "pipehandler", "core", "axi2af", "lioa", "fabric", "common-config"; - power-domains = <&ps_atc1_common>; ports { #address-cells = <1>; #size-cells = <0>; diff --git a/arch/arm64/boot/dts/apple/t8132.dtsi b/arch/arm64/boot/dts/apple/t8132.dtsi index c5b1189d344e5..e94528277a805 100644 --- a/arch/arm64/boot/dts/apple/t8132.dtsi +++ b/arch/arm64/boot/dts/apple/t8132.dtsi @@ -923,6 +923,7 @@ #phy-cells = <1>; #reset-cells = <0>; + power-domains = <&ps_atc0_usb>; orientation-switch; mode-switch; @@ -936,6 +937,7 @@ interrupt-parent = <&aic>; interrupts = ; #iommu-cells = <1>; + power-domains = <&ps_atc0_usb>; }; dwc3_0_dart_1: iommu@402f80000 { @@ -945,6 +947,7 @@ interrupt-parent = <&aic>; interrupts = ; #iommu-cells = <1>; + power-domains = <&ps_atc0_usb>; }; dwc3_0: usb@402280000 { @@ -962,6 +965,7 @@ role-switch-default-mode = "host"; iommus = <&dwc3_0_dart_0 0>, <&dwc3_0_dart_1 1>; + power-domains = <&ps_atc0_usb>; resets = <&atcphy0>; phys = <&atcphy0 PHY_TYPE_USB2>; phy-names = "usb2-phy"; @@ -978,6 +982,7 @@ #phy-cells = <1>; #reset-cells = <0>; + power-domains = <&ps_atc1_usb>; orientation-switch; mode-switch; @@ -991,6 +996,7 @@ interrupt-parent = <&aic>; interrupts = ; #iommu-cells = <1>; + power-domains = <&ps_atc1_usb>; }; dwc3_1_dart_1: iommu@40af80000 { @@ -1000,6 +1006,7 @@ interrupt-parent = <&aic>; interrupts = ; #iommu-cells = <1>; + power-domains = <&ps_atc1_usb>; }; dwc3_1: usb@40a280000 { @@ -1017,6 +1024,7 @@ role-switch-default-mode = "host"; iommus = <&dwc3_1_dart_0 0>, <&dwc3_1_dart_1 1>; + power-domains = <&ps_atc1_usb>; resets = <&atcphy1>; phys = <&atcphy1 PHY_TYPE_USB2>; phy-names = "usb2-phy"; From 94921e7f184bbd3291844583f181af0b86ed28d0 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Wed, 28 Oct 2026 11:23:28 +1100 Subject: [PATCH 382/388] selftests: apple-dcp: follow the T8132 idle runtime PM The J713 idle power-down wraps the ATC mux callback in atcphy_rpm_get() and atcphy_rpm_put(), brings a USB2-only PHY back through atcphy_setup_pipehandler() after its domain was off, and gives the dwc3-apple glue a pre_core_rpm flag. Extract the production runtime PM wrappers next to the mux callback, and atcphy_setup_pipehandler() where the restore calls it, and add the idle_rpm, rpm_held and pre_core_rpm fields to the fixture structs. The fixture PHYs and controllers are not T8132, so all three stay false: a PHY runtime PM call would trip the existing root-port mocks or the new pm_runtime_get_noresume() stub. No assertion changes. Assisted-by: LLM Signed-off-by: Ryan Murray --- .../selftests/drivers/gpu/apple-dcp/common.py | 17 +++++++++++++++++ .../drivers/gpu/apple-dcp/dwc3/mocks.c | 2 ++ .../drivers/gpu/apple-dcp/lifecycle.py | 6 +++++- .../selftests/drivers/gpu/apple-dcp/phy.py | 5 ++++- .../selftests/drivers/gpu/apple-dcp/pm.py | 6 +++++- .../drivers/gpu/apple-dcp/teardown_pm.py | 3 ++- 6 files changed, 35 insertions(+), 4 deletions(-) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/common.py b/tools/testing/selftests/drivers/gpu/apple-dcp/common.py index 918a9d91fa30f..8ef176769ebf8 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/common.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/common.py @@ -111,6 +111,23 @@ def atc_function(source, name): return block(source, marker.start()) + '\n' if marker else '' +# The runtime PM wrappers of a PHY that powers down while idle (T8132 idle_power_off). +# Fixture PHYs leave idle_rpm false, so the wrappers must not reach runtime PM; older +# sources have none and atc_function() then yields ''. +ATC_RPM = ('atcphy_rpm_get', 'atcphy_rpm_busy', 'atcphy_rpm_sync', 'atcphy_rpm_put') + + +def atc_restore_helpers(source): + """What atcphy_restore_after_pd_off() needs beyond the parking helpers. + + With T8132 idle runtime PM a USB2-only PHY is brought back as at probe, through + atcphy_setup_pipehandler(); older sources only park the PIPE. '' when unused. + """ + if 'atcphy_setup_pipehandler(' not in atc_function(source, 'atcphy_restore_after_pd_off'): + return '' + return atc_function(source, 'atcphy_setup_pipehandler') + + def atc_mux_without_pm(source): """Lane/teardown fixtures omit USB PM, keeping mode mapping and lane work real. diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/dwc3/mocks.c b/tools/testing/selftests/drivers/gpu/apple-dcp/dwc3/mocks.c index 938126350876f..22e01ce7be38d 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/dwc3/mocks.c +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/dwc3/mocks.c @@ -85,6 +85,8 @@ struct dwc3_apple { struct phy *usb2_phy, *usb3_phy; bool fixed_hub, core_active, core_exit_pending, reset_pending, shutting_down; bool rpm_held, host_only; + /* T8132 only: runtime PM before the first core probe; never set here. */ + bool pre_core_rpm; unsigned long usb2_power_off_pending, usb3_power_off_pending; }; static unsigned int polls, fail_poll, reset_calls, reset_writes, core_writes; diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py index ede39666c8413..37d7784aee33d 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py @@ -3,7 +3,7 @@ """Actual disconnect authority, DWC3 teardown and firmware resource admission.""" from pathlib import Path import re -from common import block, apple_function, internal_inline, atc_mux_without_pm, source_defines, ATC_POST_TUNABLE, tipd_pm_header, firmware_lineage +from common import block, apple_function, internal_inline, atc_mux_without_pm, source_defines, ATC_POST_TUNABLE, tipd_pm_header, firmware_lineage, atc_restore_helpers def disconnect(root): # Copyright 2026 Ryan Murray @@ -1039,6 +1039,10 @@ def fn(n, name): 'dwc3_core_exit_after_phy_power_off', 'dwc3_core_exit', 'dwc3_core_remove_common', 'dwc3_core_remove', 'dwc3_core_remove_no_shutdown') names['atc'] += ('atcphy_dwc3_reset_deassert',) + if atc_restore_helpers(files['atc']): + # A USB2-only PHY is brought back as at probe after its domain was off. + at = names['atc'].index('atcphy_restore_after_pd_off') + names['atc'] = names['atc'][:at] + ('atcphy_setup_pipehandler',) + names['atc'][at:] names['atc'] = tuple(name for name in names['atc'] if name not in ('atcphy_get_mode_config', 'j700_field_update')) names = {n: tuple(name for name in group if re.search(r'^(?:static [^\n]*\b|(?:int|void) )' + name + r'\(', files[n], re.M)) for n, group in names.items()} diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/phy.py b/tools/testing/selftests/drivers/gpu/apple-dcp/phy.py index 0520a6bdd1226..2c4db1095f005 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/phy.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/phy.py @@ -4,7 +4,7 @@ from pathlib import Path import re from common import (block, source_defines, atc_mux_without_pm, ATC_POST_TUNABLE, - atc_function, ATC_SPLIT, ATC_T8103_LIVE) + atc_function, ATC_SPLIT, ATC_T8103_LIVE, atc_restore_helpers) def parking(root): @@ -51,6 +51,8 @@ def function(source, name): bool usb3_port_off, park_owed; bool t8103_dummy_pending, t8103_usb3_retry, t8103_usb3_workaround; bool dp_only, pd_was_off, tunnel_clock_on; + /* T8132 idle runtime PM; these PHYs never power down while idle. */ + bool idle_rpm, rpm_held; unsigned int tunnel_rate, tunnel_users; struct { bool aux, pll; unsigned int pairs, rate; } dp_t8122; enum atcphy_mode mode, typec_mode; @@ -176,6 +178,7 @@ def function(source, name): for name in ('core_read32', 'atcphy_get_mode_config', 'j700_field_update', 'j700_pma_idle'): parts.append(function(atc, name)) parts.append(dp) + parts.append(atc_restore_helpers(atc)) for name in ('atcphy_configure_lanes', 'j700_aux_enable', 'j700_aux_disable', 'atcphy_enable_dp_aux', 'atcphy_disable_dp_aux', 'atcphy_power_off', 'atcphy_power_on', *ATC_POST_TUNABLE, 'atcphy_configure', 'atcphy_set_orientation', diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py b/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py index d5b3890d30861..1475451c1cef1 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py @@ -4,7 +4,7 @@ from pathlib import Path import os, re, shlex, shutil, subprocess import phy -from common import block, tipd_pm_header, atc_function +from common import block, tipd_pm_header, atc_function, ATC_RPM def ownership(root, usb_unreachable=False): ns=phy.parking(root) @@ -171,6 +171,8 @@ def ownership(root, usb_unreachable=False): assert(root_locked&&!pm_phy->lock); if(u==&link_device)assert(link_locked);else assert(u==&roots[3]&&!link_locked); } +/* Only an idle-power-off PHY takes a hold of its own; the fixture PHY never does. */ +static void __attribute__((unused)) pm_runtime_get_noresume(struct device *d) { (void)d; assert(!"PHY runtime PM hold"); } static void pm_runtime_put_autosuspend(struct device *d) { assert(root_locked&&!pm_phy->lock); if(d==&link_device.dev){assert(link_locked&&link_pm_refs==1);link_pm_refs--;} @@ -421,6 +423,8 @@ def ownership(root, usb_unreachable=False): ('atcphy_park_retry_delay','atcphy_park_retry','atcphy_park_due','atcphy_mux_change')) helpers+=ns['function'](s,'atcphy_mux_update')+'\n' # Lane fixtures provide the target mapping and locked body; use the real callback here. + # The mux callback powers an idle-power-off PHY first; this PHY is not one. + helpers+=''.join(atc_function(s,name) for name in ATC_RPM) marker=re.search(r'^static int atcphy_mux_set\(',s,re.M).group() helpers+=ns['block'](s,marker)+'\n' else: diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/teardown_pm.py b/tools/testing/selftests/drivers/gpu/apple-dcp/teardown_pm.py index b4a4b18a8f468..7381092399252 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/teardown_pm.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/teardown_pm.py @@ -94,7 +94,8 @@ def unit_for(source): #define hcd_to_xhci(h) (&xhci_context) struct dwc3 { bool sys_wakeup; struct platform_device *xhci; }; enum { DWC3_APPLE_PROBE_PENDING, DWC3_APPLE_NO_CABLE, DWC3_APPLE_HOST, DWC3_APPLE_DEVICE }; -struct dwc3_apple { struct dwc3 dwc; struct device *dev; int lock,state; void *role_sw; }; +/* pre_core_rpm is T8132 only: runtime PM before the first core probe; never set here. */ +struct dwc3_apple { struct dwc3 dwc; struct device *dev; int lock,state; void *role_sw; bool pre_core_rpm; }; static struct dwc3_apple apple_context; #define to_dwc3_apple(d) container_of(d,struct dwc3_apple,dwc) static bool inside_host_remove; From cb4fb174d50dda2e0f740b11c1b9a13a3678c5b2 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Wed, 28 Oct 2026 08:45:53 +1100 Subject: [PATCH 383/388] drm/apple: clear the desktop 26.6 peer allocation as one field group dcp_peer_hints_26_6() clears the 32-byte allocation block of the display hints (primary and peer endpoint IDs, extra pipe count, flags and the reserved bytes) with memset(&r->primary_id, 0, 32). FORTIFY_SOURCE sees a write of 32 bytes into the 4-byte primary_id field and fails the build with -Werror (__write_overflow_field), so dcp.o does not compile with the Fedora-based release configuration. Group the allocation fields with struct_group_attr(), keeping the packed layout, and clear the group by its size. The offsets are unchanged and a new static_assert pins the group at 32 bytes. Fixes: e3e7131681e2 ("drm/apple: drive a display wider than one pipe with two desktop 26.6 pipes") Assisted-by: LLM Signed-off-by: Ryan Murray (cherry picked from commit e2f5382def10e2b927edcd79a55120c9b1cbac4d) --- drivers/gpu/drm/apple/dcp-peer-v26_6.h | 11 ++++++++--- drivers/gpu/drm/apple/dcp-peer-v26_6.inc | 6 +++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp-peer-v26_6.h b/drivers/gpu/drm/apple/dcp-peer-v26_6.h index df83045cfa73e..3b88d5e040303 100644 --- a/drivers/gpu/drm/apple/dcp-peer-v26_6.h +++ b/drivers/gpu/drm/apple/dcp-peer-v26_6.h @@ -4,6 +4,7 @@ #include #include +#include #include struct dcp_peer_tiling { @@ -78,9 +79,12 @@ static inline bool dcp_peer_tiling_set(struct dcp_peer_tiling *own, struct dcp_display_hints { u8 hints[0x6c]; u8 padding[4]; - __le32 primary_id, peer_id; - u8 extra_pipes, flags; - u8 allocation_reserved[22]; + /* The allocation the AP fills in, or clears when it allocates no peer. */ + struct_group_attr(allocation, __packed, + __le32 primary_id, peer_id; + u8 extra_pipes, flags; + u8 allocation_reserved[22]; + ); __le32 status; u8 tail[12]; } __packed; @@ -88,6 +92,7 @@ struct dcp_display_hints { static_assert(sizeof(struct dcp_display_hints) == 160); static_assert(offsetof(struct dcp_display_hints, primary_id) == 0x70); static_assert(offsetof(struct dcp_display_hints, status) == 0x90); +static_assert(sizeof_field(struct dcp_display_hints, allocation) == 32); /* The widest mode one desktop 26.6 DISPEXT pipe drives (firmware: "max width: 6720"). */ #define DCP_DESKTOP_PIPE_MAX_WIDTH 6720 diff --git a/drivers/gpu/drm/apple/dcp-peer-v26_6.inc b/drivers/gpu/drm/apple/dcp-peer-v26_6.inc index b8038db010943..29c4b6e697ebc 100644 --- a/drivers/gpu/drm/apple/dcp-peer-v26_6.inc +++ b/drivers/gpu/drm/apple/dcp-peer-v26_6.inc @@ -259,7 +259,7 @@ int dcp_peer_hints_26_6(struct apple_dcp *dcp, struct dptx_port *port, width = get_unaligned_le32(r->hints + 0x38); if (!(r->hints[0] & 1) || !width || width <= DCP_DESKTOP_PIPE_MAX_WIDTH) { - memset(&r->primary_id, 0, 32); + memset(&r->allocation, 0, sizeof(r->allocation)); return 0; } @@ -277,7 +277,7 @@ int dcp_peer_hints_26_6(struct apple_dcp *dcp, struct dptx_port *port, (dcp_peer_reject_26_6(dcp, dcp->peer_reserved) & ~BIT(1))) dcp_peer_drop_locked(dcp, dcp->peer_reserved); if (dcp->peer_owner) { - memset(&r->primary_id, 0, 32); + memset(&r->allocation, 0, sizeof(r->allocation)); return 0; } peer = dcp->peer_reserved; @@ -301,7 +301,7 @@ int dcp_peer_hints_26_6(struct apple_dcp *dcp, struct dptx_port *port, dev_info(dcp->dev, "no idle peer pipe for a %u px display\n", width); WRITE_ONCE(dcp->peer_starved, true); dcp_fabric_hybrid_kick(dcp, 0); - memset(&r->primary_id, 0, 32); + memset(&r->allocation, 0, sizeof(r->allocation)); return 0; } WRITE_ONCE(dcp->peer_starved, false); From ca1a1e42eceeb3139c88a3e60e9a710e042caf98 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Wed, 28 Oct 2026 08:24:31 +1100 Subject: [PATCH 384/388] selftests: apple-dcp: follow the hybrid hand-over in the route fixtures The actual-DCP route fixtures compile the real dcp_typec_route_set(), dcp_typec_route_deactivate() and dcp_typec_route_available(). The J616s series added hybrid hand-over state to them. Deactivation now clears peer_starved and typec_narrow, releases a desktop 26.6 peer and restores the fixed output through its own fixed_xbar. A port leaving DP clears typec_yielded and dcp_hybrid_fails and hands every yielded hybrid on the port's routes back to HDMI. Availability asks dcp_typec_hybrid_first(). The mock struct apple_dcp and port had none of this, so seven units no longer compiled. Add the fields and a counter. Extract the real dcp_typec_hybrid_first() over the production defaults (module parameter -1, no DT opt-in), so the policy is as before. Stub the peer release to assert, since no scenario is on desktop 26.6. Let the single-port list walk yield the port's one route when it walks routes. Probe now records the fixed output's crossbar in fixed_xbar as well as xbar (7b990950a349), and deactivation restores the fixed route through fixed_xbar. The mux-release-stages fixed-restore case set only xbar, so set both as probe does. Its assertions are unchanged: the restore is retried once after its failure and the deselect stays balanced. Fixes: 7b990950a349 ("drm/apple: fabric: keep the hybrid pipeline's HDMI crossbar apart from its Type-C route's") Fixes: 0d43a7f471ac ("drm/apple: fabric: hand the hybrid between an HDMI and a two-pipe Type-C display") Assisted-by: LLM Signed-off-by: Ryan Murray (cherry picked from commit e549591ec26d877731f574c98b82b8fd1052cd8d) --- .../drivers/gpu/apple-dcp/accounting.py | 2 +- .../drivers/gpu/apple-dcp/transaction.py | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/accounting.py b/tools/testing/selftests/drivers/gpu/apple-dcp/accounting.py index ce2791cc3a379..9c74ec637be24 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/accounting.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/accounting.py @@ -103,7 +103,7 @@ def resources(ROOT): puts("PASS successful DCP finalizer is not repeated after a later provider failure");return 0; } if(argv[1][0]=='h')hw_failures=1; - if(argv[1][0]=='f') {display.fixed_connector_type=2;display.hdmi_hpd=(void *)1;gpio_level=1;display.xbar=&fixed_xbar;fixed_restore_failures=1;} + if(argv[1][0]=='f') {display.fixed_connector_type=2;display.hdmi_hpd=(void *)1;gpio_level=1;display.xbar=display.fixed_xbar=&fixed_xbar;fixed_restore_failures=1;} assert(typec_mux_set(&mux,&safe)==-EIO&&xbar.lock==1&&port.owner==&route&&port.safe_pending&&!port.safe_finish_ready); bool selected_after_error=route.selected; int ret=typec_mux_set(&mux,&safe); diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/transaction.py b/tools/testing/selftests/drivers/gpu/apple-dcp/transaction.py index 5dea64a19054e..6c40adf5d3a85 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/transaction.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/transaction.py @@ -40,13 +40,21 @@ def check(name, unit, cases=((),), negative=False, flags=()): bool external_power_attempted,dptx_tunnel,tb_clock_ok,fixed_route_selected; int tb_lock; unsigned typec_generation,dptx_dfp_port,dptx_phy,fixed_dptx_phy,fixed_mux_index; void *tb_dpin_set_active,*tb_dpin_ctx,*phy,*fixed_phy; - struct mux_control *xbar; + struct mux_control *xbar,*fixed_xbar; + /* desktop 26.x hybrid hand-over and dual-pipe state; fw_compat is never desktop here */ + bool peer_starved,typec_narrow,hdmi_yielded; struct apple_dcp_typec_route *active_typec_route,*typec_routes; struct connector *connector,*fixed_connector;int connector_type,fixed_connector_type; struct mux_control *direct_xbar_up; bool external_native, ext_dead; int fw_compat; unsigned long long tb_generation; int modes_lock; unsigned long long modes_generation; bool modes_admitted,modes_provisional; struct { int iomfb_method_profile; } hw; ''',1) + # The hybrid hand-over walks a port's routes; the only route is this port's. + actual=actual.replace('struct apple_dcp_typec_port { int link;', 'struct apple_dcp_typec_port { int link,routes; bool typec_yielded;',1) + actual=actual.replace('#define list_for_each_entry(p,h,m) for((void)(h),(p)=sole_port();(p);(p)=NULL)', + '#define list_for_each_entry(p,h,m) for((void)(h),(p)=_Generic((p),struct apple_dcp_typec_route *:sole_route(),default:sole_port());(p);(p)=NULL)',1) + actual=actual.replace('static struct apple_dcp_typec_port *sole_port(void) { return &port; }', + 'static struct apple_dcp_typec_port *sole_port(void) { return &port; }\nstatic struct apple_dcp_typec_route * __attribute__((unused)) sole_route(void) { return &route; }',1) actual=actual.replace('bool applied_valid,safe_pending,dp_wanted,dp_hpd,hpd;', 'struct typec_altmode *applied_alt; unsigned long applied_mode; unsigned applied_status,applied_conf; bool applied_valid,safe_pending,dp_wanted,dp_hpd,hpd;',1) actual=actual.replace('void *xbar; bool deactivate_xbar_done; int deactivate_error; bool selected,tunnel;', 'struct mux_control *xbar,*active_xbar; void *phy; unsigned dptx_phy,tunnel_dpin,mux_index;unsigned long long tunnel_generation;bool deactivate_xbar_done; int deactivate_error;bool xbar_up,selected,tunnel;',1) extra=r''' @@ -70,6 +78,10 @@ def check(name, unit, cases=((),), negative=False, flags=()): static int mux_control_deselect(struct mux_control *m) { (void)m;event('C');xbar_calls++;return xbar_error; } static int mux_control_select(struct mux_control *m,unsigned n) { (void)m;(void)n;return 0; } static void dpavservep_detach(struct apple_dcp *d) { (void)d; } + /* Desktop 26.6 dual-pipe peers: no scenario here is on that firmware. */ + static void __attribute__((unused)) dcp_peer_release_26_6(struct apple_dcp *d) { (void)d;assert(!"desktop 26.6 peer release"); } + /* failed takes of the hybrid, reset when a Type-C display leaves DP */ + static unsigned int __attribute__((unused)) dcp_hybrid_fails; static void dcp_firmware_metadata_connection(struct apple_dcp *d,unsigned n,bool c) { (void)d;(void)n;(void)c; } static bool dcp_typec_keep_order(void) { return false; } static struct apple_dcp_typec_route * __attribute__((unused)) dcp_typec_lowest_free(struct apple_dcp_typec_port *p) { (void)p;return &route; } @@ -115,6 +127,11 @@ def check(name, unit, cases=((),), negative=False, flags=()): '''+available # Route-follow: the fixed output, or a failed T6030 external processor, keeps the route busy. available+=apple_function(ROOT,'dcp_typec_route_busy') + # Hybrid-first policy: the real helper over the module parameter's and the + # device tree's defaults (-1: from the DT; no apple,typec-hybrid-first here). + hybrid_first=apple_function(ROOT,'dcp_typec_hybrid_first') + if hybrid_first: + available='static int typec_hybrid_first=-1;\n static bool typec_hybrid_first_dt;\n '+hybrid_first+available if available: actual=actual.replace('struct apple_dcp { int external_lock,external_error;','struct apple_dcp { int external_lock,external_error;\n struct dcp_fabric_presence hdmi_presence; int hdmi_presence_lock; unsigned nr_typec_routes;',1) available='''#define atomic_read(p) (*(p)) From f139b700f150f0a8182fc037c7c7dc4c87eb96e1 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Wed, 28 Oct 2026 08:24:31 +1100 Subject: [PATCH 385/388] selftests: apple-dcp: follow adaptive sync on desktop 26.6 Desktop 26.6 now takes adaptive sync with a mode set. 562eb4c4087d removed dcp_mode_vrr_supported(), which the publication fixture extracted unconditionally. 6477da65cee5 made dcp_crtc_atomic_check() turn a VRR_ENABLED change on that firmware into a mode set, through drm_atomic_get_old_crtc_state() and crtc_state->mode_changed, which the VRR admission fixture did not model. So publication-hpd failed to build its unit and vrr-rejection did not compile. Extract the VRR filter only where the source still has it. Give the VRR fixture the desktop firmware value, mode_changed and an old-state lookup that counts its calls. The existing 72 admission cases run unchanged and now also check that no other firmware looks at the old state or forces a mode set. Where the source has the desktop path, 12 more cases pin it: an enabled CRTC whose VRR_ENABLED differs from its old state gets mode_changed, and nothing else does (a disabled CRTC, an unchanged VRR_ENABLED, no old state). Fixes: 562eb4c4087d ("drm/apple: enable adaptive sync on desktop 26.6 external displays") Fixes: 6477da65cee5 ("drm/apple: enable adaptive sync on the desktop 26.6 internal panel") Assisted-by: LLM Signed-off-by: Ryan Murray (cherry picked from commit 6822701ad2722a56da72ac27cfb0e6561b901029) --- .../drivers/gpu/apple-dcp/lifecycle.py | 47 +++++++++++++++++-- .../selftests/drivers/gpu/apple-dcp/rate.py | 9 ++-- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py index 440b5202f670a..24f95a4453053 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py @@ -1672,7 +1672,8 @@ def vrr(root): struct apple_connector { bool connected; }; /* The native T6030 14.7 backend is never selected in these scenarios. */ enum dcp_firmware_version { DCP_FIRMWARE_UNKNOWN, DCP_FIRMWARE_V_12_3, DCP_FIRMWARE_V_13_5, - DCP_FIRMWARE_V_14_7, DCP_FIRMWARE_V_14_7_TEMPLATE, DCP_FIRMWARE_V_26_6 }; + DCP_FIRMWARE_V_14_7, DCP_FIRMWARE_V_14_7_TEMPLATE, DCP_FIRMWARE_V_26_6, + DCP_FIRMWARE_V_26_6_DESKTOP }; struct apple_dcp { bool crashed, external, external_native, quiescing; unsigned int nr_typec_routes; @@ -1685,14 +1686,19 @@ def vrr(root): struct platform_device { struct apple_dcp *data; }; struct drm_crtc { struct platform_device *dcp; void *primary; }; struct drm_display_mode { int unused; }; -struct drm_crtc_state { bool enable, vrr_enabled, modeset, color_mgmt_changed; struct drm_display_mode mode; void *ctm, *degamma_lut, *gamma_lut; unsigned plane_mask; }; +struct drm_crtc_state { bool enable, vrr_enabled, modeset, mode_changed, color_mgmt_changed; struct drm_display_mode mode; void *ctm, *degamma_lut, *gamma_lut; unsigned plane_mask; }; /* mainline 7.2 renamed struct drm_atomic_state to struct drm_atomic_commit */ -struct drm_atomic_commit { struct drm_crtc_state crtc; }; +struct drm_atomic_commit { struct drm_crtc_state crtc, old_crtc; bool no_old_crtc; }; #define to_apple_crtc(x) (x) #define platform_get_drvdata(x) ((x)->data) static struct drm_crtc_state *drm_atomic_get_new_crtc_state( struct drm_atomic_commit *s, struct drm_crtc *c) { (void)c; return &s->crtc; } +/* Only the desktop 26.6 firmware compares against the old CRTC state. */ +static unsigned int old_state_lookups; +static struct drm_crtc_state * __attribute__((unused)) drm_atomic_get_old_crtc_state( + struct drm_atomic_commit *s, struct drm_crtc *c) +{ (void)c; old_state_lookups++; return s->no_old_crtc ? NULL : &s->old_crtc; } static bool drm_atomic_crtc_needs_modeset(struct drm_crtc_state *s) { return s->modeset; } static int __attribute__((unused)) iomfb_v14_7_atomic_check(struct apple_dcp *d, @@ -1753,14 +1759,45 @@ def vrr(root): } cases++; } - dcp.crashed = true; + /* No other firmware turns a VRR change into a mode set. */ + assert(!old_state_lookups && !state.crtc.mode_changed); +DESKTOP_VRR dcp.crashed = true; state.crtc.vrr_enabled = false; assert(dcp_crtc_atomic_check(&crtc, &state) == -EINVAL); printf("PASS: %u production atomic VRR admission cases and crashed-device rejection\n", cases); return 0; } """ - return prefix + check + suffix + desktop = '' + if 'DCP_FIRMWARE_V_26_6_DESKTOP' in check: + # Desktop 26.6 applies adaptive sync with a mode set: a VRR_ENABLED + # change of an enabled CRTC must become one, nothing else may. + desktop = r""" dcp.fw_compat = DCP_FIRMWARE_V_26_6_DESKTOP; + dcp.hw.iomfb_method_profile = 0; + dcp.mode_state.valid = true; + dcp.connector = &connector; + connector.connected = true; + state.crtc.modeset = false; + for (unsigned int enable = 0; enable < 2; enable++) + for (unsigned int old_vrr = 0; old_vrr < 3; old_vrr++) + for (unsigned int vrr = 0; vrr < 2; vrr++) { + state.crtc.enable = enable; + state.crtc.vrr_enabled = vrr; + state.crtc.mode_changed = false; + state.no_old_crtc = old_vrr == 2; + state.old_crtc.vrr_enabled = old_vrr == 1; + assert(dcp_crtc_atomic_check(&crtc, &state) == 0); + bool expected = enable && old_vrr != 2 && (old_vrr == 1) != vrr; + if (state.crtc.mode_changed != expected) { + fprintf(stderr, "desktop enable=%u old_vrr=%u vrr=%u: mode_changed=%d expected %d\n", + enable, old_vrr, vrr, state.crtc.mode_changed, expected); + return 1; + } + cases++; + } + dcp.fw_compat = DCP_FIRMWARE_UNKNOWN; +""" + return prefix + check + suffix.replace('DESKTOP_VRR', desktop, 1) def power_reply(root): diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py b/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py index 466688aff3638..c307c9e51ff28 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py @@ -694,12 +694,9 @@ def publication(root): #define DCP_FIRMWARE_V_14_7 (DCP_FIRMWARE_H17P + 1) #define DCP_FIRMWARE_V_14_7_TEMPLATE (DCP_FIRMWARE_H17P + 2) #endif -/* Nor the desktop 26.6 firmware, the one that withholds VRR modes. */ +/* Nor the desktop 26.6 firmware. */ #define DCP_FIRMWARE_V_26_6_DESKTOP (DCP_FIRMWARE_H17P + 3) #define DIV_ROUND_UP(n,d) (((n)+(d)-1)/(d)) -/* This fixture selects an external H17P sink, never desktop adaptive sync. */ -static bool dcp_desktop_vrr_supported(struct apple_dcp *d) -{ assert(d->fw_compat != DCP_FIRMWARE_V_26_6_DESKTOP); return false; } static void dcp_update_connector_dimensions(struct apple_dcp *d,struct drm_connector *c) { (void)c;assert(d->fw_compat != DCP_FIRMWARE_V_26_6_DESKTOP); } '''+firmware_lineage(root)+internal_inline(root,'dcp_fw_is_14_7')+r'''/* Off T6030 machines the direct crossbar output is not clocked separately. */ @@ -723,7 +720,9 @@ def publication(root): + ''.join(apple_function(root, name) for name in ('dcp_queue_hotplug', 'dcp_modes_for_connector', 'apple_connector_invalidate_edid', 'apple_connector_edid_set_live')) + mode_helpers) - publication = block(iomfb,'static void dcp_clear_connector_edid(')+'\n'+block(iomfb,'static void dcp_consume_edid_invalidation(')+mode_helpers+block(iomfb,'static bool dcp_mode_vrr_supported(')+'\n'+block(iomfb,'int dcp_get_modes(')+block(iomfb,'bool lookup_mode(') + # The desktop 26.6 VRR filter is gone once that firmware takes adaptive sync. + vrr_filter = block(iomfb,'static bool dcp_mode_vrr_supported(')+'\n' if 'static bool dcp_mode_vrr_supported(' in iomfb else '' + publication = block(iomfb,'static void dcp_clear_connector_edid(')+'\n'+block(iomfb,'static void dcp_consume_edid_invalidation(')+mode_helpers+vrr_filter+block(iomfb,'int dcp_get_modes(')+block(iomfb,re.search(r'^[^\n;(]*\blookup_mode\(',iomfb,re.M).start()) # Drive malformed replacements through the same chunk/end ingress as firmware. # The dictionary cache is a boundary substitute and takes ownership of the blob. receipt = r''' From 3fb694ddf0ded4d39c649d6b01fd8155a739372b Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Wed, 28 Oct 2026 12:31:40 +1100 Subject: [PATCH 386/388] selftests: apple-dcp: follow #451's desktop adaptive sync in batch5 The batch5 merge takes #451's desktop 26.6 adaptive sync and removes the J616s series' parameter sequence (dcp_modeset_sync(), its fixed-refresh fallback on a rejected VRR mode set) and its admission gate (dcp_desktop_vrr_check(), dcp_desktop_vrr_supported()). Retire panel-adaptive-fallback, which drove only that removed sequence; vrr-rejection now pins #451's VRR_ENABLED-to-mode-set admission. Drop the stubs other fixtures kept for the removed functions (desktop-unknown-modeset-recovery, vrr-rejection), which no longer compile as unused. In the publication fixture, take lookup_mode() by its definition: the J616s takeover check starts a line with a lookup_mode() call, which the line-start pattern matched first. panel-adaptive-watchdog is unchanged: the fixed-refresh latch after an adaptive swap times out remains. Assisted-by: LLM Signed-off-by: Ryan Murray --- .../drivers/gpu/apple-dcp/desktop.py | 5 +- .../drivers/gpu/apple-dcp/display.py | 122 ------------------ .../drivers/gpu/apple-dcp/lifecycle.py | 3 - .../selftests/drivers/gpu/apple-dcp/rate.py | 2 +- .../selftests/drivers/gpu/apple-dcp/run.py | 1 - 5 files changed, 2 insertions(+), 131 deletions(-) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py index 0b00a6a81d33f..b38fb5e951997 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py @@ -329,12 +329,9 @@ def modeset_unknown(root): template = (root / 'drivers/gpu/drm/apple/iomfb_template.c').read_text() unit = modeset_unmap(root) helper = block(template, 'static bool dcp_modeset_callback_failed(') - # Keep the real modeset completion and its callback-error check. The - # adaptive retry boundary must never be reached after an unknown callback. + # Keep the real modeset completion and its callback-error check. helper += r''' struct dcp_modeset_cookie {struct dcp_wait_cookie wait;bool vrr;}; -static void dcp_modeset_fixed_fallback(struct apple_dcp *d,struct dcp_modeset_cookie *m) -{(void)d;(void)m;assert(false);} ''' + block(template, 'static void dcp_modeset_actual_done(') unit = unit.replace('int main(void)', helper + '\nint main(void)') tests = r''' diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/display.py b/tools/testing/selftests/drivers/gpu/apple-dcp/display.py index 1236325213994..83f29c1ad7be1 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/display.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/display.py @@ -195,128 +195,6 @@ def refresh(root): ''' -def adaptive(root): - source=(root/'drivers/gpu/drm/apple/iomfb_template.c').read_text() - funcs='\n'.join(block(source, marker) for marker in ( - 'static bool dcp_modeset_callback_failed(', - 'static void dcp_modeset_fixed_fallback(', - 'static void dcp_modeset_actual_done(', - 'static void dcp_modeset_after_actual_info(', - 'static void dcp_modeset_after_bootstrap(', - 'static void dcp_modeset_after_bootstrap_info(', - 'static void dcp_modeset_select(', - 'static void dcp_modeset_sync_done(', - 'static void dcp_modeset_sync(')) - cookie=block(source,'struct dcp_modeset_cookie {')+';\n' - main=(root/'drivers/gpu/drm/apple/iomfb.c').read_text() - gate=block(main,'bool dcp_desktop_vrr_supported(') + block(main,'int dcp_desktop_vrr_check(') - return r''' -#include -#include -#include -#include -#include -typedef uint32_t u32; -typedef uint64_t u64; -#define READ_ONCE(x) (x) -#define WRITE_ONCE(x,v) ((x)=(v)) -#define DCP_FIRMWARE_V_26_6_DESKTOP 5 -#define IOMFBPARAM_ADAPTIVE_SYNC 14 -#define dev_warn(...) ((void)0) -struct dcp_wait_cookie { u32 status; bool done,replied; }; -struct dcp_set_digital_out_mode_req {u32 color_mode_id,timing_mode_id;}; -struct dcp_set_parameter_dcp { u32 param; u64 value[4]; u32 count; }; -struct apple_dcp {void *dev;int fw_compat;bool main_display,has_panel;struct {bool has_vrr;} panel;struct {bool vrr_failed,modeset_callback_error;} *desktop;}; -struct drm_display_mode {int id;}; -struct drm_crtc_state {bool vrr_enabled;struct drm_display_mode mode;}; -struct dcp_display_mode {bool vrr;}; -static bool lookup_mode(struct apple_dcp *d,struct drm_display_mode *m,struct dcp_display_mode *out) -{out->vrr=m->id==120;return m->id==60 || m->id==120;} -static bool dcp_has_panel(struct apple_dcp *d) {return d->has_panel;} -static u32 get_unaligned_le32(const void *p) {u32 v;memcpy(&v,p,4);return v;} -typedef void (*callback)(struct apple_dcp *,void *,void *); -static callback pending; -static void *cookie_pending; -static unsigned kind, parameters,infos,modes; -static struct dcp_set_parameter_dcp parameter; -static struct dcp_set_digital_out_mode_req requested; -static void dcp_modeset_select(struct apple_dcp *,void *,void *); -static void dcp_modeset_sync(struct apple_dcp *,void *); -static void dcp_set_parameter_dcp(struct apple_dcp *d,bool oob, - struct dcp_set_parameter_dcp *p,callback cb,void *c) -{assert(!pending && !oob);parameter=*p;pending=cb;cookie_pending=c;kind=1;parameters++;} -static void dcp_get_mode_info(struct apple_dcp *d,const struct dcp_set_digital_out_mode_req *m,callback cb,void *c) -{assert(!pending);requested=*m;pending=cb;cookie_pending=c;kind=2;infos++;} -static void dcp_set_digital_out_mode(struct apple_dcp *d,bool oob, - struct dcp_set_digital_out_mode_req *m,callback cb,void *c) -{assert(!pending && !oob);requested=*m;pending=cb;cookie_pending=c;kind=3;modes++;} -static void complete_set_digital_out_mode(struct apple_dcp *d,void *p,void *c) -{struct dcp_wait_cookie *w=c;w->done=true;w->replied=!!p;if(p)w->status=get_unaligned_le32(p);} -''' + cookie + gate + funcs + r''' -static void reply(struct apple_dcp *d,unsigned expected,u32 *status) -{assert(kind==expected && pending);callback cb=pending;void *c=cookie_pending;pending=NULL;cb(d,status,c);} -static void initialize(struct dcp_modeset_cookie *m) -{memset(m,0,sizeof(*m));m->vrr=true;m->min_vrr=24U<<16;m->actual=(struct dcp_set_digital_out_mode_req){11,12};m->bootstrap=(struct dcp_set_digital_out_mode_req){13,12};} -static void selected(struct apple_dcp *d,struct dcp_modeset_cookie *m) -{u32 zero=0;reply(d,2,&zero);assert(requested.color_mode_id==11);reply(d,3,&zero);assert(m->wait.done && m->wait.replied && !m->wait.status);} -int main(void) -{ - struct apple_dcp d={.fw_compat=DCP_FIRMWARE_V_26_6_DESKTOP,.main_display=true,.has_panel=true}; - __typeof__(*d.desktop) desktop={0};d.desktop=&desktop; - struct drm_crtc_state state={.vrr_enabled=true,.mode={120}}; - assert(dcp_desktop_vrr_check(&d,&state)==-EINVAL); - assert(!dcp_desktop_vrr_supported(&d));d.panel.has_vrr=true; - assert(!dcp_desktop_vrr_check(&d,&state)); - state.mode.id=60;assert(dcp_desktop_vrr_check(&d,&state)==-EINVAL); - state.vrr_enabled=false;assert(!dcp_desktop_vrr_check(&d,&state)); - state.vrr_enabled=true;state.mode.id=120; - assert(dcp_desktop_vrr_supported(&d)); - d.main_display=false;assert(!dcp_desktop_vrr_supported(&d));d.main_display=true; - d.has_panel=false;assert(!dcp_desktop_vrr_supported(&d));d.has_panel=true; - d.fw_compat=3;assert(!dcp_desktop_vrr_supported(&d));d.fw_compat=DCP_FIRMWARE_V_26_6_DESKTOP; - struct dcp_modeset_cookie m;u32 zero=0,error=1; - initialize(&m);dcp_modeset_sync(&d,&m); - assert(parameter.param==14 && parameter.count==3 && parameter.value[0]==24U<<16); - assert(!parameter.value[1] && !parameter.value[2] && parameter.value[3]==0xaaaaaaaaaaaaaaaaULL); - reply(&d,1,&zero);selected(&d,&m);assert(m.vrr && !desktop.vrr_failed); - /* Rejected enable is cleared before a fixed timing is selected. */ - initialize(&m);dcp_modeset_sync(&d,&m);reply(&d,1,&error); - assert(desktop.vrr_failed && !dcp_desktop_vrr_supported(&d) && !m.vrr && !parameter.value[0]); - assert(!dcp_desktop_vrr_check(&d,&state)); /* Recovery re-applies fixed refresh. */ - reply(&d,1,&zero);selected(&d,&m); - /* A rejected modeset gets one fixed-refresh retry. */ - desktop.vrr_failed=false;initialize(&m);dcp_modeset_sync(&d,&m); - reply(&d,1,&zero);reply(&d,2,&zero);reply(&d,3,&error); - assert(!m.vrr && desktop.vrr_failed && kind==1 && !parameter.value[0]); - reply(&d,1,&zero);selected(&d,&m); - /* Failed disable and absent replies must stop, without claiming success. */ - initialize(&m);dcp_modeset_sync(&d,&m);reply(&d,1,&error);reply(&d,1,&error); - assert(m.wait.done && m.wait.status && !pending); - initialize(&m);dcp_modeset_sync(&d,&m);reply(&d,1,NULL); - assert(m.wait.done && !m.wait.replied && !pending); - /* Bootstrap keeps the DCP's actual color/timing identities. */ - desktop.vrr_failed=false;initialize(&m);m.use_bootstrap=true; - dcp_modeset_sync(&d,&m);reply(&d,1,&zero); - assert(requested.color_mode_id==13 && requested.timing_mode_id==12); - reply(&d,2,&zero);reply(&d,3,&zero);selected(&d,&m); - /* Unsupported callbacks at every chained RPC stop after its real ACK. */ - for(unsigned phase=0;phase<5;phase++) { - desktop.vrr_failed=false;desktop.modeset_callback_error=false; - initialize(&m);m.use_bootstrap=true;dcp_modeset_sync(&d,&m); - if(phase>0)reply(&d,1,&zero); - if(phase>1)reply(&d,2,&zero); - if(phase>2)reply(&d,3,&zero); - if(phase>3)reply(&d,2,&zero); - desktop.modeset_callback_error=true; - reply(&d,kind,&zero); - assert(m.wait.done && !m.wait.replied && !pending && !desktop.vrr_failed); - } - assert(parameters && infos && modes); - return 0; -} -''' - - def takeover(root): prefix=backlight(root).split('static void completed(')[0] prefix=prefix.replace('static inline ', 'static inline __attribute__((unused)) ') diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py index 24f95a4453053..c8d1c230fa329 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py @@ -1719,9 +1719,6 @@ def vrr(root): { (void)blob; assert(!"gamma LUT without a colour-management change"); return 0; } static unsigned int __attribute__((unused)) dcp_gamma_lut_size(struct platform_device *p) { (void)p; assert(!"gamma LUT without a colour-management change"); return 0; } -/* Desktop admission is covered by panel-adaptive-fallback. */ -static int dcp_desktop_vrr_check(struct apple_dcp *d, struct drm_crtc_state *s) -{ (void)d; (void)s; return 0; } /* Route-follow is for Type-C displays; this DCP has no Type-C routes. */ static bool __attribute__((unused)) typec_follow_crtc = true; static int __attribute__((unused)) dcp_typec_follow_check(struct apple_dcp *d, struct drm_crtc *c, diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py b/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py index c307c9e51ff28..86e7a1fd44e61 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py @@ -722,7 +722,7 @@ def publication(root): + mode_helpers) # The desktop 26.6 VRR filter is gone once that firmware takes adaptive sync. vrr_filter = block(iomfb,'static bool dcp_mode_vrr_supported(')+'\n' if 'static bool dcp_mode_vrr_supported(' in iomfb else '' - publication = block(iomfb,'static void dcp_clear_connector_edid(')+'\n'+block(iomfb,'static void dcp_consume_edid_invalidation(')+mode_helpers+vrr_filter+block(iomfb,'int dcp_get_modes(')+block(iomfb,re.search(r'^[^\n;(]*\blookup_mode\(',iomfb,re.M).start()) + publication = block(iomfb,'static void dcp_clear_connector_edid(')+'\n'+block(iomfb,'static void dcp_consume_edid_invalidation(')+mode_helpers+vrr_filter+block(iomfb,'int dcp_get_modes(')+block(iomfb,'bool lookup_mode(') # Drive malformed replacements through the same chunk/end ingress as firmware. # The dictionary cache is a boundary substitute and takes ownership of the blob. receipt = r''' diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/run.py b/tools/testing/selftests/drivers/gpu/apple-dcp/run.py index b78b93def55b2..efc79d1345f7a 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/run.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/run.py @@ -70,7 +70,6 @@ def main(): ('display', 'desktop-powerlog-framing', desktop.powerlog_framing, ((),), ()), ('display', 'desktop-brightness-keys-reports', desktop.keys_and_reports, ((),), ()), ('display', 'panel-refresh-range', display.refresh, ((),), ()), - ('display', 'panel-adaptive-fallback', display.adaptive, ((),), ()), ('display', 'panel-adaptive-watchdog', display.watchdog, ((),), ()), ('m4', 'm4-wire-golden', lambda r: m4.golden_check(r, output), (), ()), ('m4', 'm4-protocol', m4.protocol, ((),), ('-Wno-unused-function', '-Wno-unused-const-variable')), From 8f421b0716acfeeab173c4391109b2d8ffadac85 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Wed, 28 Oct 2026 12:50:29 +1100 Subject: [PATCH 387/388] drm/apple: Derive an EDID for T6041 external displays The external display processors of the M4 Pro/Max (apple,t6041-dcpext, desktop 26.6 IOMFB) announce no dcpav-service-epic, so the EDID read through the DCPAV service never runs and every connector they drive has an empty EDID. Userspace then names the monitor "Unknown". The firmware still exports the TimingElements and DisplayAttributes of each sink it drives, including the sink's ProductAttributes. Build the base EDID block from them as the T8140 (H17P) external path already does: enable the firmware metadata for the desktop external processors, check the property extent and parse the sink identity from DisplayAttributes, and read the IsPreferred, IsInterlaced, SyncPolarity and PixelRepetition keys of each timing so the preferred timing can be described. The published modes keep their flags; only the EDID's detailed timing takes the polarity. These processors have no cable generation of their own (their HDMI output has no Type-C session), so count sinks instead: the first is the one found at boot, and each cable attach or detach, Type-C route change or HDMI unplug starts a new one, which an earlier sink's metadata can never match. A firmware HPD loss alone does not drop it, as the link can bounce without the sink being described again. The H17P external, the other DCPs and the internal panels are unchanged. The firmware 27 path of t6050/dcp gets its EDID through the DCPAV service and its display product query only identifies an Apple display, so there was nothing there to port to 26.6. Assisted-by: LLM Signed-off-by: Ryan Murray (cherry picked from commit 221e2ac5125ba243ccfae1a2751c11063e1dd851) --- drivers/gpu/drm/apple/dcp-internal.h | 7 ++ drivers/gpu/drm/apple/dcp.c | 53 ++++++++- drivers/gpu/drm/apple/iomfb.c | 1 + drivers/gpu/drm/apple/iomfb_template.c | 7 +- drivers/gpu/drm/apple/parser-test.c | 104 ++++++++++++++++++ drivers/gpu/drm/apple/parser.c | 25 +++-- drivers/gpu/drm/apple/parser.h | 8 ++ .../selftests/drivers/gpu/apple-dcp/rate.py | 5 +- 8 files changed, 197 insertions(+), 13 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index ded53559c0882..da06e5e21eab5 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -217,6 +217,12 @@ struct apple_dcp_hw_data { * (the T8122 dcpext), see dcp_fw_is_14_7(). */ bool fw_14_7_endpoints; + /* + * An external processor with no DCPAV service to read the EDID + * through, whose sinks' EDID is built from their firmware metadata + * (desktop 26.x), see dcp_sink_metadata(). + */ + bool sink_metadata_edid; }; enum dcp_test_point { @@ -808,6 +814,7 @@ static inline bool dcp_fail_stop(const struct apple_dcp *dcp) iomfb_is_m4(dcp); } +bool dcp_firmware_metadata_enabled(struct apple_dcp *dcp); void dcp_firmware_metadata_connection(struct apple_dcp *dcp, u64 generation, bool connected); void dcp_firmware_metadata_hotplug(struct apple_dcp *dcp, bool connected); void dcp_firmware_metadata_announced(struct apple_dcp *dcp); diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 44daeb982ba5c..b6af560ffc50a 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -556,9 +556,39 @@ void dcp_drm_crtc_page_flip(struct apple_dcp *dcp, ktime_t now) spin_unlock_irqrestore(&crtc->base.dev->event_lock, flags); } -static bool dcp_firmware_metadata_enabled(struct apple_dcp *dcp) +/* + * A desktop 26.x external processor has no dcpav-service-epic to read the + * sink's EDID through, but exports the TimingElements and DisplayAttributes + * of every sink it drives, on HDMI and Type-C alike. + */ +static bool dcp_sink_metadata(struct apple_dcp *dcp) { - return dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL; + return dcp->hw.sink_metadata_edid; +} + +bool dcp_firmware_metadata_enabled(struct apple_dcp *dcp) +{ + return dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL || + dcp_sink_metadata(dcp); +} + +/* + * Such metadata belongs to the sink the firmware reports, as these + * processors have no cable generation of their own: each sink change starts + * a new connection, which the metadata of an earlier sink can never match. + */ +static void dcp_desktop_metadata_new_sink(struct apple_dcp *dcp) +{ + unsigned long flags; + + spin_lock_irqsave(&dcp->metadata.lock, flags); + dcp->metadata.attributes_valid = false; + dcp->metadata.timing_valid = false; + dcp->metadata.epoch = (struct dcp_metadata_epoch) { + .generation = dcp->metadata.epoch.generation + 1, + .connected = true, + }; + spin_unlock_irqrestore(&dcp->metadata.lock, flags); } void dcp_firmware_metadata_connection(struct apple_dcp *dcp, u64 generation, bool connected) @@ -567,6 +597,10 @@ void dcp_firmware_metadata_connection(struct apple_dcp *dcp, u64 generation, boo if (!dcp_firmware_metadata_enabled(dcp)) return; + if (dcp_sink_metadata(dcp)) { + dcp_desktop_metadata_new_sink(dcp); + return; + } spin_lock_irqsave(&dcp->metadata.lock, flags); if (generation != dcp->metadata.epoch.generation || !connected) { dcp->rate_proof.connection_sequence++; @@ -639,6 +673,12 @@ void dcp_firmware_metadata_hotplug(struct apple_dcp *dcp, bool connected) if (!dcp_firmware_metadata_enabled(dcp)) return; + /* + * A desktop 26.x link can bounce without the firmware describing its + * sink again: the cable and HDMI HPD paths report a sink change. + */ + if (dcp_sink_metadata(dcp)) + return; spin_lock_irqsave(&dcp->metadata.lock, flags); if (connected) { dcp_firmware_metadata_present_locked(dcp); @@ -729,6 +769,8 @@ void dcp_firmware_metadata_timing(struct apple_dcp *dcp) !dcp->modes[i].metadata_timing_valid) continue; dcp->metadata.preferred = dcp->modes[i].mode; + /* Desktop 26.x publishes its modes without their polarity. */ + dcp->metadata.preferred.flags |= dcp->modes[i].metadata_sync_flags; dcp->metadata.timing_ticket = dcp->chunks.metadata_ticket; dcp->metadata.timing_valid = true; break; @@ -2890,6 +2932,9 @@ static void disconnected_hpd_event(struct apple_connector *con) struct apple_dcp *dcp = platform_get_drvdata(pdev); WRITE_ONCE(dcp->ext_backlight, false); + /* Nothing else reports an HDMI unplug to the firmware metadata. */ + if (dcp_sink_metadata(dcp)) + dcp_desktop_metadata_new_sink(dcp); } /* May run with DRM modeset locks held; defer property/cache cleanup. */ @@ -5536,6 +5581,9 @@ static int dcp_platform_probe_pipe(struct platform_device *pdev) mutex_init(&dcp->dcpavserv.lock); INIT_WORK(&dcp->dcpavserv.close_wq, dcpavserv_close_work); dcp->hw = *(struct apple_dcp_hw_data *)of_device_get_match_data(dev); + /* The display such a processor finds at boot is its first sink. */ + if (dcp_sink_metadata(dcp)) + dcp_desktop_metadata_new_sink(dcp); if (dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL) { if (!dcp->hw.firmware_clock || @@ -6435,6 +6483,7 @@ static const struct apple_dcp_hw_data apple_dcp_hw_t6041 = { static const struct apple_dcp_hw_data apple_dcp_hw_t6041_dcpext = { .num_dptx_ports = 2, + .sink_metadata_edid = true, }; static const struct of_device_id of_match[] = { diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index 1b2f30b2260d1..0d266130531ed 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -1740,6 +1740,7 @@ int dcp_modes_replace(struct apple_dcp *dcp, struct dcp_parse_ctx *ctx, ret = parse_array_count(ctx, &entries); if (ret) return ret; + ctx->metadata_timing = dcp_firmware_metadata_enabled(dcp); } ctx->zero_vrr_range_fallback = dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP; modes = enumerate_modes(ctx, &nr_modes, dcp->width_mm, dcp->height_mm, diff --git a/drivers/gpu/drm/apple/iomfb_template.c b/drivers/gpu/drm/apple/iomfb_template.c index ef72f496531d3..9d248cccf72da 100644 --- a/drivers/gpu/drm/apple/iomfb_template.c +++ b/drivers/gpu/drm/apple/iomfb_template.c @@ -1264,7 +1264,7 @@ static bool dcpep_process_chunks(struct apple_dcp *dcp, } /* Validate serialized extent outside the metadata spinlock. */ - if (dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL) + if (dcp_firmware_metadata_enabled(dcp)) dcp->chunks.payload_complete = dcp->chunks.contiguous && dcp_property_complete(dcp->chunks.data, dcp->chunks.received, dcp->chunks.length); @@ -1329,10 +1329,13 @@ static bool dcpep_process_chunks(struct apple_dcp *dcp, return false; } - if (dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL) { + if (dcp_firmware_metadata_enabled(dcp)) { struct dcp_display_metadata metadata; ret = parse_display_metadata(&metadata_ctx, &metadata); + if (ret) + dev_dbg(dcp->dev, "DisplayAttributes without sink identity: %d\n", + ret); dcp_firmware_metadata_attributes(dcp, ret ? NULL : &metadata); } else if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && dcp_has_panel(dcp)) { diff --git a/drivers/gpu/drm/apple/parser-test.c b/drivers/gpu/drm/apple/parser-test.c index dca33251ad5ed..dc807a2013a43 100644 --- a/drivers/gpu/drm/apple/parser-test.c +++ b/drivers/gpu/drm/apple/parser-test.c @@ -8,6 +8,7 @@ #include #include "dcp.h" +#include #include #include "connector.h" @@ -1531,6 +1532,108 @@ static void parser_panel_j616s_publication(struct kunit *test) dcp_modes_release(dcp); } +static void blob_timing_dimension(struct parser_blob *blob, const char *key, + s64 active, s64 total, s64 porch, s64 sync, s64 rate) +{ + blob_key(blob, key); + blob_tag(blob, 1, 7); + blob_int(blob, "Active", active); + blob_int(blob, "Total", total); + blob_int(blob, "FrontPorch", porch); + blob_int(blob, "SyncWidth", sync); + blob_int(blob, "PreciseSyncRate", rate); + blob_int(blob, "SyncPolarity", 1); + blob_int(blob, "PixelRepetition", 0); +} + +/* A desktop 26.x sink timing: the keys the firmware-derived EDID needs. */ +static struct parser_blob *make_timing_blob(struct kunit *test) +{ + struct parser_blob *blob = kunit_kzalloc(test, sizeof(*blob), GFP_KERNEL); + + if (!blob) + return NULL; + put_unaligned_le32(0xd3, blob->data); + blob->size = 4; + blob_tag(blob, 2, 1); + blob_tag(blob, 1, 8); + blob_bool(blob, "IsVirtual", false); + blob_int(blob, "ID", 7); + blob_int(blob, "Score", 10); + blob_timing_dimension(blob, "HorizontalAttributes", 1920, 2200, 88, 44, + (675U << 16) / 10); + blob_timing_dimension(blob, "VerticalAttributes", 1080, 1125, 4, 5, 60 << 16); + blob_key(blob, "ColorModes"); + blob_tag(blob, 2, 1); + blob_color(blob, 100, 10, 0, DCP_COLOR_FORMAT_RGB); + blob_bool(blob, "IsPreferred", true); + blob_bool(blob, "IsInterlaced", false); + return blob; +} + +static void parser_desktop_metadata_timing(struct kunit *test) +{ + const struct dcp_display_metadata metadata = { + .width_cm = 60, .height_cm = 33, + .chromaticity = { { 44608, 20288 }, { 16960, 40448 }, { 9344, 5184 } }, + .white = { 20544, 21568 }, .gamma = 144179, + .product = 2895, .serial = 305419896, .week = 7, .year = 2013, + .manufacturer = "ACR", .name = "QG270 S3", .serial_text = "ANON000000001", + }; + struct parser_blob *blob = make_timing_blob(test); + struct drm_display_mode preferred; + struct dcp_display_mode *modes; + const struct drm_edid *edid; + const struct edid *raw; + struct dcp_parse_ctx ctx; + unsigned int count; + + KUNIT_ASSERT_NOT_NULL(test, blob); + + /* Other DCPs keep their modes and give no metadata timing. */ + KUNIT_ASSERT_EQ(test, parse(blob->data, blob->size, &ctx), 0); + ctx.dcp = &parser_dcp; + modes = enumerate_modes(&ctx, &count, 600, 330, 0, false, false); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, modes); + KUNIT_ASSERT_EQ(test, count, 1U); + KUNIT_EXPECT_TRUE(test, modes[0].preferred); + KUNIT_EXPECT_FALSE(test, modes[0].metadata_timing_valid); + KUNIT_EXPECT_EQ(test, modes[0].metadata_sync_flags, 0U); + KUNIT_EXPECT_EQ(test, modes[0].mode.flags, 0U); + kfree(modes); + + KUNIT_ASSERT_EQ(test, parse(blob->data, blob->size, &ctx), 0); + ctx.dcp = &parser_dcp; + ctx.metadata_timing = true; + modes = enumerate_modes(&ctx, &count, 600, 330, 0, false, false); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, modes); + KUNIT_ASSERT_EQ(test, count, 1U); + KUNIT_EXPECT_TRUE(test, modes[0].preferred); + KUNIT_EXPECT_TRUE(test, modes[0].mode.type & DRM_MODE_TYPE_PREFERRED); + KUNIT_EXPECT_TRUE(test, modes[0].metadata_timing_valid); + KUNIT_EXPECT_EQ(test, modes[0].metadata_sync_flags, + (u32)(DRM_MODE_FLAG_PHSYNC | DRM_MODE_FLAG_PVSYNC)); + /* The published mode is unchanged: only the EDID timing takes the polarity. */ + KUNIT_EXPECT_EQ(test, modes[0].mode.flags, 0U); + KUNIT_EXPECT_EQ(test, modes[0].mode.clock, 148500); + + preferred = modes[0].mode; + preferred.flags |= modes[0].metadata_sync_flags; + kfree(modes); + edid = dcp_metadata_edid(&metadata, &preferred); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, edid); + raw = drm_edid_raw(edid); + KUNIT_EXPECT_EQ(test, be16_to_cpu(raw->product_id.manufacturer_name), + (u16)(('A' - '@') << 10 | ('C' - '@') << 5 | ('R' - '@'))); + KUNIT_EXPECT_EQ(test, le16_to_cpu(raw->product_id.product_code), 2895); + KUNIT_EXPECT_EQ(test, raw->detailed_timings[1].data.other_data.type, + EDID_DETAIL_MONITOR_NAME); + KUNIT_EXPECT_MEMEQ(test, raw->detailed_timings[1].data.other_data.data.str.str, + "QG270 S3\n", 9); + KUNIT_EXPECT_EQ(test, le16_to_cpu(raw->detailed_timings[0].pixel_clock), 14850); + drm_edid_free(edid); +} + static struct kunit_case parser_cases[] = { KUNIT_CASE(parser_panel_of_dimensions), KUNIT_CASE(parser_panel_j616s_publication), @@ -1566,6 +1669,7 @@ static struct kunit_case parser_cases[] = { KUNIT_CASE(parser_dimensions_lifetime), KUNIT_CASE(parser_catalog_release), KUNIT_CASE(parser_property_bounds), + KUNIT_CASE(parser_desktop_metadata_timing), {} }; diff --git a/drivers/gpu/drm/apple/parser.c b/drivers/gpu/drm/apple/parser.c index 33344a5f8b10b..e5a8c8e6cc700 100644 --- a/drivers/gpu/drm/apple/parser.c +++ b/drivers/gpu/drm/apple/parser.c @@ -734,6 +734,7 @@ static int parse_mode(struct dcp_parse_ctx *handle, bool preferred = false, interlaced = false; bool have_preferred = false, have_interlaced = false; bool is_preferred = false; + bool timing_keys = external_sdr || handle->metadata_timing; struct drm_display_mode *mode = &out->mode; *out = (struct dcp_display_mode) { @@ -775,11 +776,11 @@ static int parse_mode(struct dcp_parse_ctx *handle, else if (!strcmp(key, "HorizontalAttributes")) { duplicate_scope |= have_horiz; have_horiz = true; - ret = parse_dimension(it.handle, &horiz, external_sdr || internal); + ret = parse_dimension(it.handle, &horiz, timing_keys || internal); } else if (!strcmp(key, "VerticalAttributes")) { duplicate_scope |= have_vert; have_vert = true; - ret = parse_dimension(it.handle, &vert, external_sdr || internal); + ret = parse_dimension(it.handle, &vert, timing_keys || internal); } else if (!strcmp(key, "MinimumVariableRefreshRate")) { vrr_ambiguous |= !!(vrr_seen & 1); vrr_seen |= 1; @@ -798,13 +799,16 @@ static int parse_mode(struct dcp_parse_ctx *handle, duplicate_scope |= have_virtual; have_virtual = true; ret = parse_bool(it.handle, &is_virtual); - } else if (external_sdr && !strcmp(key, "IsPreferred")) { + } else if (timing_keys && !strcmp(key, "IsPreferred")) { if (have_preferred) ret = -EINVAL; else ret = parse_bool(it.handle, &preferred); have_preferred = true; - } else if (external_sdr && !strcmp(key, "IsInterlaced")) { + /* Outside the external SDR path it also ranks the timing. */ + if (!external_sdr) + is_preferred = preferred; + } else if (timing_keys && !strcmp(key, "IsInterlaced")) { if (have_interlaced) ret = -EINVAL; else @@ -914,11 +918,16 @@ static int parse_mode(struct dcp_parse_ctx *handle, .height_mm = height_mm, }; - if ((external_sdr || internal) && horiz.sync_polarity >= 0 && + /* The internal panel also publishes its polarity with its modes (J616s series). */ + if ((timing_keys || internal) && horiz.sync_polarity >= 0 && vert.sync_polarity >= 0) - mode->flags = (horiz.sync_polarity ? DRM_MODE_FLAG_PHSYNC : DRM_MODE_FLAG_NHSYNC) | - (vert.sync_polarity ? DRM_MODE_FLAG_PVSYNC : DRM_MODE_FLAG_NVSYNC); - out->metadata_timing_valid = external_sdr && !out->automatic_rgb8_quirk && + out->metadata_sync_flags = + (horiz.sync_polarity ? DRM_MODE_FLAG_PHSYNC : DRM_MODE_FLAG_NHSYNC) | + (vert.sync_polarity ? DRM_MODE_FLAG_PVSYNC : DRM_MODE_FLAG_NVSYNC); + /* The external SDR path and the internal panel publish it with their modes. */ + if (external_sdr || internal) + mode->flags = out->metadata_sync_flags; + out->metadata_timing_valid = timing_keys && !out->automatic_rgb8_quirk && have_preferred && preferred && have_interlaced && !interlaced && horiz.pixel_repetition == 0 && vert.pixel_repetition == 0 && horiz.sync_polarity >= 0 && vert.sync_polarity >= 0; diff --git a/drivers/gpu/drm/apple/parser.h b/drivers/gpu/drm/apple/parser.h index 9b419ce83621b..5e573371e0072 100644 --- a/drivers/gpu/drm/apple/parser.h +++ b/drivers/gpu/drm/apple/parser.h @@ -27,6 +27,12 @@ struct dcp_parse_ctx { bool ambiguous_180hz; /* Desktop 26.6 uses zero placeholders for an unreported VRR range. */ bool zero_vrr_range_fallback; + /* + * A desktop 26.x external processor: read the sink timing keys that + * describe the preferred timing for the firmware-derived EDID, without + * the H17P external color policy. + */ + bool metadata_timing; #if IS_ENABLED(CONFIG_DRM_APPLE_DCP_T6030_AUDIO) /* * T6030 external processor audio elements (DCP firmware 14.7, external @@ -132,6 +138,8 @@ struct dcp_display_mode { bool preferred; bool metadata_timing_valid; bool automatic_rgb8_quirk; + /* The sync polarity flags of the timing, if it reports them */ + u32 metadata_sync_flags; /* * The best virtual color element of the timing, which the desktop * 26.x firmware selects first to bootstrap its generic pipe (valid if diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py b/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py index 86e7a1fd44e61..24335a890408b 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py @@ -371,7 +371,10 @@ def case(value, automatic=True, internal=False, external=True, admitted=True): callback_types = '\n'.join(block(dptx, marker)+' __packed;' for marker in ('struct dptxport_apcall_link_rate {', 'struct dptxport_apcall_set_active_lane_count {')) callback = block(dptx, 'static int dptxport_call_link_config(') dcp = (base/'dcp.c').read_text() - connection = '\n'.join(block(dcp, marker) for marker in ( + # The modelled DCP is the H17P external one, never a desktop 26.x processor. + connection = 'static bool dcp_sink_metadata(struct apple_dcp *d) { (void)d; return false; }\n' + \ + 'static void dcp_desktop_metadata_new_sink(struct apple_dcp *d) { (void)d; assert(0); }\n' + \ + '\n'.join(block(dcp, marker) for marker in ( 'void dcp_firmware_metadata_connection(', 'static void dcp_firmware_metadata_present_locked(', 'static void dcp_announce_kick(', 'void dcp_firmware_metadata_announced(', 'void dcp_firmware_metadata_hotplug(')) From ec622e2b4acf49dc15b4c793044f1dda96680671 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Wed, 28 Oct 2026 15:05:46 +1100 Subject: [PATCH 388/388] selftests: apple-dcp: follow the J616s DP Alt Mode reconcile in tipd fixtures #519 makes the CD321x update publish the DisplayPort mode the firmware entered as a partner altmode, and drop it with the partner. The probe now takes the connector from a "displayport" reference when the port has one. The tipd host fixtures did not know either, so 24 of them stopped building. - lifecycle: carry partner_altmode_dp and the DP mode VDO, and count partner altmode registration against removal. - quiesce and the transaction fixtures built on it: carry partner_altmode_dp. They never enter DP on the partner, so the altmode helpers assert they are not reached. - pm: report a "displayport" reference, which the probe fixtures already resolve, and assert the port-node fallback is not taken. No assertion is relaxed. apple-dcp host selftests: 80/80. Assisted-by: LLM Signed-off-by: Ryan Murray Co-Authored-By: Claude Opus 5.5 --- .../drivers/gpu/apple-dcp/lifecycle.py | 19 +++++++++++++++++-- .../selftests/drivers/gpu/apple-dcp/pm.py | 2 ++ .../drivers/gpu/apple-dcp/quiesce.py | 5 ++++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py index 9907b055ab75d..f27c5b243d32f 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py @@ -192,8 +192,9 @@ def function(file, name): struct cd321x_status { u32 status, data_status, status_changed, data_status_changed, pwr_status; struct usb_pd_identity partner_identity; bool dp_sid_event,dp_sid_hpd_low,dp_sid_irq; - struct { u32 status_rx,configure; } dp_sid_status; }; -struct tps6598x { void *partner, *dev, *port, *psy; int lock; u32 data_status; enum usb_role *role_sw; }; + struct { u32 status_rx,configure,mode_data; } dp_sid_status; }; +struct tps6598x { void *partner, *dev, *port, *psy; int lock; u32 data_status; enum usb_role *role_sw; + void *partner_altmode_dp; }; struct cd321x { struct tps6598x tps; struct cd321x_status update_status; struct cd321x_pm_state pm; struct delayed_work update_work, resume_work; struct typec_mux_state state; bool state_valid, display_route_active, cur_partner_is_pd; @@ -236,6 +237,20 @@ def function(file, name): static void *typec_register_partner(void *p, struct typec_partner_desc *d) { (void)p; (void)d; return (void *)1; } static void typec_partner_set_identity(void *p) { (void)p; } +#ifndef USB_TYPEC_DP_SID +#define USB_TYPEC_DP_SID 0xff01 +#endif +#ifndef USB_TYPEC_DP_MODE +#define USB_TYPEC_DP_MODE 1 +#endif +struct typec_altmode_desc { unsigned short svid; unsigned char mode; u32 vdo; bool mode_selection; }; +/* The partner DP altmode the firmware entered: registered and dropped in pairs. */ +static int partner_altmodes; +static void *typec_partner_register_altmode(void *p, const struct typec_altmode_desc *d) +{ assert(p && d->svid == USB_TYPEC_DP_SID); partner_altmodes++; return &partner_altmodes; } +static void typec_altmode_update_active(void *a, bool active) { assert(a); (void)active; } +static void __attribute__((unused)) typec_unregister_altmode(void *a) +{ if (a == &partner_altmodes) { assert(partner_altmodes > 0); partner_altmodes--; } } #define typec_set_pwr_opmode(...) ((void)0) #define typec_set_pwr_role(...) ((void)0) #define typec_set_vconn_role(...) ((void)0) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py b/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py index abf1afd88e24c..3c2f2b8df2bd5 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py @@ -513,6 +513,8 @@ def function(text, name): static struct fwnode_handle *fwnode_find_reference(struct fwnode_handle *f,const char *s,int n) { (void)f;(void)s;(void)n;connector.refs++;return &connector; } +static bool fwnode_property_present(struct fwnode_handle *f,const char *s) { (void)f;(void)s;return true; } +static struct fwnode_handle *fwnode_handle_get(struct fwnode_handle *f) { assert(!"connector is a displayport reference");return f; } static void __attribute__((unused)) fwnode_handle_put(struct fwnode_handle *f) { if(f){assert(f->refs>0);f->refs--;} } diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/quiesce.py b/tools/testing/selftests/drivers/gpu/apple-dcp/quiesce.py index 30eb0a183d6b7..aadd700a40097 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/quiesce.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/quiesce.py @@ -93,7 +93,7 @@ def make_unit(source, parent=False, reference=None): struct tps6598x; struct tipd_data { REMOVE_RETURN (*remove)(struct tps6598x *); void (*unregister_port)(struct tps6598x *); int (*reset)(struct tps6598x *); }; struct typec_port { struct typec_mux *mux; }; -struct tps6598x { bool remove_safe_prepared; struct typec_mux *mux; void *partner,*dev,*psy,*role_sw,*reset; int irq,wq_poll; struct typec_port *port; struct tipd_data *data; }; +struct tps6598x { bool remove_safe_prepared; struct typec_mux *mux; void *partner,*dev,*psy,*role_sw,*reset,*partner_altmode_dp; int irq,wq_poll; struct typec_port *port; struct tipd_data *data; }; struct cd321x { struct tps6598x tps; struct typec_mux *mux; void *tbt_switch; }; struct typec_thunderbolt_switch_data { int state; }; static void cd321x_stop_work(struct cd321x *c) { (void)c;event('W'); } @@ -101,6 +101,9 @@ def make_unit(source, parent=False, reference=None): static int usb_role_switch_set_role(void *p,int r) { (void)p;assert(r==USB_ROLE_NONE);if(ordinary_port&&!role_error){int ret=ordinary_power_off();if(ret)return ret;controller_active=false;phy_off=true;}event('R');role_calls++;return role_error; } static enum usb_role __attribute__((unused)) usb_role_switch_get_role(void *p) { (void)p;return controller_active?USB_ROLE_HOST:USB_ROLE_NONE; } static void typec_unregister_partner(void *p) { (void)p;event('P'); } +/* These fixtures never enter DP on the partner, so its altmode stays NULL. */ +static void typec_altmode_update_active(void *a,bool v) { (void)a;(void)v;assert(!"no partner DP altmode here"); } +static void typec_unregister_altmode(void *a) { (void)a;assert(!"no partner DP altmode here"); } #define typec_set_pwr_opmode(...) ((void)0) #define typec_set_pwr_role(...) ((void)0) #define typec_set_vconn_role(...) ((void)0)