From cde8dcbfc7ce6bda3a9d010457c009a757c65e91 Mon Sep 17 00:00:00 2001 From: Jonathan Goyvaerts Date: Tue, 27 Oct 2026 16:25:57 -0300 Subject: [PATCH 01/40] drm/asahi: lock only the VM reservation for read-only VM queries Submit validation (covers_ranges_batch and friends), translate_offset, vm_context_mask, new_execution_context and capacity only read VmInner, but took the full GPUVM exec lock: the VM reservation plus every external BO's reservation. Each submit therefore cost O(external BOs). Under virtio-gpu every guest BO is external (libkrun maps blobs through dma-buf export), so a game with ~15k BOs spent 12-20 ms per submit in drm_gpuvm_prepare_objects and was capped at single-digit fps. Every page-table and mapping change already holds the VM reservation, so readers need only that lock; use GpuVm::lock_inner() for them. Signed-off-by: Jonathan Goyvaerts Assisted-by: LLM --- drivers/gpu/drm/asahi/mmu.rs | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/drivers/gpu/drm/asahi/mmu.rs b/drivers/gpu/drm/asahi/mmu.rs index 3522010949e35..8599b424745a8 100644 --- a/drivers/gpu/drm/asahi/mmu.rs +++ b/drivers/gpu/drm/asahi/mmu.rs @@ -1305,9 +1305,7 @@ impl KernelMapping { return Err(EINVAL); } let address = self.iova().checked_add(offset as u64).ok_or(EOVERFLOW)?; - lock_vm(&self.0.owner, None, false)? - .page_table - .translate_iova(address) + self.0.owner.lock_inner().page_table.translate_iova(address) } /// Writes a userspace timestamp through the retained GEM mapping. Aligned results @@ -2250,9 +2248,7 @@ impl Vm { } pub(crate) fn covers_range(&self, address: u64, size: u64, read: bool, write: bool) -> bool { - let Ok(mut inner) = lock_vm(&self.gpuvm, None, false) else { - return false; - }; + let mut inner = self.gpuvm.lock_inner(); Self::covers_range_locked(&mut inner, address, size, read, write) } @@ -2267,9 +2263,7 @@ impl Vm { if stride <= block_size { return false; } - let Ok(mut inner) = lock_vm(&self.gpuvm, None, false) else { - return false; - }; + let mut inner = self.gpuvm.lock_inner(); addresses.iter().all(|&address| { let Some(guard) = address.checked_add(block_size) else { return false; @@ -2279,12 +2273,12 @@ impl Vm { }) } - /// Checks all exact request tuples under one GPUVM exec lock. Access bit0 + /// Checks all exact request tuples under one hold of the VM reservation. Access bit0 /// requests reading and bit1 writing. The epoch names this same snapshot. pub(crate) fn covers_ranges_batch(&self, ranges: &[(u64, u64, u8)]) -> (bool, Option) { - let Ok(mut inner) = lock_vm(&self.gpuvm, None, false) else { - return (false, None); - }; + // Every mapping change holds the VM reservation, so readers need only that one. The full + // exec lock would also lock every external BO, making each submit O(external BOs). + let mut inner = self.gpuvm.lock_inner(); let epoch = inner.epoch; let covered = ranges.iter().all(|&(address, size, access)| { Self::covers_range_locked(&mut inner, address, size, access & 1 != 0, access & 2 != 0) @@ -3136,7 +3130,7 @@ impl Uat { return Err(EINVAL); } let root = { - let owner = lock_vm(&vm.gpuvm, None, false)?; + let owner = vm.gpuvm.lock_inner(); if owner.is_kernel || !Arc::ptr_eq(&owner.uat_inner, &self.inner) { return Err(EINVAL); } @@ -3281,7 +3275,7 @@ impl Uat { return Err(EINVAL); } { - let owner = lock_vm(&vm.gpuvm, None, false)?; + let owner = vm.gpuvm.lock_inner(); if owner.is_kernel || !core::ptr::eq(Arc::as_ptr(&owner.uat_inner), Arc::as_ptr(&self.inner)) { @@ -3663,7 +3657,7 @@ impl Vm { /// Diagnostic reservation occupancy (including guards), not a placement promise. /// Removal can progress between queries; allocations are excluded by the VM lock. pub(crate) fn capacity(&self, range: Range) -> Result<(u64, u64)> { - let inner = lock_vm(&self.gpuvm, None, false)?; + let inner = self.gpuvm.lock_inner(); let mut next = range.start; let mut used = 0; let mut largest = 0; From f3cc4a691438be2e067a4035241ee028370ad1fd Mon Sep 17 00:00:00 2001 From: Jonathan Goyvaerts Date: Tue, 27 Oct 2026 16:25:57 -0300 Subject: [PATCH 02/40] drm/asahi: lock only the VM reservation for VA updates Every map and unmap (user VM_BIND, kernel mappings such as each G17 submission's completion record, object teardown) took GpuVm::exec_lock(), i.e. drm_gpuvm_exec_lock(): the VM reservation plus the reservation of every external object. Under virtio-gpu every guest BO is external, so with ~15k BOs each bind and each submit spent around a millisecond in drm_gpuvm_prepare_objects(). This VM is IMMEDIATE_MODE and RESV_PROTECTED: VA updates need the VM reservation and each object's gpuva lock, which the link and unlink paths take themselves. Backing is pinned and its segments copied before any reservation is taken, no caller adds fences, and the driver already unmaps under the VM reservation alone on its low-memory path. Add GpuVm::exec_lock_vm(), which runs the drm_exec loop over the VM reservation (and an external obj, if given) without drm_gpuvm_prepare_objects(), and use it for all VA updates. Signed-off-by: Jonathan Goyvaerts Assisted-by: LLM --- drivers/gpu/drm/asahi/mmu.rs | 12 ++--- drivers/gpu/drm/asahi/mmu/bind.rs | 4 +- rust/helpers/drm_gpuvm.c | 6 +++ rust/kernel/drm/gpuvm/mod.rs | 81 +++++++++++++++++++++++++++++-- 4 files changed, 91 insertions(+), 12 deletions(-) diff --git a/drivers/gpu/drm/asahi/mmu.rs b/drivers/gpu/drm/asahi/mmu.rs index 8599b424745a8..c4afbc0127d70 100644 --- a/drivers/gpu/drm/asahi/mmu.rs +++ b/drivers/gpu/drm/asahi/mmu.rs @@ -2381,7 +2381,7 @@ impl Vm { let size = object_range.range(); let mut vm_exec = unsafe { - self.gpuvm.exec_lock(/*Some(gem)*/ None, false)? + self.gpuvm.exec_lock_vm(/*Some(gem)*/ None, false)? }; let inner = vm_exec.unique_gpuvm.data(); @@ -2461,7 +2461,7 @@ impl Vm { // SAFETY: no other UniqRefGpuVm exists so `GpuVm::exec_lock()` is safe let mut vm_exec = unsafe { self.gpuvm - .exec_lock(/*Some(&gem)*/ None, false) + .exec_lock_vm(/*Some(&gem)*/ None, false) .inspect_err(|_| { dev_err!(self.dev, "map_at(): Failed to exec_lock gpuvm\n"); })? @@ -2549,7 +2549,7 @@ impl Vm { let mut ctx = StepContext::new(Some(device.as_ref().as_ref()), prot, &mut resources)?; let mut vm_exec = unsafe { - self.gpuvm.exec_lock(/*Some(&gem)*/ None, false)? + self.gpuvm.exec_lock_vm(/*Some(&gem)*/ None, false)? }; let inner = vm_exec.unique_gpuvm.data(); @@ -2620,7 +2620,7 @@ impl Vm { size: usize, prot: Prot, ) -> Result { - let mut vm_exec = unsafe { self.gpuvm.exec_lock(None, false)? }; + let mut vm_exec = unsafe { self.gpuvm.exec_lock_vm(None, false)? }; if (iova as usize | phys | size) & UAT_PGMSK != 0 { dev_err!( @@ -2724,7 +2724,7 @@ impl Vm { let mut resources = VmOpResources::for_unmap()?; let mut ctx = StepContext::new(dev, Prot::default(), &mut resources)?; - let mut vm_exec = unsafe { self.gpuvm.exec_lock(None, false)? }; + let mut vm_exec = unsafe { self.gpuvm.exec_lock_vm(None, false)? }; mod_dev_dbg!(self.dev, "MMU: sm_unmap: {:#x}:{:#x}\n", iova, size); let _commit = if user { @@ -2755,7 +2755,7 @@ impl Vm { let mut ctx = StepContext::new(None, Prot::default(), &mut resources)?; // SAFETY: See `lock_vm()`. - let mut vm_exec = match unsafe { self.gpuvm.exec_lock(None, false) } { + let mut vm_exec = match unsafe { self.gpuvm.exec_lock_vm(None, false) } { Err(error) if error == ENOMEM => return self.drop_mappings_low_memory(gem), result => result?, }; diff --git a/drivers/gpu/drm/asahi/mmu/bind.rs b/drivers/gpu/drm/asahi/mmu/bind.rs index 0b3d85077aacb..0fa3f304af1d4 100644 --- a/drivers/gpu/drm/asahi/mmu/bind.rs +++ b/drivers/gpu/drm/asahi/mmu/bind.rs @@ -132,7 +132,7 @@ impl Vm { PreparedUserBindOp::Map(map) => { // SAFETY: See `lock_vm()`. As in `Vm::bind_object()`, the object itself is // not locked: the map step may still populate its pages. - let mut inner = unsafe { self.gpuvm.exec_lock(None, true)? }; + let mut inner = unsafe { self.gpuvm.exec_lock_vm(None, true)? }; match self.mapping_commit() { Ok(commit) => { let (flags, repeat) = if map.single_page { @@ -177,7 +177,7 @@ impl Vm { } PreparedUserBindOp::Unmap(unmap) => { // SAFETY: See `lock_vm()`. - let mut inner = unsafe { self.gpuvm.exec_lock(None, true)? }; + let mut inner = unsafe { self.gpuvm.exec_lock_vm(None, true)? }; match self.mapping_commit() { Ok(commit) => { let mut ctx = diff --git a/rust/helpers/drm_gpuvm.c b/rust/helpers/drm_gpuvm.c index 30fb1777fbf2b..d42d3d3491006 100644 --- a/rust/helpers/drm_gpuvm.c +++ b/rust/helpers/drm_gpuvm.c @@ -23,6 +23,12 @@ bool rust_helper_drm_gpuvm_is_extobj(struct drm_gpuvm *gpuvm, return drm_gpuvm_is_extobj(gpuvm, obj); } +__rust_helper +bool rust_helper_drm_exec_is_contended(struct drm_exec *exec) +{ + return drm_exec_is_contended(exec); +} + __rust_helper void rust_helper_drm_gpuvm_exec_unlock(struct drm_gpuvm_exec *vm_exec) { diff --git a/rust/kernel/drm/gpuvm/mod.rs b/rust/kernel/drm/gpuvm/mod.rs index 146f3c9ed8362..d873eb4ef6384 100644 --- a/rust/kernel/drm/gpuvm/mod.rs +++ b/rust/kernel/drm/gpuvm/mod.rs @@ -60,6 +60,43 @@ pub use self::vm_bo::*; mod va; pub use self::va::*; +/// `drm_gpuvm_exec_lock()` without `drm_gpuvm_prepare_objects()`: the +/// `drm_exec_until_all_locked()` loop over the VM reservation and `vm_exec.extra`. +/// +/// # Safety +/// +/// `vm_exec` must be initialized as for `drm_gpuvm_exec_lock()`. +unsafe fn exec_lock_vm_raw(vm_exec: &mut bindings::drm_gpuvm_exec) -> core::ffi::c_int { + let exec = &raw mut vm_exec.exec; + // SAFETY: Per the safety requirements, `exec` belongs to a valid `drm_gpuvm_exec`. + unsafe { bindings::drm_exec_init(exec, vm_exec.flags, 0) }; + let mut ret = 0; + // SAFETY (whole loop): `exec` was initialized above and `vm_exec.vm` is a valid GPUVM. + // `drm_exec_cleanup()` drops all locks and restarts after contention, as the C macros do. + while unsafe { bindings::drm_exec_cleanup(exec) } { + ret = unsafe { bindings::drm_gpuvm_prepare_vm(vm_exec.vm, exec, vm_exec.num_fences) }; + if unsafe { bindings::drm_exec_is_contended(exec) } { + continue; + } + if ret == 0 { + if let Some(extra) = vm_exec.extra.fn_ { + ret = unsafe { extra(vm_exec) }; + if unsafe { bindings::drm_exec_is_contended(exec) } { + continue; + } + } + } + if ret != 0 { + break; + } + } + if ret != 0 { + // SAFETY: Finalize the context initialized above, as `drm_gpuvm_exec_lock()` does on error. + unsafe { bindings::drm_exec_fini(exec) }; + } + ret +} + pub(super) unsafe extern "C" fn exec_lock_gem_object( vm_exec: *mut bindings::drm_gpuvm_exec, ) -> core::ffi::c_int { @@ -260,6 +297,36 @@ impl GpuVm { &'a self, obj: Option<&T::Object>, interruptible: bool, + ) -> Result> { + // SAFETY: Same contract as this function. + unsafe { self.exec_lock_impl(obj, interruptible, true) } + } + + /// HACK: added for the asahi downstream driver. Like [`GpuVm::exec_lock`], but locks only + /// the VM's shared reservation (and `obj` if it is external), not every external object. + /// + /// Enough for VA updates of this immediate-mode, `DRM_GPUVM_RESV_PROTECTED` VM: they need + /// the VM reservation and each object's gpuva lock, which the link and unlink paths take. + /// Callers must not need external objects' reservations (fences, validation, eviction). + /// Locking all of them made each update cost O(external objects). + /// + /// # Safety + /// + /// Same as [`GpuVm::exec_lock`]. + pub unsafe fn exec_lock_vm<'a>( + &'a self, + obj: Option<&T::Object>, + interruptible: bool, + ) -> Result> { + // SAFETY: Same contract as this function. + unsafe { self.exec_lock_impl(obj, interruptible, false) } + } + + unsafe fn exec_lock_impl<'a>( + &'a self, + obj: Option<&T::Object>, + interruptible: bool, + all_objects: bool, ) -> Result> { // Do not try to lock the object if it is internal (since it is already locked). let is_ext = obj.map(|a| self.is_extobj(a)).unwrap_or(false); @@ -286,10 +353,16 @@ impl GpuVm { GFP_KERNEL, )?; - // SAFETY: The object is valid and was initialized above. On failure, - // `drm_gpuvm_exec_lock()` has already finalized the exec context, so the box only needs to - // be freed, which dropping it does. - to_result(unsafe { bindings::drm_gpuvm_exec_lock(&mut *vm_exec) })?; + // SAFETY: The object is valid and was initialized above. On failure, both lock functions + // have already finalized the exec context, so the box only needs to be freed, which + // dropping it does. + to_result(unsafe { + if all_objects { + bindings::drm_gpuvm_exec_lock(&mut *vm_exec) + } else { + exec_lock_vm_raw(&mut *vm_exec) + } + })?; // Build the guard, and take its VM reference, only once the locks are held: its `Drop` // unlocks them. From 925df2acdf61c4e4b21e98c35f0ffaa358eb9cd5 Mon Sep 17 00:00:00 2001 From: Jonathan Goyvaerts Date: Wed, 28 Oct 2026 11:47:53 -0300 Subject: [PATCH 03/40] drm/asahi: g17: stop cancelling behind a failed command once it has retired A queue remembered the earliest failed ordinal of each engine for good, and every later command whose prefix covered it was cancelled. GL orders each submission after every earlier one, so once one command of a queue failed, every later command of that queue was cancelled too. With cancellation no longer reported as a lost device, the client never noticed: after a CS2 GPU hang, one Hyprland render that sampled the game's failed buffer was cancelled, then every later Hyprland render (about 47 a second, all fences ECANCELED), and the screen kept the game's last frame until Hyprland or the machine restarted. New buffers did not help; the queue itself was poisoned. The record exists so that nothing runs before a failed producer the firmware may still own or replay. That holds only while the command keeps its VM pin: complete() and fail_unpublished() release it, fail_while_owned() keeps it until release_after_retirement(). Track the failed commands that still hold their pin and stop counting each one once it has released it. A failure that cannot be tracked for lack of memory keeps cancelling for good, as before. Signed-off-by: Jonathan Goyvaerts Assisted-by: LLM --- drivers/gpu/drm/asahi/g17/completion.rs | 5 ++ drivers/gpu/drm/asahi/g17/dependency.rs | 107 ++++++++++++++++++++++++ drivers/gpu/drm/asahi/g17/job.rs | 9 +- drivers/gpu/drm/asahi/g17/queue/mod.rs | 28 +++---- 4 files changed, 133 insertions(+), 16 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/completion.rs b/drivers/gpu/drm/asahi/g17/completion.rs index 02e7bbc5c33dc..362cf507e1575 100644 --- a/drivers/gpu/drm/asahi/g17/completion.rs +++ b/drivers/gpu/drm/asahi/g17/completion.rs @@ -606,6 +606,11 @@ impl Completion { drop(lease); } + /// Whether the command still holds its VM pin: firmware may still own or replay it. + pub(crate) fn retains_vm(&self) -> bool { + self.vm_job.lock().is_some() + } + fn release_healthy(&self) { if let Some(sources) = &self.sources { sources.release(); } drop(self.render.lock().take()); diff --git a/drivers/gpu/drm/asahi/g17/dependency.rs b/drivers/gpu/drm/asahi/g17/dependency.rs index 0bc1d5024566e..387ce9ba2fb3c 100644 --- a/drivers/gpu/drm/asahi/g17/dependency.rs +++ b/drivers/gpu/drm/asahi/g17/dependency.rs @@ -25,6 +25,48 @@ pub(crate) fn replay_status(status: i32) -> Result { else { Ok(()) } } +/// Whether a failed command may still run or write: it holds its VM pin until it has physically +/// retired, or was never published. +pub(crate) trait Retention { + fn retained(&self) -> bool; +} + +/// The failed ordinals of one engine that later commands must not bypass. A command whose prefix +/// covers one is cancelled, so none runs before a failed producer the firmware may still replay. +/// Once that producer has retired nothing of it can run or write any more, so it stops counting: +/// otherwise one cancelled command cancelled every later command of its queue for good. +pub(crate) struct FailureRecord { + retained: KVec<(u64, T)>, + /// A failure that could not be tracked: it keeps cancelling for good, as all failures once did. + untracked: Option<(u64, T)>, +} + +impl FailureRecord { + pub(crate) const fn new() -> Self { + Self { retained: KVec::new(), untracked: None } + } + + /// A finished frontier entry, at pruning. + pub(crate) fn note(&mut self, sequence: u64, status: i32, entry: &T) { + if status >= 0 || !entry.retained() { + return; + } + if self.retained.push((sequence, entry.clone()), GFP_KERNEL).is_err() + && self.untracked.as_ref().is_none_or(|(first, _)| sequence < *first) + { + self.untracked = Some((sequence, entry.clone())); + } + } + + /// The earliest failure later prefixes must not bypass. + pub(crate) fn earliest(&mut self) -> Option<(u64, &T)> { + self.retained.retain(|(_, entry)| entry.retained()); + Iterator::chain(self.retained.iter(), self.untracked.iter()) + .min_by_key(|(sequence, _)| *sequence) + .map(|(sequence, entry)| (*sequence, entry)) + } +} + pub(crate) struct Frontier { dependencies: [KickDependency; KICK_BARRIERS_MAX], sequences: [u64; KICK_BARRIERS_MAX], @@ -89,3 +131,68 @@ impl Frontier { self.insert(sequence, dependency) } } + +#[kunit_tests(asahi_g17_dependency)] +mod tests { + use super::*; + + #[derive(Clone)] + struct Probe(kernel::sync::Arc); + + impl Probe { + fn new(retained: bool) -> Self { + Self(kernel::sync::Arc::new(core::sync::atomic::AtomicBool::new(retained), GFP_KERNEL).unwrap()) + } + fn retire(&self) { + self.0.store(false, core::sync::atomic::Ordering::Relaxed); + } + } + + impl Retention for Probe { + fn retained(&self) -> bool { + self.0.load(core::sync::atomic::Ordering::Relaxed) + } + } + + fn earliest(record: &mut FailureRecord) -> Option { + record.earliest().map(|(sequence, _)| sequence) + } + + #[test] + fn a_cancelled_command_does_not_poison_its_queue() { + // Cancelled or failed before publication: it released its pin and can never run. + let mut record = FailureRecord::new(); + record.note(5, ECANCELED.to_errno(), &Probe::new(false)); + assert_eq!(earliest(&mut record), None); + } + + #[test] + fn a_retained_failure_cancels_until_it_retires() { + let mut record = FailureRecord::new(); + let failed = Probe::new(true); + record.note(5, EIO.to_errno(), &failed); + assert_eq!(earliest(&mut record), Some(5)); + failed.retire(); + assert_eq!(earliest(&mut record), None); + } + + #[test] + fn a_later_retained_failure_survives_an_earlier_retirement() { + let mut record = FailureRecord::new(); + let (first, second) = (Probe::new(true), Probe::new(true)); + record.note(5, EIO.to_errno(), &first); + record.note(9, ECANCELED.to_errno(), &second); + assert_eq!(earliest(&mut record), Some(5)); + first.retire(); + assert_eq!(earliest(&mut record), Some(9)); + second.retire(); + assert_eq!(earliest(&mut record), None); + } + + #[test] + fn successes_are_not_recorded() { + let mut record = FailureRecord::new(); + record.note(5, 1, &Probe::new(true)); + assert_eq!(earliest(&mut record), None); + } +} diff --git a/drivers/gpu/drm/asahi/g17/job.rs b/drivers/gpu/drm/asahi/g17/job.rs index ad9adcecf0f81..0c940f74213b0 100644 --- a/drivers/gpu/drm/asahi/g17/job.rs +++ b/drivers/gpu/drm/asahi/g17/job.rs @@ -401,6 +401,13 @@ pub(crate) struct Fences { pub(crate) ready: Fence, pub(crate) completed: Fence, pub(crate) publication: Option>, + pub(crate) owner: Arc, +} + +impl super::dependency::Retention for Fences { + fn retained(&self) -> bool { + self.owner.retains_vm() + } } impl Fences { @@ -479,6 +486,6 @@ impl Scheduler { self.backend.count_backlog(&packet); } job.push(); - Ok(Fences { ready, completed, publication: host_publication }) + Ok(Fences { ready, completed, publication: host_publication, owner: packet.completion.clone() }) } } diff --git a/drivers/gpu/drm/asahi/g17/queue/mod.rs b/drivers/gpu/drm/asahi/g17/queue/mod.rs index d22c0f617f374..f6637f2858c26 100644 --- a/drivers/gpu/drm/asahi/g17/queue/mod.rs +++ b/drivers/gpu/drm/asahi/g17/queue/mod.rs @@ -10,6 +10,7 @@ use super::{ command::{Parser, UscWindow, Validated}, completion::{Completion, Destinations, PreparedPublication}, context::Context, + dependency::FailureRecord, fence::Outputs, job::{self, Backend, Engine, Fences, Order, Packet, Scheduler}, validation::Proof, @@ -98,7 +99,7 @@ pub(crate) struct Queue { /// 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], + failures: [FailureRecord; 2], // Closing the userspace queue releases admission even if firmware still // retains one of its execution contexts or installed physical graphs. _slot: admission::QueueSlot, @@ -137,7 +138,7 @@ impl Queue { sequence: [0; 2], frontiers: core::array::from_fn(|_| KVec::new()), finished: KVec::new(), - failures: [None, None], + failures: [FailureRecord::new(), FailureRecord::new()], _slot: slot, }) } @@ -172,11 +173,7 @@ impl Queue { 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) { - *failure = Some((*sequence, fences.completed.clone())); - } + failure.note(*sequence, status, fences); false }); } @@ -224,9 +221,9 @@ impl Queue { !Order::contains(order.wait_through[1], *sequence) || fences.ready.raw() != fences.completed.raw() || fences.status() > 0) - && self.failures.iter().enumerate().all(|(index, failure)| - failure.as_ref().is_none_or(|(sequence, _)| - !Order::contains(order.wait_through[index], *sequence))) + && self.failures.iter_mut().enumerate().all(|(index, failure)| + failure.earliest().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()); @@ -284,11 +281,12 @@ impl Queue { .iter() .filter(|(_, fences)| fences.ready.raw() == fences.completed.raw()) .count(); - let failures: [Option<&Fence>; 2] = core::array::from_fn(|index| { - self.failures[index].as_ref().and_then(|(sequence, fence)| { - Order::contains(order.wait_through[index], *sequence).then_some(fence) - }) - }); + let mut failures: [Option; 2] = [None, None]; + for (index, failure) in self.failures.iter_mut().enumerate() { + failures[index] = failure.earliest().and_then(|(sequence, fences)| { + Order::contains(order.wait_through[index], sequence).then(|| fences.completed.clone()) + }); + } let failure_count = failures.iter().flatten().count(); let mut dependencies = KVec::with_capacity(inputs.len() + count + timestamp_count + failure_count, GFP_KERNEL)?; From 2519646a8827e0964ee771ff927f18629a68d9ad Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Thu, 29 Oct 2026 04:00:00 +0000 Subject: [PATCH 04/40] tools: aurora-sep: share the SELinux device type and let fprintd read the lid The fedora-aurora package builds a policy module of the same name, aurora_sep, but labels /dev/sep-bio aurora_sep_device_t where this tree says sep_bio_device_t. Only one module of a name is active per priority, so a Mac that moved from this tree's aurora-sep-selinux to the package logged "Context sep_bio_device_t became invalid" and was left with an unlabelled /dev/sep-bio, which fprintd may not open. Use the package's name. It follows the reference policy habit of prefixing a module's types with the module name. The package's module also has two rules this one lacks. libfprint's aurora build hides the reader while the lid is closed by following SW_LID on the input event devices. Under the targeted policy fprintd may not read those, logs "Cannot open /dev/input/eventN: Permission denied" and keeps offering Touch ID in clamshell. dev_read_input(fprintd_t) allows the read; the fprintd unit drop-in that libfprint ships already allows char-input for the device cgroup. The package also lets init_t open, read and query the device, for the readiness helpers its units run. Take both, so the two modules are the same policy. The README says what the input rule is for and that a module loaded by hand with semodule -i sits at priority 400 and shadows a packaged one. Bump the Fedora spec to 1-10 for the policy change, and the Arch pkgrel to 15 for the README checksum, since 1-9 and 14 went to the boot policy change. Built with the Fedora 44 selinux-policy-devel Makefile. Assisted-by: LLM Signed-off-by: Ryan Murray Co-Authored-By: Claude Opus 5.5 (cherry picked from commit 7cc30f35ecea3e90ceedd44babbde128c1d487a6) --- tools/aurora-sep/README.md | 11 ++++++++ tools/aurora-sep/aurora_sep.fc | 2 +- tools/aurora-sep/aurora_sep.te | 26 +++++++++++++++---- .../packaging/arch/aurora-sep/PKGBUILD | 4 +-- .../packaging/fedora/aurora-sep.spec | 10 +++++-- 5 files changed, 43 insertions(+), 10 deletions(-) diff --git a/tools/aurora-sep/README.md b/tools/aurora-sep/README.md index 6c490233fc42b..c220e3ee95a3b 100644 --- a/tools/aurora-sep/README.md +++ b/tools/aurora-sep/README.md @@ -162,6 +162,17 @@ semodule -i aurora_sep.pp restorecon /dev/sep-bio ``` +The module also lets fprintd read input event devices. libfprint follows the +lid through their SW_LID switch to hide the reader while the lid is closed; +without it fprintd logs "Permission denied" for `/dev/input/event*` and keeps +offering Touch ID in clamshell. The device type, `aurora_sep_device_t`, is the +one the distribution packages use for the same module, so moving between +them keeps `/dev/sep-bio` labelled. + +`semodule -i` installs at priority 400, above the packages' 200, so a module +loaded by hand keeps winning after a package is installed. Remove it then with +`semodule -X 400 -r aurora_sep`. + On Omarchy, run `omarchy-apply-lock` once after enrollment. The Quickshell lock screen then selects `omarchy-lock-fingerprint` automatically and accepts Touch ID through fprintd. Hyprlock's separate fingerprint switch is not used diff --git a/tools/aurora-sep/aurora_sep.fc b/tools/aurora-sep/aurora_sep.fc index c0ef756351d51..669583254a5c0 100644 --- a/tools/aurora-sep/aurora_sep.fc +++ b/tools/aurora-sep/aurora_sep.fc @@ -1 +1 @@ -/dev/sep-bio -c gen_context(system_u:object_r:sep_bio_device_t,s0) +/dev/sep-bio -c gen_context(system_u:object_r:aurora_sep_device_t,s0) diff --git a/tools/aurora-sep/aurora_sep.te b/tools/aurora-sep/aurora_sep.te index fd501d6be0fc4..88aded70972f7 100644 --- a/tools/aurora-sep/aurora_sep.te +++ b/tools/aurora-sep/aurora_sep.te @@ -1,24 +1,40 @@ -policy_module(aurora_sep, 1.0.0) +policy_module(aurora_sep, 1.1.0) ######################################## # # Declarations # -# /dev/sep-bio, the apple_sep driver's Touch ID device. -type sep_bio_device_t; -dev_node(sep_bio_device_t) +# /dev/sep-bio, the apple_sep driver's Touch ID device. The distribution +# packages build this module under the same name, so the type name has to +# match theirs: a module that drops the type leaves the node with a context +# the policy no longer knows. +type aurora_sep_device_t; +dev_node(aurora_sep_device_t) ######################################## # # Local policy # +gen_require(` + type init_t; +') + +# The units' helpers activate Touch ID with a read-only open and ask the +# driver about its sensor with an ioctl. Let them where they run as init_t. +allow init_t aurora_sep_device_t:chr_file { getattr open read ioctl }; + # fprintd, through libfprint's aurora driver, is the device's only user. optional_policy(` gen_require(` type fprintd_t; ') - allow fprintd_t sep_bio_device_t:chr_file rw_chr_file_perms; + allow fprintd_t aurora_sep_device_t:chr_file rw_chr_file_perms; + + # libfprint hides the reader while the lid is closed. It follows the + # lid through the SW_LID switch of an input event device, which the + # targeted policy keeps fprintd from reading. + dev_read_input(fprintd_t) ') diff --git a/tools/aurora-sep/packaging/arch/aurora-sep/PKGBUILD b/tools/aurora-sep/packaging/arch/aurora-sep/PKGBUILD index 32d93599340ae..ec3799e213d36 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=14 +pkgrel=15 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) @@ -45,7 +45,7 @@ sha256sums=('5f0193851b017ac59ba7ae15663f2481588b83a04a3f0b9743c327d83cdcd607' '3d1c9c0cab1b090bd6b7d3c4bacaec8719e264afba6b565ada80b1e7914f203a' 'fb58f07738b7654b15173fe0ffce2dff2f14b588d8edd24545e6c2c739605026' '6d96cdd20bc993c00eb0b2d7f77d52f81531c72e9d7141455822690ddec348bb' - 'fdde42bf67667746e9bb5e435746808de49866a0d24b8d59f08f0568fa130a37' + 'fbb61228397e59c088cba1537ce36f09d26f0f6721e887d43f8138dbdeea2598' '6f7b828fb42ebbae011a4d4ee041bf37fc6d68fea1f7c3a643a8259b7fb8705a' '52020e636e62b60f0c3e6251c3b67b8a8c409bff0486bc3463f356fd3073384b') diff --git a/tools/aurora-sep/packaging/fedora/aurora-sep.spec b/tools/aurora-sep/packaging/fedora/aurora-sep.spec index 51b31aefc3065..fc30995ee7f5c 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: 9%{?dist} +Release: 10%{?dist} Summary: Loader and desktop integration for the Apple SEP fingerprint driver License: GPL-2.0-only @@ -63,7 +63,8 @@ Requires(post): selinux-policy-%{selinuxtype} %description selinux Labels /dev/sep-bio, the Apple SEP driver's Touch ID device, and lets fprintd open it. The targeted policy otherwise leaves the device as device_t, which -fprintd may not open, so fprintd lists no fingerprint reader. +fprintd may not open, so fprintd lists no fingerprint reader. It also lets +fprintd read input event devices, for libfprint's lid switch follower. %prep %setup -q -c -T @@ -152,6 +153,11 @@ fi %ghost %verify(not md5 size mode mtime) %{_sharedstatedir}/selinux/%{selinuxtype}/active/modules/200/%{modulename} %changelog +* Fri Oct 30 2026 Ryan Murray - 1-10 +- aurora-sep-selinux: name the device type aurora_sep_device_t, as the + distribution packages do, let fprintd read input devices for the lid + follower and let the readiness helpers open the device as init_t + * Wed Oct 28 2026 Ryan Murray - 1-9 - Start the login screen without waiting for Touch ID, and show it again once Touch ID is ready if nobody has logged in yet From ce4cf9954e81a776b9c18ba985d1cbf5c427afe1 Mon Sep 17 00:00:00 2001 From: Ryan <81541646+RyanTheTide@users.noreply.github.com> Date: Fri, 30 Oct 2026 11:28:39 +1100 Subject: [PATCH 05/40] tools: aurora-sep: fix the Arch checksums of load-driver and XART-SAFETY.md The boot policy change rewrote load-driver and XART-SAFETY.md but left their old sums in the Arch PKGBUILD, so makepkg refuses the sources and tests/test_packaging.py fails on integration/aurora. Use the sums of the files in the tree. Every source sum now matches its file. Assisted-by: LLM Signed-off-by: Ryan Murray Co-Authored-By: Claude Opus 5.5 --- tools/aurora-sep/packaging/arch/aurora-sep/PKGBUILD | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/aurora-sep/packaging/arch/aurora-sep/PKGBUILD b/tools/aurora-sep/packaging/arch/aurora-sep/PKGBUILD index ec3799e213d36..c11412556abb0 100644 --- a/tools/aurora-sep/packaging/arch/aurora-sep/PKGBUILD +++ b/tools/aurora-sep/packaging/arch/aurora-sep/PKGBUILD @@ -34,7 +34,7 @@ source=(load-driver README.md XART-SAFETY.md OS-IDENTITY.md) -sha256sums=('5f0193851b017ac59ba7ae15663f2481588b83a04a3f0b9743c327d83cdcd607' +sha256sums=('129225c2602290221fc38f754c486e1cba8f0b160e56c5aee2446100aa4e8a04' '2f401629733d280a1b8a5a74f5b668d42307f9885f916abf9e377a811e358b9a' 'f8d0c4b9385e6716d5ac5dd9941a57459fbc551a8238383bcde36cd929d0523f' '40f0d21446a85bab3e3800b31342e94e553af0fcde2c985503147519737e011d' @@ -46,7 +46,7 @@ sha256sums=('5f0193851b017ac59ba7ae15663f2481588b83a04a3f0b9743c327d83cdcd607' 'fb58f07738b7654b15173fe0ffce2dff2f14b588d8edd24545e6c2c739605026' '6d96cdd20bc993c00eb0b2d7f77d52f81531c72e9d7141455822690ddec348bb' 'fbb61228397e59c088cba1537ce36f09d26f0f6721e887d43f8138dbdeea2598' - '6f7b828fb42ebbae011a4d4ee041bf37fc6d68fea1f7c3a643a8259b7fb8705a' + '5c799f27a424162200071b4e19aba6fae6bc9d2152a0b5a518512ae12d62104a' '52020e636e62b60f0c3e6251c3b67b8a8c409bff0486bc3463f356fd3073384b') # A unit and the helper it runs ($1, installed as $3), pointed at the From afd51176986a8ad1f786853a2e44e0f728012c0e Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Sat, 24 Oct 2026 23:16:40 +1100 Subject: [PATCH 06/40] 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 (cherry picked from commit d80847118522567a39106da6bc6fb1ba16b57275) Co-Authored-By: Claude Opus 5.5 --- 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 ab30807676fbd5a32d3518e6ab8276c775c04f29 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Sun, 25 Oct 2026 04:55:18 +1100 Subject: [PATCH 07/40] 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 (cherry picked from commit d5ee0bd28d7e75edfb2acceef113138c457e5c76) Co-Authored-By: Claude Opus 5.5 --- 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 24f586cb1c2fb..3f2672f803ce9 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. @@ -416,8 +415,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. @@ -1471,7 +1470,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" @@ -1480,6 +1479,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(); @@ -1748,7 +1750,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 4177ad5f368bdf8391af4b38bb699105c5c3613a Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 14:18:53 +1100 Subject: [PATCH 08/40] 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 (cherry picked from commit e3653e6e1a563902ef2d32e25ac1d6e3b9f69a7d) Co-Authored-By: Claude Opus 5.5 --- .../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 495adc52c7e6e..5b6baf0a14024 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 029d791fd6620..4422527c3ed65 100644 --- a/arch/arm64/configs/asahi.config +++ b/arch/arm64/configs/asahi.config @@ -87,7 +87,6 @@ CONFIG_RTC_DRV_MACSMC=y CONFIG_RTC_DRV_APPLE_PMU=m CONFIG_APPLE_ADMAC=m CONFIG_APPLE_SIO=m -CONFIG_APPLE_SIO_T6030=y CONFIG_MFD_MACSMC=y CONFIG_COMMON_CLK_APPLE_NCO=m CONFIG_APPLE_DART=y 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 488ad2bd59ef8425cf3feb28e0ecacd983f8dab4 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 15:08:26 +1100 Subject: [PATCH 09/40] 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 (cherry picked from commit a2b3221b38cd211d4dc9e67487575e2897ad9e31) Co-Authored-By: Claude Opus 5.5 --- 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 d77b2cf6d82892ad7d11ce4195aecba086224124 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 30 Oct 2026 12:46:32 +1100 Subject: [PATCH 10/40] 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 (cherry picked from commit 1ea4c92a7e4b, rebased onto integration/aurora) Co-Authored-By: Claude Opus 5.5 --- 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 ba84b223e8c8d..d966b42b5cbe6 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -2745,6 +2745,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 14b55156c4d6a64a0db70140ae5852bdd429d907 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 30 Oct 2026 13:10:43 +1100 Subject: [PATCH 11/40] selftests: apple-dcp: follow the PMP auxiliary buffer in ordered-rx #472 added a T6050 PMP registry-v2 path to apple_rtkit_rx_work(): endpoint 7 now goes to apple_rtkit_pmp_aux_rx() when rtk->pmp_registry_v2 is set. The ordered-rx fixture extracts apple_rtkit_rx_work(), but its struct apple_rtkit has no such field and nothing defines the new handler, so the group stopped compiling and the apple-dcp host suite went from 80/80 to 79/80 on integration/aurora. Give the fixture struct apple_rtkit the field and stub the handler the way the CoreAnalytics one is stubbed, as unreachable in this fixture. No assertion changes. The edid group also still passes on the tree before #472, where the stub is unused. Assisted-by: LLM Signed-off-by: Ryan Murray Co-Authored-By: Claude Opus 5.5 --- tools/testing/selftests/drivers/gpu/apple-dcp/edid.py | 2 +- tools/testing/selftests/drivers/gpu/apple-dcp/run.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/edid.py b/tools/testing/selftests/drivers/gpu/apple-dcp/edid.py index 60ed9b365d099..9b949a1a6ff1d 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/edid.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/edid.py @@ -855,7 +855,7 @@ def function(text, signature): }; struct afk_receive_message_work { struct apple_dcp_afkep *ep; u64 message; struct work_struct work; }; struct apple_rtkit_ops { void (*recv_message)(void *,u8,u64); }; -struct apple_rtkit { bool shutting_down, sync_syslog_ack; const struct apple_rtkit_ops *ops; void *cookie,*dev; }; +struct apple_rtkit { bool shutting_down, sync_syslog_ack, pmp_registry_v2; const struct apple_rtkit_ops *ops; void *cookie,*dev; }; struct apple_rtkit_rx_work { struct apple_rtkit *rtk; u8 ep; u64 msg; struct work_struct work; }; #define current ((void *)1) static pthread_mutex_t reply_lock=PTHREAD_MUTEX_INITIALIZER; diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/run.py b/tools/testing/selftests/drivers/gpu/apple-dcp/run.py index efc79d1345f7a..f836743f2fc8d 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/run.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/run.py @@ -156,6 +156,9 @@ def main(): prefix += ('#define APPLE_RTKIT_EP_COREANALYTICS ' + endpoint + '\n' '#define apple_rtkit_coreanalytics_rx(...) ' 'assert(!"CoreAnalytics is outside this ordered-display fixture")\n') + # The T6050 PMP registry-v2 buffer shares endpoint 7. + prefix += ('#define apple_rtkit_pmp_aux_rx(...) ' + 'assert(!"The PMP auxiliary buffer is outside this ordered-display fixture")\n') unit = prefix + unit if unit is not None: result['translation_unit_sha256'] = run_unit(name, unit, output, cases, includes if name == 'service-rebind' else (), flags) From bd4a566572abe8edcef5294e4957db999c08198a Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 14:14:30 +1100 Subject: [PATCH 12/40] 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 (cherry picked from commit 6b0a32bcf17435e560ec8392b5c3c48b76a86e2f) --- 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 7244b8946f643..2c3404593e69a 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -203,15 +203,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"); @@ -4839,20 +4830,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 @@ -4999,7 +4976,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 18ee687efec0f11c3bd13b51e5e58df41b3217f9 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 26 Oct 2026 14:21:51 +1100 Subject: [PATCH 13/40] docs: gpu: identify the Apple parameter migration license Assisted-by: LLM Signed-off-by: Ryan Murray (cherry picked from commit c3a5cff12d7f6915c4c6fd119a77d3f1331b71e8) --- 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 16bf2b19f7efc4fb895dac4a46988ce35d8046a4 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 30 Oct 2026 14:07:35 +1100 Subject: [PATCH 14/40] rust: keep bindgen layout checks in every build Stock bindgen 0.73.2 drops the alignment of an explicitly aligned empty C struct. With CONFIG_SCHEDSTATS=n the empty, cacheline-aligned sched_statistics in task_struct loses its alignment, the following fields move, and Rust read a scheduler list pointer as task_struct.mm. The Asahi RSS charge then faulted in percpu_counter_add_batch. With --no-layout-tests the build gives no sign of this. Drop --no-layout-tests again so every Rust build checks the size, alignment and offsets bindgen emits, and select the corrected generator in tools/aurora-bindgen automatically when it has been prepared (also for O= builds). build.py now defaults to tools/aurora-bindgen/build and publishes build/bin/bindgen only after its compile tests pass. An explicit BINDGEN= still wins. The Fedora, Arch and NixOS kernel packages and the fleet recipe already build with the corrected generator. Drop the stale dead_code expectation on Device::parent(), which made the WERROR KUnit build fail once a driver uses parent() with AUXILIARY_BUS=n. Add the rust-build CI job that prepares the corrected generator and compiles the SEP module for arm64. The SEP ABI and recovery tests from aurora-wip stay there until the v5 enrolment ABI lands. This takes aurora-wip's b47f66634c46, 7b563df1ba87, b18158c5ab59 and 27060370b201, and the CI step from 5449640813e7, and replaces the opt-in default from 4c56f9cd3033. Assisted-by: LLM Signed-off-by: Ryan Murray Co-Authored-By: Claude Opus 5.5 --- .github/workflows/sep-source.yml | 43 ++++++++++++ Documentation/rust/bindgen-layout-checks.rst | 71 +++++++++++--------- Makefile | 5 +- rust/Makefile | 18 ++--- rust/kernel/device.rs | 1 - tools/aurora-bindgen/.gitignore | 1 + tools/aurora-bindgen/README.rst | 70 ++++++++++++++----- tools/aurora-bindgen/build.py | 7 +- 8 files changed, 152 insertions(+), 64 deletions(-) create mode 100644 .github/workflows/sep-source.yml create mode 100644 tools/aurora-bindgen/.gitignore diff --git a/.github/workflows/sep-source.yml b/.github/workflows/sep-source.yml new file mode 100644 index 0000000000000..11ff022654dbd --- /dev/null +++ b/.github/workflows/sep-source.yml @@ -0,0 +1,43 @@ +name: SEP source checks + +on: + pull_request: + paths: + - 'Makefile' + - 'rust/**' + - 'tools/aurora-bindgen/**' + - 'drivers/soc/apple/**' + - 'include/uapi/linux/apple-sep.h' + - '.github/workflows/sep-source.yml' + +jobs: + 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 + - name: Prepare the layout-corrected bindgen + env: + LIBCLANG_PATH: /usr/lib/llvm-18/lib + run: python3 tools/aurora-bindgen/build.py + - 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 + 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 diff --git a/Documentation/rust/bindgen-layout-checks.rst b/Documentation/rust/bindgen-layout-checks.rst index aaac5b24fb742..4e0a996bd701d 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,26 +36,27 @@ 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 ------------------- ``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:: + + python3 tools/aurora-bindgen/build.py + make LLVM=1 ... - make LLVM=1 BINDGEN=/path/to/bindgen BINDGEN_LAYOUT_TESTS=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 @@ -64,5 +64,12 @@ 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. + +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/rust/Makefile b/rust/Makefile index f9b22e936fa40..8f45c44177693 100644 --- a/rust/Makefile +++ b/rust/Makefile @@ -513,23 +513,19 @@ 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. +# Keep bindgen layout assertions enabled: a lost alignment can change field +# offsets without changing the containing type size. See Documentation/rust/ +# bindgen-layout-checks.rst for generator requirements. 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/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. 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 75d4c3a2480e7..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 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. +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. @@ -70,14 +81,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 +98,31 @@ 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. + +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 7aa222cc028bb..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)+' 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') +# 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 97dafebbcceadb931a05f495d4a9724fb83ff855 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 30 Oct 2026 14:19:11 +1100 Subject: [PATCH 15/40] tools: asahi: model the T8132 members in the ATCPHY USB3 harness The T8132 support in atc.c added the PIPE lock timeout helper, T8132 lane and PIPE back ends, a USB2-only power restore and new state members. The harness extracts the driver bodies, so it stopped compiling on integration/aurora. Extract atcphy_pipehandler_lock_timeout() with the other driver functions, since the ACK polling moved into it, and point the missing-ack-check mutation there. Add the new members to the modeled structs, and stub the T8132 back ends and the USB2-only power restore with assertions: the modeled machines never select that generation. All 36 cases pass and all 49 mutations are killed. Assisted-by: LLM Signed-off-by: Ryan Murray Co-Authored-By: Claude Opus 5.5 --- tools/asahi/atc-usb3-tests.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/tools/asahi/atc-usb3-tests.py b/tools/asahi/atc-usb3-tests.py index 5e45e11c3c46d..af585383ed33e 100644 --- a/tools/asahi/atc-usb3-tests.py +++ b/tools/asahi/atc-usb3-tests.py @@ -74,7 +74,7 @@ def block(source, marker): STUBS = r''' struct atcphy_hw { enum atcphy_generation gen; - bool has_t8140_dp, dp_t8122, has_usb4, park_pipe_unlocked, park_dummy_phy; }; + bool has_t8140_dp, dp_t8122, has_usb4, park_pipe_unlocked, park_dummy_phy, usb2_only; }; struct device_node { const char *const *compatible; unsigned int count; }; static bool of_device_is_compatible(const struct device_node *root, const char *compatible) { if (!root) return false; @@ -95,6 +95,9 @@ def block(source, marker): enum atcphy_mode typec_mode; bool pipe_pending, t8103_dummy_pending, t8103_usb3_retry, t8103_usb3_workaround, mode_dirty, usb_off_pending; bool host_active, usb3_port_off, dp_only, ss_tunables, fixed_usb2, t8140_dp_tunables, dp_tunables; + /* T8132 state: the modeled machines never select that generation. */ + bool t8132_ss, t8132_common_owned, t8132_failed; + struct { void *usb3_dp; } tunables; }; struct phy { struct apple_atcphy *data; }; struct typec_mux_dev { struct apple_atcphy *data; }; @@ -169,6 +172,16 @@ def block(source, marker): static int atcphy_usb2_power_off(struct apple_atcphy *a) { (void)a; usb2_power_off_calls++; return 0; } +/* T8132 back ends: out of scope, and never reached by the modeled generations. */ +static int atcphy_configure_t8132(struct apple_atcphy *a, enum atcphy_mode mode) { + (void)a; (void)mode; assert(!"T8132 lane setup reached"); return -EINVAL; +} +static int atcphy_configure_pipehandler_usb3_host_t8132(struct apple_atcphy *a) { + (void)a; assert(!"T8132 PIPE setup reached"); return -EINVAL; +} +static void atcphy_usb2_power_on(struct apple_atcphy *a) { + (void)a; assert(!"USB2-only power restore reached"); +} static int atcphy_configure(struct apple_atcphy *atcphy, enum atcphy_mode mode) { /* ACTUAL_CONFIGURE_ENTRY */ struct apple_atcphy *a = atcphy; @@ -619,6 +632,7 @@ def harness(source): end = source.index("\n};", source.index("} atcphy_modes[] = {", start)) + 3 table = source[start:end] function_specs = [ + ("int", "atcphy_pipehandler_lock_timeout"), ("int", "atcphy_pipehandler_lock"), ("int", "atcphy_pipehandler_lock_live"), ("int", "atcphy_configure_pipehandler_usb3"), @@ -662,7 +676,7 @@ def harness(source): MUTATIONS = {'no-live-retry': ('atcphy_pipehandler_lock_live', '? 10 : 1;', '? 1 : 1;'), 'retry-later-generations': ('atcphy_pipehandler_lock_live', '? 10 : 1;', '? 10 : 10;'), 'sleep-after-last': ('atcphy_pipehandler_lock_live', 'if (attempt + 1 < attempts)', 'if (true)'), - 'missing-ack-check': ('atcphy_pipehandler_lock', + 'missing-ack-check': ('atcphy_pipehandler_lock_timeout', 'if (requested & PIPEHANDLER_LOCK_EN) {', 'if (requested & PIPEHANDLER_LOCK_EN) return 0;\n' '\tif (requested & PIPEHANDLER_LOCK_EN) {'), From febd79788c15f9f2f60759f86e0793952855bf4b Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 30 Oct 2026 05:17:56 +0000 Subject: [PATCH 16/40] soc: apple: display gate: say why the port controller link stays managed The supplier link from each T6041 port controller to its display processor becomes a managed device link. Unbinding the processor therefore unbinds the port controllers as well, and USB on those ports is down until the processor is bound again. A stateless link would avoid that, but it would leave the port unusable. The controller holds one mux handle for its USB and display muxes, and once the display route is unregistered, typec_mux_begin() refuses every transaction on that handle with -ENODEV. A controller left bound could not even return the port to the safe or USB state on the next plug, and would never take the routes the processor registers when it is bound again. With the managed link the controller probes again once the processor is back, with every mux. Also note why the deferred probe timeout cannot make a controller take only its USB muxes. The wait is a fwnode link until the processor has a device, which the timeout does not touch. The device is created with its node enabled, and from then on the Type-C core defers for the routes that are not registered yet. No functional change. Assisted-by: LLM Signed-off-by: Ryan Murray Co-Authored-By: Claude Opus 5.5 --- drivers/soc/apple/display-gate.c | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/drivers/soc/apple/display-gate.c b/drivers/soc/apple/display-gate.c index 2beb8ef39fd1d..deb84564bc2bc 100644 --- a/drivers/soc/apple/display-gate.c +++ b/drivers/soc/apple/display-gate.c @@ -2406,6 +2406,21 @@ static int staged_set(struct device_node *display, bool dcps, const char *dcp_st * each port controller wired to a route waits for the processor that registers * the route: a supplier link from the controller to that processor makes it * probe once, when it can take every mux it is wired to. + * + * The device link that fw_devlink makes of it is a managed one, and has to + * be. A port controller holds one mux handle for all of its muxes, and once + * one of them is unregistered every later transaction on that handle fails + * with -ENODEV, the USB and safe states included. So a processor that is + * unbound takes its port controllers with it, and they probe again when it + * is bound again, instead of staying bound to ports they cannot switch. + * + * The deferred probe timeout cannot reorder this. Until the processor has a + * device the wait is a fwnode link, which the timeout leaves alone. The + * device is created with its node enabled, so a controller that the timeout + * lets go before the processor binds finds the routes not yet registered and + * defers in the Type-C core, which never gives up on a mux. A processor that + * gives up on its routes disables them (dcp_typec_routes_disable()), and its + * controllers then probe without them. */ static struct fwnode_handle *staged_port_ctrls[ARRAY_SIZE(t6041_usbc_nodes)]; static unsigned int staged_nr_port_ctrls; From e8193ee31b2c199ec537cb9b4295c8815cf6ac9c Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 30 Oct 2026 10:42:55 +1100 Subject: [PATCH 17/40] wifi: brcmfmac: query assoc_info with a full-sized buffer Since commit cda37449d576 ("wifi: brcmfmac bound association IEs to returned payloads") the "assoc_info" iovar is queried with an 8-byte buffer, the size of the two lengths the driver reads. The firmware fails a get whose buffer is shorter than its whole association record, so every association on the BCM4387 and BCM4388 now logs brcmf_get_assoc_ies: could not get assoc info (-52) and cfg80211 gets no association request or response IEs, and the WMM AC priorities are never read. Saved boot logs show the message on every association after that change and on none before it. Query with WL_ASSOC_INFO_MAX bytes as before, into the buffer the IE queries already use, and keep the bounds checks on what the firmware returns. Fixes: cda37449d576 ("wifi: brcmfmac bound association IEs to returned payloads") Assisted-by: LLM Signed-off-by: Ryan Murray Co-Authored-By: Claude Opus 5.5 (cherry picked from commit cfbedf606e557cb3a5ba3ba645df0a8272d1029f) --- .../broadcom/brcm80211/brcmfmac/cfg80211.c | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c index 1ded4c578b5f5..989546c595291 100644 --- a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c +++ b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c @@ -6572,30 +6572,37 @@ static s32 brcmf_get_assoc_ies(struct brcmf_cfg80211_info *cfg, s32 err; brcmf_clear_assoc_ies(cfg); - err = brcmf_fil_iovar_data_get_len(ifp, "assoc_info", &assoc_info, - sizeof(assoc_info), &received); + buffer = kzalloc(WL_EXTRA_BUF_MAX, GFP_KERNEL); + if (!buffer) + return -ENOMEM; + /* + * The firmware fails the query with a buffer shorter than its whole + * association record, so ask with WL_ASSOC_INFO_MAX bytes and take + * the two lengths that lead the reply. + */ + err = brcmf_fil_iovar_data_get_len(ifp, "assoc_info", buffer, + WL_ASSOC_INFO_MAX, &received); if (err) { bphy_err(drvr, "could not get assoc info (%d)\n", err); - return err; + goto done; } if (received < sizeof(assoc_info)) { bphy_err(drvr, "short assoc info: received %u required %zu\n", received, sizeof(assoc_info)); - return -EBADMSG; + err = -EBADMSG; + goto done; } + memcpy(&assoc_info, buffer, sizeof(assoc_info)); req_len = le32_to_cpu(assoc_info.req_len); resp_len = le32_to_cpu(assoc_info.resp_len); if (req_len > WL_EXTRA_BUF_MAX || resp_len > WL_EXTRA_BUF_MAX) { bphy_err(drvr, "invalid lengths in assoc info: req %u resp %u\n", req_len, resp_len); - return -EINVAL; - } - if (req_len || resp_len) { - buffer = kzalloc(WL_EXTRA_BUF_MAX, GFP_KERNEL); - if (!buffer) - return -ENOMEM; + err = -EINVAL; + goto done; } if (req_len) { + memset(buffer, 0, WL_EXTRA_BUF_MAX); err = brcmf_fil_iovar_data_get_len(ifp, "assoc_req_ies", buffer, max_t(u32, req_len, WL_ASSOC_INFO_MAX), &received); From 4bb084021b88821a7da87c44791b73a34ff0fd93 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 30 Oct 2026 10:44:16 +1100 Subject: [PATCH 18/40] drm/apple: expose the pixel blend mode of DCP planes Every DCP plane advertises formats with alpha but no "pixel blend mode" property, so drm_mode_config_validate() warns once per plane at boot: [PLANE:35:plane-0] pixel format with alpha exposed but blend mode not setup. Please fix. Without the property userspace must assume pre-multiplied alpha, while the driver leaves is_premultiplied clear for every alpha format and DCP blends those by coverage. Create the property. DCP blends pre-multiplied when the surface's is_premultiplied flag is set and by coverage when it is clear, so expose both, with pre-multiplied as the default the uAPI documents, and set the flag from the plane state. The H17P swap surface has no is_premultiplied byte and its firmware applies alpha as coverage, so H17P planes expose coverage only. Assisted-by: LLM Signed-off-by: Ryan Murray Co-Authored-By: Claude Opus 5.5 (cherry picked from commit 4d1241d8fb1d89162ee2dfccec44b21f731d46cc) --- drivers/gpu/drm/apple/apple_drv.c | 6 +++++- drivers/gpu/drm/apple/plane.c | 21 +++++++++++++++++++-- drivers/gpu/drm/apple/plane.h | 3 ++- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/drivers/gpu/drm/apple/apple_drv.c b/drivers/gpu/drm/apple/apple_drv.c index a3946fae4b5da..d71c5c0370f2e 100644 --- a/drivers/gpu/drm/apple/apple_drv.c +++ b/drivers/gpu/drm/apple/apple_drv.c @@ -698,6 +698,7 @@ static int apple_probe_per_dcp(struct device *dev, bool supports_l10r = !dcp_fw_compat_is_12_x(dcp); struct apple_dcp *dcp_data = platform_get_drvdata(dcp); bool supports_xrgb2101010; + bool supports_premultiplied; enum drm_plane_type plane_type; bool interchange; bool supports_interchange; @@ -709,6 +710,8 @@ static int apple_probe_per_dcp(struct device *dev, * (T8140 and T8122): advertise linear framebuffers only there. */ supports_interchange = dcp_data->fw_compat != DCP_FIRMWARE_H17P; + /* The H17P swap surface has no is_premultiplied flag. */ + supports_premultiplied = dcp_data->fw_compat != DCP_FIRMWARE_H17P; if (bitmap_empty(iomfb_surfaces, DCP_MAX_PLANES)) return dev_err_probe(dev, -EINVAL, "No usable display surfaces\n"); @@ -748,7 +751,8 @@ static int apple_probe_per_dcp(struct device *dev, planes[zpos] = apple_plane_init(drm, 1U << num, surf, supports_l10r, supports_xrgb2101010, - plane_type, interchange); + plane_type, interchange, + supports_premultiplied); } if (IS_ERR(planes[zpos])) return PTR_ERR(planes[zpos]); diff --git a/drivers/gpu/drm/apple/plane.c b/drivers/gpu/drm/apple/plane.c index a0343aa5ec5ec..87fecce18ee20 100644 --- a/drivers/gpu/drm/apple/plane.c +++ b/drivers/gpu/drm/apple/plane.c @@ -12,6 +12,7 @@ #include "iomfb_plane.h" #include #include +#include #include #include #include @@ -429,7 +430,8 @@ static void apple_plane_atomic_update(struct drm_plane *plane, */ if (fmt->format == DRM_FORMAT_XRGB8888 || fmt->format == DRM_FORMAT_XBGR8888 || - fmt->format == DRM_FORMAT_XBGR2101010) + fmt->format == DRM_FORMAT_XBGR2101010 || + base->pixel_blend_mode == DRM_MODE_BLEND_PREMULTI) is_premultiplied = true; new_state->src_rect = drm_to_dcp_rect_fp(&base->src); @@ -742,11 +744,14 @@ struct drm_plane *apple_plane_init(struct drm_device *dev, bool supports_l10r, bool supports_xrgb2101010, enum drm_plane_type type, - bool interchange) + bool interchange, + bool premultiplied) { struct apple_plane *plane; + unsigned int blend_modes; const u32 *fmts; u32 num_fmts; + int ret; switch (type) { case DRM_PLANE_TYPE_PRIMARY: @@ -786,6 +791,18 @@ struct drm_plane *apple_plane_init(struct drm_device *dev, if (IS_ERR(plane)) return ERR_PTR(PTR_ERR(plane)); + /* + * The surface flag is_premultiplied selects pre-multiplied blending; + * without it DCP applies the alpha channel as coverage. Firmware whose + * swap surface has no such flag only blends by coverage. + */ + blend_modes = BIT(DRM_MODE_BLEND_COVERAGE); + if (premultiplied) + blend_modes |= BIT(DRM_MODE_BLEND_PREMULTI); + ret = drm_plane_create_blend_mode_property(&plane->base, blend_modes); + if (ret) + return ERR_PTR(ret); + drm_plane_create_color_properties(&plane->base, (1 << DRM_COLOR_ENCODING_MAX) - 1, (1 << DRM_COLOR_RANGE_MAX) - 1, diff --git a/drivers/gpu/drm/apple/plane.h b/drivers/gpu/drm/apple/plane.h index f7a819c73645a..69229ad6a5775 100644 --- a/drivers/gpu/drm/apple/plane.h +++ b/drivers/gpu/drm/apple/plane.h @@ -38,7 +38,8 @@ struct drm_plane *apple_plane_init(struct drm_device *dev, bool supports_l10r, bool supports_xrgb2101010, enum drm_plane_type type, - bool interchange); + bool interchange, + bool premultiplied); struct drm_plane *apple_plane_init_v14_7(struct drm_device *dev, unsigned long possible_crtcs, u32 iomfb_surf, From 5bc9ff640132538e6c211c0d152bd3df40230335 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 30 Oct 2026 10:46:10 +1100 Subject: [PATCH 19/40] Bluetooth: hci_bcm4377: handle function resets through the PCI core Probe resets the function with pci_reset_function_locked() while the driver is bound. The driver has no reset_prepare or reset_done handler, so since commit a3151e6daaec ("PCI: Warn if a running device is unaware of reset") every boot logs hci_bcm4377 0000:01:00.1: resetting hci_bcm4377 0000:01:00.1: reset done on the M1 Pro, the M2 and the M4 Pro. The warning has a point: a reset from the PCI core or from sysfs while the controller runs loses its firmware and rings, and nothing in the driver notices until commands time out. Add the handlers. Probe resets before it sets the driver data, and bcm4377_pci_reset() already quiesces and restarts the controller around its own reset, so both are left alone. Any other reset keeps the interrupt off while it runs and then rebinds the driver, the recovery the driver already uses after a command timeout; that path moves into bcm4377_queue_rebind() so both callers share it. Assisted-by: LLM Signed-off-by: Ryan Murray Co-Authored-By: Claude Opus 5.5 (cherry picked from commit ec5c702bb54d3332295e7d03344b9005f5e203a8) --- drivers/bluetooth/hci_bcm4377.c | 62 +++++++++++++++++++++++++++++---- 1 file changed, 56 insertions(+), 6 deletions(-) diff --git a/drivers/bluetooth/hci_bcm4377.c b/drivers/bluetooth/hci_bcm4377.c index 9d3bc53d54607..dc24c3e5057a0 100644 --- a/drivers/bluetooth/hci_bcm4377.c +++ b/drivers/bluetooth/hci_bcm4377.c @@ -601,6 +601,8 @@ struct bcm4377_data { const char *board_type; bool needs_reset; + /* Set while bcm4377_pci_reset() resets the function itself. */ + bool own_reset; bool cal_needed; bool cal_loaded; bool setup_retry_used; @@ -1422,7 +1424,9 @@ static int bcm4377_pci_reset(struct bcm4377_data *bcm4377) if (bcm4377->hw->disable_aspm) bcm4377_disable_aspm(bcm4377); + WRITE_ONCE(bcm4377->own_reset, true); ret = pci_reset_function(bcm4377->pdev); + WRITE_ONCE(bcm4377->own_reset, false); if (ret) { dev_err(&bcm4377->pdev->dev, "function level reset failed: %d\n", ret); @@ -2783,14 +2787,12 @@ static void bcm4377_reset_work(struct work_struct *work) * every later command times out until the module is reloaded. Recover the * same way, by rebinding the driver. */ -static void bcm4377_hci_reset(struct hci_dev *hdev) +static void bcm4377_queue_rebind(struct bcm4377_data *bcm4377, + const char *why) { - struct bcm4377_data *bcm4377 = hci_get_drvdata(hdev); + struct hci_dev *hdev = bcm4377->hdev; struct bcm4377_reset_work *rw; - /* A timeout while still setting up would only loop probe failures. */ - if (hci_dev_test_flag(hdev, HCI_SETUP)) - return; if (test_and_set_bit(0, &bcm4377->reset_pending)) return; /* @@ -2808,12 +2810,59 @@ static void bcm4377_hci_reset(struct hci_dev *hdev) return; } - bt_dev_err(hdev, "controller not responding, rebinding the driver"); + bt_dev_err(hdev, "%s, rebinding the driver", why); rw->pdev = pci_dev_get(bcm4377->pdev); INIT_WORK(&rw->work, bcm4377_reset_work); queue_work(bcm4377_reset_wq, &rw->work); } +static void bcm4377_hci_reset(struct hci_dev *hdev) +{ + struct bcm4377_data *bcm4377 = hci_get_drvdata(hdev); + + /* A timeout while still setting up would only loop probe failures. */ + if (hci_dev_test_flag(hdev, HCI_SETUP)) + return; + bcm4377_queue_rebind(bcm4377, "controller not responding"); +} + +/* + * A function reset requested by the PCI core or by userspace. Probe resets the + * function before it sets the driver data, and bcm4377_pci_reset() quiesces + * and restarts the controller around its own reset, so neither needs more. + * Any other reset loses the firmware and the rings under a running driver: + * keep the interrupt off across it, then rebind, as for a command timeout. + */ +static void bcm4377_reset_prepare(struct pci_dev *pdev) +{ + struct bcm4377_data *bcm4377 = pci_get_drvdata(pdev); + int irq; + + if (!bcm4377 || READ_ONCE(bcm4377->own_reset)) + return; + irq = pci_irq_vector(pdev, 0); + if (irq > 0) + disable_irq(irq); +} + +static void bcm4377_reset_done(struct pci_dev *pdev) +{ + struct bcm4377_data *bcm4377 = pci_get_drvdata(pdev); + int irq; + + if (!bcm4377 || READ_ONCE(bcm4377->own_reset)) + return; + irq = pci_irq_vector(pdev, 0); + if (irq > 0) + enable_irq(irq); + bcm4377_queue_rebind(bcm4377, "function reset"); +} + +static const struct pci_error_handlers bcm4377_err_handler = { + .reset_prepare = bcm4377_reset_prepare, + .reset_done = bcm4377_reset_done, +}; + static void bcm4377_hci_unregister_dev(void *data) { struct hci_dev *hdev = data; @@ -3250,6 +3299,7 @@ static struct pci_driver bcm4377_pci_driver = { .name = "hci_bcm4377", .id_table = bcm4377_devid_table, .probe = bcm4377_probe, + .err_handler = &bcm4377_err_handler, .driver.pm = &bcm4377_ops, }; From 60b5d0b571f155becfea8eff9dd43d2105194129 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 30 Oct 2026 02:23:21 +0000 Subject: [PATCH 20/40] arm64: configs: asahi: Cap the page block order at 2 MiB With 16K pages the page block order follows the PMD order, so a page block is 32 MiB. khugepaged sizes min_free_kbytes from the page block size, eleven blocks per zone, which comes to 360448 kB (352 MiB) on every 8 GB Mac with this config. The low and high watermarks follow at 440 MiB and 528 MiB, and the per-CPU page list limits are derived from the low watermark, so another 250 to 440 MiB of free pages sits on per-CPU lists where MemFree and MemAvailable do not count it. On a MacBook Neo (8 GB) idling at the GNOME desktop this explains about 1.4 GiB of the 2.9 GiB that free and the desktop monitors report as used: 1.2 GiB of watermark deductions in MemAvailable plus 265 MiB on the per-CPU lists. kswapd also keeps about half a gigabyte free that the page cache could use. The M2 MacBook Air shows the same 360448 kB. Cap the page block order at 7, a 2 MiB block, the same size 4K arm64 and x86 kernels use. min_free_kbytes on the Neo drops to about 22 MiB and the high watermark to about 37 MiB. 32 MiB transparent huge pages are still allocated; compaction needs more free blocks to build one, as the Kconfig help notes. THP is madvise only in our configs. CMA areas need 2 MiB alignment instead of 32 MiB. Booted in QEMU (virt, 6 CPUs, 8 GB, 16K pages, THP madvise) with only this option changed: order 11: min_free_kbytes 411824, MemAvailable 7476144 kB order 7: min_free_kbytes 45056, MemAvailable 8026816 kB QEMU has two zones, so its values are twice a single-zone Mac's. A 128 MiB MADV_HUGEPAGE region got 4 of 4 32 MiB THPs with no fallback on both kernels. Assisted-by: LLM Signed-off-by: Ryan Murray Co-Authored-By: Claude Opus 5.5 (cherry picked from commit a6d008b8fc2b9b6bcafa0e65bdb91d0ba4dadd03) --- arch/arm64/configs/asahi.config | 3 +++ 1 file changed, 3 insertions(+) diff --git a/arch/arm64/configs/asahi.config b/arch/arm64/configs/asahi.config index 4422527c3ed65..25c4ee4183cc6 100644 --- a/arch/arm64/configs/asahi.config +++ b/arch/arm64/configs/asahi.config @@ -11,6 +11,9 @@ CONFIG_ARM64_16K_PAGES=y CONFIG_ARM64_VA_BITS_48=y CONFIG_ARM64_USER4K_EXPERIMENTAL=y CONFIG_ARM64_MEMORY_MODEL_CONTROL=y +# 16K pages make a page block 32 MiB, and khugepaged then sets min_free_kbytes +# to 352 MiB on an 8 GB Mac. Keep page blocks at 2 MiB as on 4K kernels. +CONFIG_PAGE_BLOCK_MAX_ORDER=7 CONFIG_ARM_APPLE_CPUIDLE=y CONFIG_ARM_APPLE_SOC_CPUFREQ=m CONFIG_THERMAL_GOV_POWER_ALLOCATOR=y From e01cbc2b167bccc5985c7959d809c4c898e23e42 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 30 Oct 2026 17:21:47 +1100 Subject: [PATCH 21/40] tools/asahi: follow the CLPC dense GPU floor in the demand test The CLPC sampler gained dense light GPU work (264558de4e36) and floor release counters (87e3a4d597af). The host test still extracted the old bodies, so it stopped compiling. Two of its mutations had also gone stale: the streaming and PMP-backoff strings no longer appear in the driver, so those controls tested nothing. Extract the dense and counter helpers, give the fake GPU a submission sequence, and check that dense submissions on T8140 earn the floor, that one sparse or late period ends the streak, and that the release and return counters follow the PMP backoff. Rewrite the stale mutations against the current code, add controls for the new paths, and refuse any mutation that no longer changes the source. Assisted-by: LLM Signed-off-by: Ryan Murray Co-Authored-By: Claude Opus 5.5 --- tools/asahi/clpc-gpu-demand-tests.py | 58 ++++++++++++++++++++++++---- 1 file changed, 50 insertions(+), 8 deletions(-) diff --git a/tools/asahi/clpc-gpu-demand-tests.py b/tools/asahi/clpc-gpu-demand-tests.py index ecf0fff465711..66f0175802e5a 100644 --- a/tools/asahi/clpc-gpu-demand-tests.py +++ b/tools/asahi/clpc-gpu-demand-tests.py @@ -24,6 +24,7 @@ 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) + dense_samples = re.search(r'^#define APPLE_CLPC_GPU_DENSE_SAMPLES\s.*$', source, 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) + ';' @@ -31,7 +32,10 @@ def main(): methods = '\n'.join(item(source, s) for s in [ 'static unsigned int apple_clpc_vote(', 'static u32 apple_clpc_floor(', 'static bool apple_clpc_gpu_streaming(', + 'static bool apple_clpc_gpu_dense_step(', + 'static bool apple_clpc_gpu_dense(', 'static bool apple_clpc_gpu_limited(', + 'static void apple_clpc_gpu_floor_count(', 'static void apple_clpc_sample(']) code = r''' #include @@ -56,7 +60,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_gpu { bool active; u64 sequence; }; struct apple_clpc_cluster { unsigned int request[APPLE_CLPC_RAILS]; }; struct apple_clpc { const struct apple_clpc_hw *hw; @@ -66,14 +70,18 @@ def main(): unsigned long samples, next_scan, update_errors; void *covered, *pmc, *gpu_pmgr; u32 gpu_offset, mask, last_floor, last_reason; - bool gpu_on, gpu_limited; + bool gpu_on, gpu_limited, gpu_floor_released; unsigned long gpu_on_since, gpu_sampled, gpu_limited_at; + u64 gpu_sequence; + unsigned int gpu_dense_streak; + unsigned long gpu_floor_releases, gpu_floor_returns; + unsigned long gpu_streaming_samples, gpu_dense_samples; 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 bool apple_clpc_gpu_active(struct apple_clpc_gpu *g, u64 *u, u64 *s) { *u=jiffies; *s=g->sequence; return g->active; } static void *cpu_possible_mask; static u32 pmgr_status, pmp_floor, published; static int read_error, vote_error; @@ -91,7 +99,7 @@ def main(): (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''' +''' + dense_samples + '\n' + tables + '\n' + methods + r''' static void check_t8132(void) { struct apple_clpc c={.hw=&apple_clpc_hw_t8132,.agent=1,.mask=3,.gpu_pmgr=(void *)1}; read_error=vote_error=0; pmgr_status=0; pmp_floor=0; jiffies=0; @@ -118,12 +126,38 @@ def main(): pmp_floor=3; for (jiffies=168; jiffies<=204; jiffies+=4) apple_clpc_sample(&c); assert(published==0 && c.gpu_limited && !queued_idle); + assert(c.gpu_floor_releases==1 && c.gpu_floor_returns==0); pmp_floor=4; for (jiffies=208; jiffies<=304; jiffies+=4) apple_clpc_sample(&c); assert(published==0x34 && c.last_reason==4); + assert(c.gpu_floor_releases==1 && c.gpu_floor_returns==1); + assert(c.gpu_streaming_samples && !c.gpu_dense_samples); jiffies=308; pmgr_status=0; apple_clpc_sample(&c); jiffies=340; apple_clpc_sample(&c); assert(published==0 && queued_idle); } +// Dense light work: graphics powers off between submissions, so it never +// streams, but 2+ submissions in each of 4 periods earn the GPU floor. +static void check_t8140_dense(void) { + struct apple_clpc_gpu gpu={0}; + struct apple_clpc c={.hw=&apple_clpc_hw_t8140,.agent=1,.mask=3,.gpu_pmgr=(void *)1,.gpu=&gpu}; + read_error=vote_error=0; pmp_floor=4; pmgr_status=0; published=~0u; + jiffies=100; apple_clpc_sample(&c); assert(published==0 && queued_idle); + for (jiffies=104; jiffies<=116; jiffies+=4) { + gpu.sequence+=2; apple_clpc_sample(&c); + assert(published==(jiffies<116 ? 0 : 0x34)); + } + assert(c.last_reason==8 && c.gpu_dense_samples==1 && !c.gpu_streaming_samples); + // One sparse period ends the streak; the vote holds, then drops. + jiffies=120; gpu.sequence+=1; apple_clpc_sample(&c); + assert(published==0x34 && c.last_reason==16); + jiffies=148; apple_clpc_sample(&c); assert(published==0 && queued_idle); + // A sample more than one period late does not count as a dense period. + for (jiffies=152; jiffies<=164; jiffies+=4) { gpu.sequence+=2; apple_clpc_sample(&c); } + assert(published==0x34); + jiffies=176; gpu.sequence+=40; apple_clpc_sample(&c); + assert(c.last_reason==16); +} + int main(void) { // T6040's supplier is always powered: it must NEVER substitute for work. struct apple_clpc_gpu gpu={0}; @@ -149,7 +183,8 @@ def main(): jiffies=264; apple_clpc_sample(&c); assert(published==0); check_t8132(); check_t8140(); - puts("PASS: T6040 host demand; T8132 powered floor; T8140 streaming and PMP backoff; idle release"); + check_t8140_dense(); + puts("PASS: T6040 host demand; T8132 powered floor; T8140 streaming, dense work and PMP backoff; idle release"); } ''' code = code.replace('#define APPLE_PMC_RAIL_SHIFT(r) ((r)*8)', rail_shift) @@ -158,12 +193,19 @@ def main(): for name, text in [ ('fixed', code), ('omit-host-demand', code.replace('if (clpc->gpu && !clpc->hw->gpu_demand) {', 'if (false) {')), - ('omit-streaming-demand', code.replace('if (clpc->gpu_pmgr && apple_clpc_gpu_streaming(clpc, now) &&', 'if (false && apple_clpc_gpu_streaming(clpc, now) &&')), - ('omit-pmp-backoff', code.replace('!apple_clpc_gpu_limited(clpc, now))', - '(!apple_clpc_gpu_limited(clpc, now) || true))')), + ('omit-streaming-demand', code.replace('bool streaming = apple_clpc_gpu_streaming(clpc, now);', + 'bool streaming = (apple_clpc_gpu_streaming(clpc, now), false);')), + ('omit-dense-demand', code.replace('if (streaming || dense) {', 'if (streaming || (dense && false)) {')), + ('dense-ignores-late', code.replace('if (!threshold || late || submissions < threshold)', + 'if (!threshold || (late && false) || submissions < threshold)')), + ('uncounted-floor-return', code.replace('clpc->gpu_floor_returns++;', 'clpc->gpu_floor_returns += 0;')), + ('omit-pmp-backoff', code.replace('bool released = apple_clpc_gpu_limited(clpc, now);', + 'bool released = (apple_clpc_gpu_limited(clpc, now), false);')), ('release-too-early', code.replace('msecs_to_jiffies(APPLE_CLPC_HOLD_MS)', '0')), ('retain-at-idle', code.replace('vote->state = 0;', '')), ]: + # A mutation that no longer matches the driver tests nothing. + assert name == 'fixed' or text != code, 'stale mutation: ' + name path = root / (name + '.c') path.write_text(text) binary = path.with_suffix('') From ae8725d081e6300958f51c524916e0e7435335dc Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 30 Oct 2026 17:21:51 +1100 Subject: [PATCH 22/40] tools/asahi: fall back to rustc when rustup is absent in G15 tests The PB growth and stats drain runners default to 'rustup run 1.93.1 rustc'. On a host without rustup they stop before testing anything. Keep the pinned toolchain where rustup exists, and otherwise use $RUSTC or rustc, as the other Rust host tests do. The receipt already records the compiler version that ran. Both pass, mutations included, with rustc 1.96. Assisted-by: LLM Signed-off-by: Ryan Murray Co-Authored-By: Claude Opus 5.5 --- tools/asahi/g15-pb-growth-tests.py | 12 ++++++++++-- tools/asahi/g15-stats-drain-tests.py | 12 ++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/tools/asahi/g15-pb-growth-tests.py b/tools/asahi/g15-pb-growth-tests.py index f25195017d686..12ee4f16a4955 100644 --- a/tools/asahi/g15-pb-growth-tests.py +++ b/tools/asahi/g15-pb-growth-tests.py @@ -58,6 +58,7 @@ import hashlib import json import os +import shutil from pathlib import Path import re import resource @@ -1362,13 +1363,20 @@ def main(): p = argparse.ArgumentParser(description=__doc__.splitlines()[0]) p.add_argument('--output', type=Path, help='Work directory (default: a temporary one)') p.add_argument('--rustc', default=os.environ.get('HOSTRUSTC'), - help='Host compiler executable (default: rustup run 1.93.1 rustc)') + help='Host compiler executable (default: rustup run 1.93.1 rustc, ' + 'or $RUSTC/rustc where rustup is not installed)') p.add_argument('--ref', help='Read the tested bodies at this git revision') p.add_argument('--mutation', choices=sorted(MUTATIONS) + sorted(CONTROLS)) p.add_argument('--mutations', action='store_true', help='Also require every mutation to fail and every control to pass') a = p.parse_args() - compiler = [a.rustc] if a.rustc else ['rustup', 'run', '1.93.1', 'rustc'] + # The receipt records whichever compiler version ran. + if a.rustc: + compiler = [a.rustc] + elif shutil.which('rustup'): + compiler = ['rustup', 'run', '1.93.1', 'rustc'] + else: + compiler = [os.environ.get('RUSTC', 'rustc')] rustc_version = subprocess.check_output(compiler + ['--version'], text=True).strip() import tempfile out = (a.output or Path(tempfile.mkdtemp(prefix='g15-pb-growth-'))).resolve() diff --git a/tools/asahi/g15-stats-drain-tests.py b/tools/asahi/g15-stats-drain-tests.py index 0db5a798f9253..230a72c260e76 100644 --- a/tools/asahi/g15-stats-drain-tests.py +++ b/tools/asahi/g15-stats-drain-tests.py @@ -27,6 +27,7 @@ import hashlib import json import os +import shutil from pathlib import Path import re import resource @@ -376,12 +377,19 @@ def main(): p = argparse.ArgumentParser(description=__doc__.splitlines()[0]) p.add_argument('--output', type=Path, help='Work directory (default: a temporary one)') p.add_argument('--rustc', default=os.environ.get('HOSTRUSTC'), - help='Host compiler executable (default: rustup run 1.93.1 rustc)') + help='Host compiler executable (default: rustup run 1.93.1 rustc, ' + 'or $RUSTC/rustc where rustup is not installed)') p.add_argument('--ref', help='Read the tested bodies at this git revision') p.add_argument('--mutation', choices=sorted(MUTATIONS)) p.add_argument('--mutations', action='store_true', help='Also require every mutation to fail') a = p.parse_args() - compiler = [a.rustc] if a.rustc else ['rustup', 'run', '1.93.1', 'rustc'] + # The receipt records whichever compiler version ran. + if a.rustc: + compiler = [a.rustc] + elif shutil.which('rustup'): + compiler = ['rustup', 'run', '1.93.1', 'rustc'] + else: + compiler = [os.environ.get('RUSTC', 'rustc')] rustc_version = subprocess.check_output(compiler + ['--version'], text=True).strip() import tempfile out = (a.output or Path(tempfile.mkdtemp(prefix='g15-stats-drain-'))).resolve() From 5a12fc06cc805ae7b3f15d773e665ea5742f1ad7 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 30 Oct 2026 17:21:54 +1100 Subject: [PATCH 23/40] tools/asahi: pass the engine to work_state acquire in the G17 test Work-state acquisition now takes the engine (3b5847f4d6b3, a node is kept for the other engine), so the compute deadline fixture no longer compiled. Give the fixture the Engine type, have its acquire check that compute admission asks for a compute node, and add a control that requests a render node instead. Assisted-by: LLM Signed-off-by: Ryan Murray Co-Authored-By: Claude Opus 5.5 --- tools/asahi/g17-compute-deadline-tests.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tools/asahi/g17-compute-deadline-tests.py b/tools/asahi/g17-compute-deadline-tests.py index 88c89f6857799..98c8751773f9b 100644 --- a/tools/asahi/g17-compute-deadline-tests.py +++ b/tools/asahi/g17-compute-deadline-tests.py @@ -70,7 +70,9 @@ def fixture(): struct Scratch;impl Scratch{fn pages(&self)->u16{0}} enum Validated{Compute{scratch:Scratch}} struct Status;impl Status{fn get(&self)->i32{if now_ns()>=STATUS.load(Ordering::SeqCst){-5}else{0}}} -struct Work;impl Work{fn acquire(&self)->Result{if MODE.load(Ordering::SeqCst)==4{Err(EBUSY)}else{Ok(())}}} +mod g17{pub mod job{#[derive(Clone,Copy,Debug,PartialEq)]pub enum Engine{Render,Compute}}} +// A work-state node is taken per engine: compute admission must ask for a compute one. +struct Work;impl Work{fn acquire(&self,engine:g17::job::Engine)->Result{assert_eq!(engine,g17::job::Engine::Compute,"compute admission takes a compute work state");if MODE.load(Ordering::SeqCst)==4{Err(EBUSY)}else{Ok(1)}}} struct Completion;impl Completion{fn status(&self)->Status{Status}fn work_state(&self)->Result{Ok(Work)}} struct Packet{command:Validated,completion:Completion} 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}} @@ -159,6 +161,7 @@ def main(): ('fixed', source, ''), ('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'), + ('render-work-state', source.replace('acquire(crate::g17::job::Engine::Compute)', 'acquire(crate::g17::job::Engine::Render)'), 'compute admission takes a compute work state'), ('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. From ac72d4f74c277a40f78f944f166c6b65fc55dd13 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 30 Oct 2026 17:21:58 +1100 Subject: [PATCH 24/40] tools/asahi: cover the new RTKit buffers in the OSLog test apple_rtkit_reinit() now also frees the CoreAnalytics and PMP aux buffers and resets the CoreAnalytics completion. The OSLog host test's struct lacked those fields and stopped compiling. Add them, fill every buffer before reinit and check each one is freed, rather than counting six frees. Add a control that keeps the OSLog buffer across reinit, and refuse a mutation that no longer changes the source. Assisted-by: LLM Signed-off-by: Ryan Murray Co-Authored-By: Claude Opus 5.5 --- tools/asahi/rtkit-oslog-tests.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/tools/asahi/rtkit-oslog-tests.py b/tools/asahi/rtkit-oslog-tests.py index 5f8eb290ebde5..160c163758d54 100644 --- a/tools/asahi/rtkit-oslog-tests.py +++ b/tools/asahi/rtkit-oslog-tests.py @@ -45,14 +45,14 @@ def function(source, name): void *dev, *wq; struct mailbox *mbox; struct apple_rtkit_shmem oslog_buffer, syslog_buffer, ioreport_buffer, crashlog_buffer; - struct apple_rtkit_shmem kdebug_buffer0, kdebug_buffer1; + struct apple_rtkit_shmem kdebug_buffer0, kdebug_buffer1, coreanalytics_buffer, pmp_aux_buffer; int sync_ack_fifo; bool sync_syslog_ack, oslog_inherited, syslog_inherited, crashlog_inherited, crashed, shutting_down; unsigned iop_power_state, ap_power_state; unsigned long endpoints[4]; char *syslog_msg_buffer; size_t syslog_n_entries, syslog_msg_size; - int epmap_completion, iop_pwr_ack_completion, ap_pwr_ack_completion; + int epmap_completion, iop_pwr_ack_completion, ap_pwr_ack_completion, coreanalytics_completion; }; static unsigned sent, warnings, errors, requests, detachments, flushes, freed, checks; static int send_error, setup_error, start_error; @@ -121,11 +121,16 @@ def function(source, name): for (unsigned fail=0;fail<2;fail++) { memset(&r,0,sizeof(r));r.mbox=&m;apple_rtkit_mark_running(&r); assert(r.oslog_inherited && (r.endpoints[0]&(1UL<size=4096; + r.crashed=true;r.syslog_msg_size=128; detachments=flushes=freed=0;start_error=fail?-5:0; assert(apple_rtkit_reinit(&r)==start_error); assert(!r.oslog_inherited && !r.syslog_inherited && !r.crashlog_inherited); - assert(!r.oslog_buffer.size && !r.crashed && !r.syslog_msg_size && freed==6); + assert(!r.crashed && !r.syslog_msg_size && freed==ARRAY_SIZE(buffers)); + // Every buffer of the old firmware session goes, the OSLog one included. + for (unsigned i=0;isize); assert(r.endpoints[0]==(1UL<oslog_inherited = false;', 'rtk->oslog_inherited = true;') + elif args.mutate == 'buffer': + bodies = bodies.replace('apple_rtkit_free_buffer(rtk, &rtk->oslog_buffer);', '') + # A mutation that no longer matches the driver would test nothing. + assert not args.mutate or bodies != '\n'.join( + function(source, name) for name in ['apple_rtkit_oslog_rx', 'apple_rtkit_mark_running', 'apple_rtkit_reinit']), \ + 'stale mutation: ' + args.mutate code = PREFIX.replace('struct apple_rtkit_shmem', constants + '\nstruct apple_rtkit_shmem', 1) + bodies + MAIN def run(out): From ff1234c49ce3913598d5d3b1df3a2b5aafd51cfc Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 30 Oct 2026 17:22:02 +1100 Subject: [PATCH 25/40] tools/asahi: model the yielded Type-C port in the tunnel route test dcp_fabric_snapshot_port() now leaves a port that yielded both pipes to an HDMI display unwanted (0d43a7f471ac). The fixture's port had no typec_yielded field, so the test stopped compiling. Add the field and check the snapshot for both values. Also check that a T6030 tunnel on source 1, which has no display-engine clock, gets no crossbar control, since no case covered a blocked source. The runner now builds two mutations, ignoring the clock and ignoring the yield, and requires both to fail. Assisted-by: LLM Signed-off-by: Ryan Murray Co-Authored-By: Claude Opus 5.5 --- tools/asahi/tunnel-route-tests.py | 26 ++++++++++++++++++++------ tools/asahi/tunnel-route/fixture.c | 23 +++++++++++++++++++++-- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/tools/asahi/tunnel-route-tests.py b/tools/asahi/tunnel-route-tests.py index 5be05c91c156f..088ca4df1e1a3 100644 --- a/tools/asahi/tunnel-route-tests.py +++ b/tools/asahi/tunnel-route-tests.py @@ -16,6 +16,11 @@ 'drivers/gpu/drm/apple/dcp-fabric.c': [ 'dcp_fabric_snapshot_port', 'dcp_typec_tunnel_ctl'], } +# Each must make the fixture fail. +MUTATIONS = { + 'clock-ignored': ('& BIT(dpin)))', '& 0))'), + 'snapshot-ignores-yield': ('port->dp_wanted && !port->typec_yielded;', 'port->dp_wanted;'), +} def function(source, name): @@ -45,12 +50,21 @@ def main(): code = (ROOT / 'tools/asahi/tunnel-route/fixture.c').read_text().replace( '/* CORE_HEADER */', header).replace( '/* PRODUCTION_FUNCTIONS */', '\n'.join(bodies)) - source = out / 'production.c' - source.write_text(code) - binary = out / 'production' - subprocess.run(['cc', '-std=gnu11', '-Wall', '-Werror', - str(source), '-o', str(binary)], check=True) - run = subprocess.run([str(binary)], capture_output=True, text=True) + + def build(name, text): + source = out / (name + '.c') + source.write_text(text) + binary = out / name + subprocess.run(['cc', '-std=gnu11', '-Wall', '-Werror', + str(source), '-o', str(binary)], check=True) + return subprocess.run([str(binary)], capture_output=True, text=True) + + for name, (old, new) in MUTATIONS.items(): + assert code.count(old) == 1, 'stale mutation: ' + name + mutant = build(name, code.replace(old, new)) + assert mutant.returncode, 'mutation survived: ' + name + print('PASS negative control:', name) + run = build('production', code) receipt = { 'bodies': {name: hashlib.sha256(body.encode()).hexdigest() for name, body in zip( diff --git a/tools/asahi/tunnel-route/fixture.c b/tools/asahi/tunnel-route/fixture.c index dd63853c3bd36..98133274f9ff8 100644 --- a/tools/asahi/tunnel-route/fixture.c +++ b/tools/asahi/tunnel-route/fixture.c @@ -50,7 +50,7 @@ struct apple_dcp_typec_port { struct apple_dcp_typec_route *routes, *owner, *secondary_owner, *preferred_route; struct apple_connector *connector, *secondary_connector; - bool dp_wanted, dp_hpd; + bool dp_wanted, dp_hpd, typec_yielded; }; static bool t6030; static bool of_machine_is_compatible(const char *compatible) @@ -128,6 +128,25 @@ int main(void) &chip.mux[1]); } } - puts("PASS: DCP snapshot, named/legacy controls, invalid DP IN, stale snapshot"); + /* T6030 has no display-engine clock for a tunnel on source 1. */ + struct apple_dcp_typec_route source1 = { + .dcp = &dcp, + .xbar = &chip.mux[0], + .mux_index = 1, + }; + for (unsigned int soc = 0; soc < 2; soc++) { + t6030 = soc; + for (unsigned int dpin = 0; dpin < 2; dpin++) + assert(dcp_typec_tunnel_ctl(&source1, dpin) == + (soc ? NULL : &chip.mux[1 + dpin])); + } + /* A display that yielded both pipes to an HDMI display wants none. */ + for (unsigned int yielded = 0; yielded < 2; yielded++) { + port.dp_wanted = true; + port.typec_yielded = yielded; + dcp_fabric_snapshot_port(&port, true); + assert(port.core.wanted == !yielded); + } + puts("PASS: DCP snapshot, named/legacy controls, invalid DP IN, stale snapshot, yielded port"); return 0; } From dda23b948860f44b3fe5c54a9a33425debab69c0 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 30 Oct 2026 17:22:05 +1100 Subject: [PATCH 26/40] tools/asahi: load mt7932_power by path in the 6 GHz policy tool mt7932-policy-6ghz.py imported its sibling mt7932_power through sys.path. Under 'python3 -I', which our host-test runs use, the script directory is not on sys.path, so the tool and its test failed with ModuleNotFoundError. Load the sibling from the tool's own directory. Assisted-by: LLM Signed-off-by: Ryan Murray Co-Authored-By: Claude Opus 5.5 --- tools/asahi/mt7932-policy-6ghz.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tools/asahi/mt7932-policy-6ghz.py b/tools/asahi/mt7932-policy-6ghz.py index 7c0dde2fecb12..a2f8a5e36b9c0 100644 --- a/tools/asahi/mt7932-policy-6ghz.py +++ b/tools/asahi/mt7932-policy-6ghz.py @@ -3,12 +3,24 @@ """Extend J7RP country files from authenticated IZUBA text data, without blobs.""" import argparse import hashlib +import importlib.util import json from pathlib import Path import re import struct +import sys -import mt7932_power as power + +def _sibling(name): + """Load a module from this directory, also under python3 -I or by path.""" + spec = importlib.util.spec_from_file_location(name, Path(__file__).with_name(name + '.py')) + module = importlib.util.module_from_spec(spec) + sys.modules[name] = module + spec.loader.exec_module(module) + return module + + +power = _sibling('mt7932_power') SIX_NAME = 'IZUBA_TxPwrLimit6G_MT79x1.dat' DB_NAME = 'IZUBA_db.dat' From d031104b8b8a7821e65d3e4757ec0bc1285b0e47 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 30 Oct 2026 17:40:34 +1100 Subject: [PATCH 27/40] tools/asahi: follow the variant table and suspend rollback in the bcm4377-resume test The bcm4377-resume host test stopped compiling after two driver changes. Commit 0877cd766126 ("arm64: apple: enable J713 Bluetooth with scoped radio DMA") made the hardware variant table name its context register through BCM4377_BAR2_CONTEXT_ADDR_HI and BCM4388_BAR2_CONTEXT_ADDR_HI. The test compiles that table unchanged but never had those macros. Commit afd51176986a ("Bluetooth: bcm4377: abort suspend if wake IRQ arming fails") made bcm4377_suspend() call bcm4377_resume() to undo a failed arming, through a forward declaration ahead of suspend. The test pastes suspend before resume, so the call became an implicit declaration that clashed with the later static definition. Take every #define the variant table refers to from the driver source, and take the forward declaration of bcm4377_resume() from the driver as well, so both follow the code instead of being restated in the fixture. The controls, assertions and the five mutations are unchanged, and each mutation is still killed by its control. Assisted-by: LLM Signed-off-by: Ryan Murray Co-Authored-By: Claude Opus 5.5 --- tools/asahi/bcm4377-resume-tests.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tools/asahi/bcm4377-resume-tests.py b/tools/asahi/bcm4377-resume-tests.py index 2eab82a96c1ca..4c7104ff1b458 100755 --- a/tools/asahi/bcm4377-resume-tests.py +++ b/tools/asahi/bcm4377-resume-tests.py @@ -35,6 +35,11 @@ def extract(s,n): hw_enum=re.search(r'^enum bcm4377_chip \{.*?^\};',s,re.M|re.S).group() hw_table=re.search(r'^static const struct bcm4377_hw bcm4377_hw_variants\[\] = \{.*?^\};',s,re.M|re.S).group() fixture=re.sub(r'^struct bcm4377_hw \{.*?\};', 'typedef uint32_t u32;\n'+hw_struct,fixture,count=1,flags=re.M) +# The variant table names register offsets by macro; take their definitions from the driver. +hw_macros='\n'+''.join(m.group()+'\n' for name in sorted(set(re.findall(r'\b[A-Z][A-Z0-9_]+\b',hw_table))) for m in [re.search(r'^#define '+name+r'\b.*$',s,re.M)] if m) +assert 'BAR2_CONTEXT_ADDR_HI' in hw_macros,hw_macros +# bcm4377_suspend() undoes a failed wake arming through bcm4377_resume(), declared ahead of it. +resume_decl=re.search(r'^static int bcm4377_resume\(struct device \*dev\);$',s,re.M).group()+'\n' fixture=fixture.replace('struct bcm4377_hw *hw;', 'const struct bcm4377_hw *hw;') table_callbacks=''' static int send_cal(struct bcm4377_data *); @@ -45,7 +50,7 @@ def extract(s,n): #define bcm4377_send_ptb send_ptb #define bcm4378_send_ptb send_ptb ''' -code=fixture+hw_enum+table_callbacks+hw_table+'\n'.join(bodies.values())+'\n'.join(core_bodies.values())+extract(core,'hci_reset_dev_sync')+(r/'tools/asahi/bcm4377-resume/controls.c').read_text() +code=fixture+hw_enum+hw_macros+table_callbacks+hw_table+resume_decl+'\n'.join(bodies.values())+'\n'.join(core_bodies.values())+extract(core,'hci_reset_dev_sync')+(r/'tools/asahi/bcm4377-resume/controls.c').read_text() (o/'production.c').write_text(code);env=dict(os.environ,TMPDIR=str(o/'tmp')) c=subprocess.run(['cc','-std=gnu11','-pthread','-Wall','-Wextra','-Wno-unused-function','-Wno-unused-parameter','-fsanitize=address,undefined','-fno-pie','-no-pie',str(o/'production.c'),'-o',str(o/'controls')],env=env,capture_output=True,text=True) (o/'compile.log').write_text(c.stdout+c.stderr);assert not c.returncode,c.stderr From 4d21609d1ad9f7e5d55bc1b9a19179178ec68c17 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 30 Oct 2026 17:42:02 +1100 Subject: [PATCH 28/40] tools/asahi: follow the grown variant table in the bcm4377-id test The bcm4377-id host test compiles the gate from before commit 8935b9f50620 ("Bluetooth: bcm4377: match the hardware table ID for J493 recovery") against the current hardware tables, and it required the hardware struct and the variant table to be byte for byte the same in the old and the current driver. Commit 0877cd766126 ("arm64: apple: enable J713 Bluetooth with scoped radio DMA") added a context register field to both, so that check failed with any pre-fix ref. The new table entries also name BCM4377_BAR2_CONTEXT_ADDR_HI and BCM4388_BAR2_CONTEXT_ADDR_HI, which the test never defined, so even the positive build stopped compiling. Keep the exact match for the chip enum, the PCI ID table, the probe selection and the reset registration, which is what the old gate is paired with. For the struct and the table, check what the gate reads instead, the id field and the same chip to ID mapping in both versions. Take every #define the variant table refers to from the driver source. All five builds keep their expected results, and with the pre-fix ref the original, decimal ID, other chip and board bypass gates still abort on the probe registration check. Assisted-by: LLM Signed-off-by: Ryan Murray Co-Authored-By: Claude Opus 5.5 --- tools/asahi/bcm4377-id-tests.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/tools/asahi/bcm4377-id-tests.py b/tools/asahi/bcm4377-id-tests.py index 6d5bdb4b68427..167139cfffbc4 100755 --- a/tools/asahi/bcm4377-id-tests.py +++ b/tools/asahi/bcm4377-id-tests.py @@ -97,15 +97,28 @@ def main(): } pieces = {name: take(current, pattern) for name, pattern in patterns.items()} old_pieces = {name: take(old, pattern) for name, pattern in patterns.items()} + # The old gate is compiled against the current tables. Later commits added + # hardware fields and variant settings, so the struct and table may differ, + # but the old gate must still see the same chip IDs, PCI table and probe code. for name in patterns: - if name != 'gate': + if name not in ('gate', 'hw_struct', 'variants'): assert pieces[name] == old_pieces[name], name + for src in (pieces, old_pieces): + assert re.search(r'^\tunsigned int id;$', src['hw_struct'], re.M), src['hw_struct'] + variant_ids = r'^\t\[(BCM\d+)\] = \{\n\t\t\.id = (0x[0-9a-f]+),' + current_ids = re.findall(variant_ids, pieces['variants'], re.M) + assert len(current_ids) == 4, current_ids + assert current_ids == re.findall(variant_ids, old_pieces['variants'], re.M), 'variant ids' defs = '\n'.join(re.findall(r'^#define BCM\d+_DEVICE_ID [^\n]+', current, re.M)) assert defs == '\n'.join(re.findall(r'^#define BCM\d+_DEVICE_ID [^\n]+', old, re.M)) ids = (root / 'include/linux/pci_ids.h').read_text() pci_defs = '\n'.join(re.findall(r'^#define (?:PCI_VENDOR_ID_BROADCOM|PCI_CLASS_NETWORK_OTHER)\s+[^\n]+', ids, re.M)) any_id = take((root / 'include/linux/device-id/pci.h').read_text(), r'^#define PCI_ANY_ID[^\n]+') - source_prefix = PREFIX + defs + '\n' + pci_defs + '\n' + any_id + '\n' + '\n'.join(pieces[n] for n in ('enum', 'hw_struct', 'variants', 'pci_macro_table')) + # Register offsets the variant table names by macro, as defined in the driver. + table_macros = sorted(set(re.findall(r'\b[A-Z][A-Z0-9_]+\b', pieces['variants']))) + hw_defs = '\n'.join(m.group() for name in table_macros for m in [re.search(r'^#define ' + name + r'\b[^\n]*', current, re.M)] if m) + assert 'BAR2_CONTEXT_ADDR_HI' in hw_defs, hw_defs + source_prefix = PREFIX + defs + '\n' + hw_defs + '\n' + pci_defs + '\n' + any_id + '\n' + '\n'.join(pieces[n] for n in ('enum', 'hw_struct', 'variants', 'pci_macro_table')) suffix = SUFFIX.replace('@SELECTION@', pieces['selection']).replace('@REGISTRATION@', pieces['registration']) gates = {'positive': pieces['gate'], 'published-original': old_pieces['gate']} assert pieces['gate'].count('0x4378') == 1 @@ -124,7 +137,7 @@ def no_core(): (args.out / f'{name}.log').write_text(run.stdout + run.stderr) assert run.returncode == (0 if name == 'positive' else -6), (name, run.returncode) results[name] = {'exit': run.returncode, 'source_sha256': sha(source.read_text()), 'stdout': run.stdout.strip()} - receipt = {'source_sha256': sha(current), 'old_source_sha256': sha(old), 'old_ref': args.old_ref, 'actual_inputs': {name: sha(value) for name, value in pieces.items()}, 'device_ids_sha256': sha(defs), 'pci_defs_sha256': sha(pci_defs + any_id), 'results': results, 'scope': 'Actual variant/PCI tables, hardware struct, probe selection statement, reset registration block and admission function. PCI enumeration, OF board matching and unrelated probe operations are controlled; no full probe or hardware execution.'} + receipt = {'source_sha256': sha(current), 'old_source_sha256': sha(old), 'old_ref': args.old_ref, 'actual_inputs': {name: sha(value) for name, value in pieces.items()}, 'device_ids_sha256': sha(defs), 'table_macros_sha256': sha(hw_defs), 'variant_ids': current_ids, 'pci_defs_sha256': sha(pci_defs + any_id), 'results': results, 'scope': 'Actual variant/PCI tables, hardware struct, probe selection statement, reset registration block and admission function. PCI enumeration, OF board matching and unrelated probe operations are controlled; no full probe or hardware execution.'} (args.out / 'receipt.json').write_text(json.dumps(receipt, indent=2) + '\n') print(json.dumps(receipt, indent=2)) From 075e86aa44baefb147f0cb212dd62a17e2157431 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 30 Oct 2026 17:43:01 +1100 Subject: [PATCH 29/40] tools/asahi: follow the max3() conversion in the dma-heap-fd test The dma-heap-fd host test compiles the real dma_heap_ioctl() against a small user space harness that defines max() but not max3(). Commit d59676b3ebad ("dma-buf: heaps: use max3 in dma_heap_ioctl") came in with the 7.3 base and changed the ioctl to size its buffer with max3(), so the positive build stopped compiling with an implicit declaration error. Define max3() in the harness the same way the kernel composes it from max(). The bodies under test, the old ref build and all five mutations are unchanged. With the parent of 30d0aff2c65a ("dma-buf: dma-heap: don't publish fd before copy_to_user() succeeds") as the old ref, the positive build passes at both page sizes and the original and every mutation still abort on their assertions. Assisted-by: LLM Signed-off-by: Ryan Murray Co-Authored-By: Claude Opus 5.5 --- tools/asahi/dma-heap-fd-tests.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/asahi/dma-heap-fd-tests.py b/tools/asahi/dma-heap-fd-tests.py index f40d0709d58a8..4bf6770547efe 100755 --- a/tools/asahi/dma-heap-fd-tests.py +++ b/tools/asahi/dma-heap-fd-tests.py @@ -45,6 +45,7 @@ def function(source, name): #define PAGE_ALIGN(x) (((x)+PAGE_SIZE-1)&~(uint64_t)(PAGE_SIZE-1)) #define ARRAY_SIZE(x) (sizeof(x)/sizeof((x)[0])) #define max(a,b) ((a)>(b)?(a):(b)) +#define max3(a,b,c) max(max(a,b),c) #define IOC_IN (_IOC_WRITE << _IOC_DIRSHIFT) #define IOC_OUT (_IOC_READ << _IOC_DIRSHIFT) #define array_index_nospec(x,n) (x) From 6ea8034db6eb43c41e161dd74b3e139b12987bb9 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 30 Oct 2026 17:39:47 +1100 Subject: [PATCH 30/40] tools/asahi: follow the closed-client logging in the g15-input-error test The test copies the real VmStatus::report_failure body into a host prelude. Commit f227c7671c0f ("drm/asahi: log the first GPU work failure reported to each G17 VM") made that body use a warned flag, and commit 769e0b0f6214 ("drm/asahi: g17: log a closed client's teardown failures at debug level") made it call unobserved_teardown and mod_pr_debug. The prelude had none of these, so the control no longer built. Give the host VmStatus the client_closed and warned fields, add ENOENT and a mod_pr_debug stub next to pr_warn, and extract the real unobserved_teardown body with the other status methods. The control and all nine mutations run as before, and the removed-fix control still reproduces the lost input error with --ref ffe8011a479c^. Assisted-by: LLM Signed-off-by: Ryan Murray Co-Authored-By: Claude Opus 5.5 --- tools/asahi/g15-input-error-tests.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tools/asahi/g15-input-error-tests.py b/tools/asahi/g15-input-error-tests.py index 47971fccfa710..809411963fd17 100644 --- a/tools/asahi/g15-input-error-tests.py +++ b/tools/asahi/g15-input-error-tests.py @@ -32,13 +32,14 @@ def extract(text, marker): impl Error { fn from_errno(n:i32)->Self {assert!(n<0);Self(n)} fn to_errno(self)->i32{self.0} } type Result = std::result::Result; const EIO:Error=Error(-5);const ENODEV:Error=Error(-19);const ETIMEDOUT:Error=Error(-110); -const ECANCELED:Error=Error(-125);const EINVAL:Error=Error(-22);const ENOMEM:Error=Error(-12); +const ECANCELED:Error=Error(-125);const EINVAL:Error=Error(-22);const ENOMEM:Error=Error(-12);const ENOENT:Error=Error(-2); macro_rules! pr_warn { ($($arg:tt)*)=>{{ let _=format_args!($($arg)*); }} } +macro_rules! mod_pr_debug { ($($arg:tt)*)=>{{ let _=format_args!($($arg)*); }} } struct Mutex(StdMutex);impl Mutex{fn lock(&self)->MutexGuard<'_,T>{self.0.lock().unwrap()}} struct Mirror{word:*mut i32} -struct VmStatus{error:AtomicI32,reported_error:AtomicI32,mirror:Mutex>} +struct VmStatus{error:AtomicI32,reported_error:AtomicI32,client_closed:AtomicBool,warned:AtomicBool,mirror:Mutex>} impl VmStatus { - fn new(word:*mut i32)->Self{Self{error:AtomicI32::new(0),reported_error:AtomicI32::new(0),mirror:Mutex(StdMutex::new(Some(Mirror{word})))}} + fn new(word:*mut i32)->Self{Self{error:AtomicI32::new(0),reported_error:AtomicI32::new(0),client_closed:AtomicBool::new(false),warned:AtomicBool::new(false),mirror:Mutex(StdMutex::new(Some(Mirror{word})))}} __STATUS__ } mod g17 {use super::*;__POLICY__} @@ -137,7 +138,8 @@ def main(): bodies = "\n".join(methods) code = PRELUDE.replace("__JOB__", bodies).replace("__STATUS__", "\n".join( extract(status, marker) for marker in [" pub(crate) fn get(", " pub(crate) fn record(", - " pub(crate) fn reported_error(", " pub(crate) fn report_failure("])).replace( + " pub(crate) fn reported_error(", " pub(crate) fn report_failure(", + " fn unobserved_teardown("])).replace( "__POLICY__", extract(policy, "pub(crate) fn inherited_error(")) variants = {"control": code} expected = {} From 17cd686c4de60fb05df2f23ac027e37fd2b4a66a Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 30 Oct 2026 17:40:45 +1100 Subject: [PATCH 31/40] tools/asahi: follow the moved queue info setup in the compute-preempt test The test extracts the priority policy from WorkQueue::new as the text between the PRIORITY lookup and the WorkQueueInner literal. Commit ab30807676fb ("drm/asahi: keep work-queue firmware structures until the context is invalidated") moved the QueueInfo allocation out of that literal to just after the policy, so the extracted body pulled in try_init!, alloc, gpu_context and DebugFlags::VerboseFaults and no longer built. End the extracted body at the close of the compute priority override, which is the last statement of the policy in the current driver and in the older ones. All four mutations are unchanged and still fail, and --old-ref c8aed729e9bf^ still fails the same assertions. Assisted-by: LLM Signed-off-by: Ryan Murray Co-Authored-By: Claude Opus 5.5 --- tools/asahi/compute-preempt-tests.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tools/asahi/compute-preempt-tests.py b/tools/asahi/compute-preempt-tests.py index fdab1e9e7406b..ed246a3e69060 100755 --- a/tools/asahi/compute-preempt-tests.py +++ b/tools/asahi/compute-preempt-tests.py @@ -29,7 +29,9 @@ def read(path, ref=None): return (root / path).read_text() def policy(text): start = text.index(' let mut prio = *raw::PRIORITY.get(priority as usize).ok_or(EINVAL)?;') - end = text.index(' let inner = WorkQueueInner::ver {', start) + # The policy ends with the compute priority override; queue allocation follows. + tail = ' prio.5 = 1;\n }\n' + end = text.index(tail, start) + len(tail) return text[start:end] queue = read('drivers/gpu/drm/asahi/workqueue.rs', args.ref) macro = read('rust/macros/versions.rs', args.ref) From d9a2cfeea54ea292ad614f3a1fd8323e3c3ca68c Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 30 Oct 2026 17:40:19 +1100 Subject: [PATCH 32/40] tools/asahi: follow the IRQ-safe PMP report entries in the T6030 graph test The PMP report entry probe gained an apple,irq-safe opt-in that sets GENPD_FLAG_IRQ_SAFE (540f52fa22b1). The host fixture had no such flag, so the extracted probe stopped compiling. Define the flag, let the fake OF node carry apple,irq-safe and read it from the overlay alongside the other entry properties. Check that each entry gets the ACTIVE_WAKEUP and IRQ_SAFE flags its overlay node asks for, and add a case where only one entry opts in. Two new mutations drop the flag and set it without the property, and both are killed. Assisted-by: LLM Signed-off-by: Ryan Murray Co-Authored-By: Claude Opus 5.5 --- tools/asahi/t6030-pmp-graph-tests.c | 27 +++++++++++++++++++++++++-- tools/asahi/t6030-pmp-graph-tests.py | 7 +++++-- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/tools/asahi/t6030-pmp-graph-tests.c b/tools/asahi/t6030-pmp-graph-tests.c index be106bf7e3d1f..9f018ee94d628 100644 --- a/tools/asahi/t6030-pmp-graph-tests.c +++ b/tools/asahi/t6030-pmp-graph-tests.c @@ -22,6 +22,7 @@ typedef uint8_t u8; #define PMP_REPORT_READY 1 #define GFP_KERNEL 0 #define GENPD_FLAG_ACTIVE_WAKEUP 1 +#define GENPD_FLAG_IRQ_SAFE 2 #define MAX_PHANDLE_ARGS 4 #define EPROBE_DEFER 517 #define SZ_4K 4096 @@ -44,7 +45,7 @@ static void host_log(const char *format, ...) #define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) struct device_node { const char *name, *compatible; - bool available, always_on, no_ack, deferred, uuid, soc_record; + bool available, always_on, no_ack, deferred, irq_safe, uuid, soc_record; u32 reg; int refs; struct device_node *parent, *child, *sibling, *pmp; @@ -387,6 +388,8 @@ static bool of_property_read_bool(const struct device_node *n, const char *p) return n->no_ack; if (!strcmp(p, "apple,defer-startup-ack")) return n->deferred; + if (!strcmp(p, "apple,irq-safe")) + return n->irq_safe; if (!strcmp(p, "apple,soc-device-record")) return n->soc_record; return false; @@ -620,7 +623,7 @@ static int of_platform_populate(struct device_node *n, void *a, void *b, } struct overlay_entry { u32 reg; - bool always_on, no_ack, deferred; + bool always_on, no_ack, deferred, irq_safe; }; #include "overlay.h" static void cleanup(void) @@ -669,6 +672,7 @@ static struct apple_pmp_report *setup(void) .always_on = o->always_on, .no_ack = o->no_ack, .deferred = o->deferred, + .irq_safe = o->irq_safe, .parent = &report_node, .sibling = i < 3 ? &entries[i + 1] : NULL }; @@ -727,6 +731,14 @@ int main(void) assert(regs[0x1180 / 8] == r->start->seed); assert(((struct apple_pmp_report_entry *)entry_devs[3].dev.data) ->no_ack); + /* genpd flags follow the overlay: always-on wakes, irq-safe opts in. */ + for (unsigned int i = 0; i < 4; i++) { + const struct apple_pmp_report_entry *e = entry_devs[i].dev.data; + assert(!!(e->genpd.flags & GENPD_FLAG_ACTIVE_WAKEUP) == + overlay_entries[i].always_on); + assert(!!(e->genpd.flags & GENPD_FLAG_IRQ_SAFE) == + overlay_entries[i].irq_safe); + } assert(apple_pmp_report_wait_ready(&entries[0], 30000) == 0 && enables == 1 && pmp_node.available); assert(g15_vote() == 0 && commands == 1 && enables == 1); @@ -747,6 +759,17 @@ int main(void) assert(g15_vote() == -EIO && commands == 1); cases++; cleanup(); + /* Only an entry that carries apple,irq-safe becomes an IRQ-safe domain. */ + setup(); + entries[1].irq_safe = true; + assert(apple_pmp_report_probe(&report_dev) == 0); + for (unsigned int i = 0; i < 4; i++) { + const struct apple_pmp_report_entry *e = entry_devs[i].dev.data; + assert(!!(e->genpd.flags & GENPD_FLAG_IRQ_SAFE) == + (i == 1 || overlay_entries[i].irq_safe)); + } + cases++; + cleanup(); /* Missing display acknowledgement cannot release G15, even if bound+READY. */ setup(); assert(apple_pmp_report_probe(&report_dev) == 0); diff --git a/tools/asahi/t6030-pmp-graph-tests.py b/tools/asahi/t6030-pmp-graph-tests.py index af7c849401d3b..6ba26a09c0176 100644 --- a/tools/asahi/t6030-pmp-graph-tests.py +++ b/tools/asahi/t6030-pmp-graph-tests.py @@ -30,9 +30,10 @@ def main(): entries = [] for node in re.findall(r'report@[0-9a-f]+\s*\{(.*?)\n\s*\};', overlay, re.S): reg = int(re.search(r'reg = <(0x[0-9a-f]+|[0-9]+)>', node)[1], 0) - entries.append((reg, 'apple,always-on;' in node, 'apple,no-ack;' in node, 'apple,defer-startup-ack;' in node)) + entries.append((reg, 'apple,always-on;' in node, 'apple,no-ack;' in node, 'apple,defer-startup-ack;' in node, + 'apple,irq-safe;' in node)) assert len(entries) == 4 - entry_data = ',\n'.join('{%d, %d, %d, %d}' % e for e in entries) + entry_data = ',\n'.join('{%d, %d, %d, %d, %d}' % e for e in entries) names = ['static int apple_pmp_report_bit(', 'static int apple_pmp_start_check_layout(', 'static int apple_pmp_start_describe(', 'static void apple_pmp_start_finish(', 'static bool apple_pmp_start_stopping(', 'static void apple_pmp_start_work(', @@ -81,6 +82,8 @@ def main(): ('ignore-startup-ack', '(ack & st->startup_ack) != st->startup_ack', 'false'), ('ignore-entry-ack', 'status == PMP_REPORT_READY && (ack & BIT_ULL(id))', '((void)ack, status == PMP_REPORT_READY)'), ('storage-ack-layout', 'st->ack & ~st->record_seed,', 'st->seed & ~st->record_seed,'), + ('ignore-irq-safe', 'ent->genpd.flags |= GENPD_FLAG_IRQ_SAFE;', '(void)0;'), + ('irq-safe-without-property', 'of_property_read_bool(node, "apple,irq-safe")', 'true'), ('skip-supplier-wait', 'apple_pmp_report_wait_ready(entry, msecs_to_jiffies(timeout_ms))', '((void)timeout_ms, 0)'), ] for name, old, new in controls: From 09ac0e85fa2e42183d39016a86e21085ce882301 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 30 Oct 2026 17:43:22 +1100 Subject: [PATCH 33/40] tools/asahi: follow the AV1 segmentation map tail in the AVD test The AV1 capture tail gained a per-frame segmentation map below the CDFs (57b0e7c91643), and the preamble now refuses frames larger than the capture format and out-of-range primary references (9fb5f796fed2). The host test did not extract avd_av1_seg_size() and its fixture had no decoded format or s8, so it stopped compiling. Extract avd_av1_seg_size() and avd_av1_set_seg(), give the fixture buffers a DMA address, and give the context a decoded format. The reservation check now counts the map and places it between the compressed data and the CDFs, and the preamble check covers the map address. New cases cover the size and reference index guards, and the map that set_seg zeroes, copies from a reference with the same MiCols and MiRows, or leaves for the AVD, with the CPU and DMA offsets agreeing. Every mutation now names the text it rewrites and is refused if the driver no longer contains it. Four new ones break the map CPU offset, the MiCols match, the reference index bound and the frame width guard, and all six are killed. Assisted-by: LLM Signed-off-by: Ryan Murray Co-Authored-By: Claude Opus 5.5 --- tools/asahi/avd-av1-tail-tests.py | 105 +++++++++++++++++++++++++----- 1 file changed, 88 insertions(+), 17 deletions(-) diff --git a/tools/asahi/avd-av1-tail-tests.py b/tools/asahi/avd-av1-tail-tests.py index 8a36298013af6..d602801b7c0a7 100644 --- a/tools/asahi/avd-av1-tail-tests.py +++ b/tools/asahi/avd-av1-tail-tests.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # SPDX-License-Identifier: GPL-2.0-only OR MIT -"""AV1 auxiliary alignment, probability inheritance and reservation controls.""" +"""AV1 auxiliary alignment, probability and segmentation map inheritance, and reservation controls.""" import argparse import hashlib import json @@ -44,20 +44,22 @@ def structure(source, name): #include #include "controls.h" typedef uint8_t u8; +typedef int8_t s8; typedef uint16_t u16; typedef uint32_t u32; typedef uint64_t dma_addr_t; #define ALIGN(x,a) (((x)+(a)-1)&~((__typeof__(x))(a)-1)) #define ALIGN_DOWN(x,a) ((x)&~((__typeof__(x))(a)-1)) #define WARN_ON(x) (x) +#define DIV_ROUND_UP(n,d) (((n)+(d)-1)/(d)) @ALIGNMENT@ @CDF_HEADER@ @CDF_DEFAULTS@ -struct vb2_buffer {struct {unsigned int length;} planes[1]; unsigned char *memory;}; +struct vb2_buffer {struct {unsigned int length;} planes[1]; unsigned char *memory; dma_addr_t dma;}; struct vb2_v4l2_buffer {struct vb2_buffer vb2_buf;}; struct avd_decoded_buffer { struct {struct vb2_v4l2_buffer vb;} base; - struct {unsigned int color_size;} av1; + struct {unsigned int color_size; unsigned short width,height; size_t seg_size;} av1; }; struct avd_buf {void *cpu; dma_addr_t addr;}; struct avd_run {struct {struct vb2_v4l2_buffer *dst;} bufs; dma_addr_t y_out;}; @@ -65,7 +67,7 @@ def structure(source, name): @CTX_STRUCT@ struct v4l2_ctrl {struct {void *p;} p_cur;}; struct v4l2_pix_format_mplane {unsigned int width,height; struct {unsigned int sizeimage;} plane_fmt[1];}; -struct avd_ctx {void *priv; int ctrl_hdl;}; +struct avd_ctx {void *priv; int ctrl_hdl; struct {struct {struct {unsigned int width,height;} pix_mp;} fmt;} decoded_fmt;}; static struct avd_decoded_buffer *reference; static struct v4l2_ctrl_av1_frame frame; static struct v4l2_ctrl_av1_sequence sequence; @@ -74,6 +76,7 @@ def structure(source, name): static struct v4l2_ctrl controls[4]; static unsigned int lookup_calls, preamble_calls; static void *vb2_plane_vaddr(struct vb2_buffer *b,unsigned int p){assert(p==0);return b->memory;} +static dma_addr_t vb2_dma_contig_plane_dma_addr(struct vb2_buffer *b,unsigned int p){assert(p==0);return b->dma;} static struct avd_decoded_buffer *vb2_to_avd_decoded_buf(struct vb2_buffer *b){return (void *)b;} static struct avd_decoded_buffer *avd_get_ref_buf(struct avd_ctx *ctx,struct vb2_v4l2_buffer *dst,uint64_t timestamp){(void)ctx;(void)dst;assert(timestamp==1000);lookup_calls++;return reference;} static void avd_run_preamble(struct avd_ctx *ctx,struct avd_run *run){(void)ctx;(void)run;preamble_calls++;} @@ -89,11 +92,14 @@ def structure(source, name): TESTS = r''' static size_t aligned_cdf(size_t length,size_t color){return (length&~255UL)-((color+255)&~255UL)-9216;} +/* 3 bits per 4x4 block over 128x128 superblocks, 256-byte aligned. */ +static size_t seg_bytes(size_t w,size_t h){return ALIGN(ALIGN(w,128)*ALIGN(h,128)/128*3,256);} +static void set_frame_ctx(struct avd_ctx *ctx){ctx->decoded_fmt.fmt.pix_mp.width=1920;ctx->decoded_fmt.fmt.pix_mp.height=1080;controls[0].p_cur.p=&sequence;controls[1].p_cur.p=&frame;controls[2].p_cur.p=&tile;controls[3].p_cur.p=&grain;} static void read_exact(const char *path,void *data,size_t n){FILE *f=fopen(path,"rb");assert(f);assert(fread(data,1,n,f)==n);assert(fgetc(f)==EOF);assert(fclose(f)==0);} static void inheritance(unsigned int residue,unsigned int mode,const char *neighbors,bool check_addresses){ const size_t ref_len=6964992+residue,dst_len=7000064+(residue?((residue+79)%256):0); const size_t color=138240,ref_offset=aligned_cdf(ref_len,color),dst_offset=aligned_cdf(dst_len,color); - struct avd_decoded_buffer src={0},dst={0};struct avd_av1_ctx av1={0};struct avd_ctx ctx={.priv=&av1};struct avd_av1_run run={0}; + struct avd_decoded_buffer src={0},dst={0};struct avd_av1_ctx av1={0};struct avd_ctx ctx={.priv=&av1};struct avd_av1_run run={0};set_frame_ctx(&ctx); struct avd_av1_cdfs expected={0};unsigned char probabilities[sizeof(expected)]; src.base.vb.vb2_buf.planes[0].length=ref_len;src.base.vb.vb2_buf.memory=malloc(ref_len);assert(src.base.vb.vb2_buf.memory); dst.base.vb.vb2_buf.planes[0].length=dst_len;dst.base.vb.vb2_buf.memory=malloc(dst_len);assert(dst.base.vb.vb2_buf.memory); @@ -108,7 +114,7 @@ def structure(source, name): controls[0].p_cur.p=&sequence;controls[1].p_cur.p=&frame;controls[2].p_cur.p=&tile;controls[3].p_cur.p=&grain; run.base.bufs.dst=&dst.base.vb;run.base.y_out=0x120000000UL;av1.bufs.probs.cpu=probabilities;lookup_calls=0;preamble_calls=0; assert(avd_av1_run_preamble(&ctx,&run)==0);assert(preamble_calls==1); - if(check_addresses){assert(run.addresses.probs_out==run.base.y_out+dst_offset);assert(run.addresses.color==run.base.y_out+dst_offset+9216);} + if(check_addresses){assert(run.addresses.probs_out==run.base.y_out+dst_offset);assert(run.addresses.color==run.base.y_out+dst_offset+9216);assert(run.addresses.seg==run.base.y_out+dst_offset-seg_bytes(1920,1080));} avd_av1_set_prob(&ctx,&run); assert(lookup_calls==(mode?0:1));assert(memcmp(probabilities,&expected,sizeof(expected))==0); assert(memcmp(dst.base.vb.vb2_buf.memory+dst_offset,&expected,sizeof(expected))==0); @@ -120,18 +126,68 @@ def structure(source, name): struct v4l2_pix_format_mplane pix={.width=width,.height=height};struct avd_ctx ctx={0};pix.plane_fmt[0].sizeimage=compressed; avd_av1_adjust_decoded_fmt(&ctx,&pix); size_t min=pix.plane_fmt[0].sizeimage,color=avd_color_size(width,height),length=min+residue; - assert(min%256==0);assert(min==ALIGN(compressed,256)+9216+ALIGN(color,256)); - size_t cdf=AVD_AV1_CDFS_OFFSET(length,color),co=AVD_AV1_COLOR_OFFSET(length,color); + size_t seg=seg_bytes(width,height); + assert(min%256==0);assert(min==ALIGN(compressed,256)+9216+ALIGN(color,256)+seg); + size_t cdf=AVD_AV1_CDFS_OFFSET(length,color),co=AVD_AV1_COLOR_OFFSET(length,color),sg=AVD_AV1_SEG_OFFSET(length,color,seg); + assert(sg%256==0);assert(sg>=ALIGN(compressed,256));assert(sg+seg==cdf); assert(cdf%256==0&&co%256==0);assert(cdf>=compressed);assert(cdf+sizeof(struct avd_av1_cdfs)<=co);assert(co+color<=length); assert(cdf==aligned_cdf(length,color));assert(co==cdf+9216); } +/* The preamble refuses frames larger than the format and out-of-range reference indices. */ +static int preamble(unsigned int w,unsigned int h,unsigned char primary,signed char idx){ + struct avd_decoded_buffer dst={0};struct avd_av1_ctx av1={0};struct avd_ctx ctx={.priv=&av1};struct avd_av1_run run={0};set_frame_ctx(&ctx); + dst.base.vb.vb2_buf.planes[0].length=7000064;memset(&frame,0,sizeof(frame)); + frame.frame_width_minus_1=w-1;frame.frame_height_minus_1=h-1;frame.primary_ref_frame=primary;if(primary<7)frame.ref_frame_idx[primary]=idx; + run.base.bufs.dst=&dst.base.vb;run.base.y_out=0x120000000UL;return avd_av1_run_preamble(&ctx,&run); +} +static void guards(void){ + assert(preamble(1920,1080,0,0)==0);assert(preamble(1280,720,0,0)==0); + assert(preamble(1921,1080,0,0)==-EINVAL);assert(preamble(1920,1081,0,0)==-EINVAL); + assert(preamble(1920,1080,7,0)==0);assert(preamble(1920,1080,8,0)==-EINVAL);assert(preamble(1920,1080,255,0)==-EINVAL); + assert(preamble(1920,1080,6,7)==0);assert(preamble(1920,1080,6,8)==-EINVAL);assert(preamble(1920,1080,0,-1)==-EINVAL);assert(preamble(1920,1080,3,-128)==-EINVAL); +} +/* Segmentation map kept with each frame: expect 0 zeroes it, 1 copies the reference's, 2 leaves the AVD's map. */ +static void segmap(unsigned char flags,unsigned char primary,unsigned int ref_w,unsigned int ref_h,bool missing,int expect){ + const size_t len=7000064+79,ref_len=6964992+64+(ref_w*ref_h>1920*1080?65536:0),color=138240,seg=seg_bytes(1920,1080),ref_seg=seg_bytes(ref_w,ref_h); + const size_t off=aligned_cdf(len,color)-seg,roff=aligned_cdf(ref_len,color)-ref_seg;const dma_addr_t none=0x140000000UL; + struct avd_decoded_buffer src={0},dst={0};struct avd_av1_ctx av1={0};struct avd_ctx ctx={.priv=&av1};struct avd_av1_run run={0};set_frame_ctx(&ctx); + dst.base.vb.vb2_buf.planes[0].length=len;dst.base.vb.vb2_buf.memory=malloc(len);dst.base.vb.vb2_buf.dma=0x120000000UL;assert(dst.base.vb.vb2_buf.memory); + src.base.vb.vb2_buf.planes[0].length=ref_len;src.base.vb.vb2_buf.memory=malloc(ref_len);src.base.vb.vb2_buf.dma=0x130000000UL;assert(src.base.vb.vb2_buf.memory); + dst.av1.color_size=color;dst.av1.seg_size=seg;src.av1.color_size=color;src.av1.seg_size=ref_seg;src.av1.width=ref_w-1;src.av1.height=ref_h-1; + memset(dst.base.vb.vb2_buf.memory,0xa5,len);memset(src.base.vb.vb2_buf.memory,0x3c,ref_len); + for(size_t i=0;i=roff&&i1&&(strcmp(argv[1],"snapshot")==0||strcmp(argv[1],"snapshot-copy")==0)){assert(argc==3);inheritance(64,0,argv[2],strcmp(argv[1],"snapshot")==0);puts("snapshot inheritance PASS");return 0;} if(argc>1&&strcmp(argv[1],"copy")==0){inheritance(64,0,NULL,false);puts("probability inheritance PASS");return 0;} if(argc>1&&strcmp(argv[1],"seed")==0){inheritance(64,1,NULL,false);puts("default destination seed PASS");return 0;} if(argc>1&&strcmp(argv[1],"aligned")==0){for(unsigned int mode=0;mode<=4;mode++)inheritance(0,mode,NULL,true);reservation(1920,1080,6425600,0);puts("aligned compatibility PASS");return 0;} unsigned int cases=0; + guards();cases++;cases+=segmaps(); for(unsigned int residue=0;residue<256;residue++){ inheritance(residue,0,NULL,true);cases++; for(unsigned int mode=1;mode<=4;mode++){inheritance(residue,mode,NULL,true);cases++;} @@ -148,21 +204,36 @@ def main(): parser.add_argument('--out', type=Path, required=True) parser.add_argument('--neighbors', type=Path) parser.add_argument('--case', choices=['all', 'snapshot', 'snapshot-copy', 'copy', 'seed', 'aligned'], default='all') - parser.add_argument('--mutation', choices=['round-up', 'cpu-unaligned']) + parser.add_argument('--mutation', choices=['round-up', 'cpu-unaligned', 'seg-cpu-unaligned', 'seg-any-size', 'ref-index-upper', 'frame-width-unchecked']) args = parser.parse_args() args.out.mkdir(parents=True, exist_ok=True) paths = {name: args.source / DRIVER / name for name in ['avd-av1.c', 'avd-av1-entropymode.h', 'avd-av1-entropymode.c', 'avd.h']} text = {name: p.read_text() for name, p in paths.items()} src = text['avd-av1.c'] macros = src[src.index('#define AVD_CDFS_SIZE'):src.index('struct avd_av1_run {')] - functions = {n: function(src, n) for n in ['avd_av1_set_prob', 'avd_color_size', 'avd_av1_run_preamble', 'avd_av1_adjust_decoded_fmt']} - if args.mutation == 'round-up': - if 'ALIGN_DOWN((dst), AVD_ALIGN)' not in macros: - parser.error('round-up mutation requires aligned tail macros') - macros = macros.replace('ALIGN_DOWN((dst), AVD_ALIGN)', 'ALIGN((dst), AVD_ALIGN)') - elif args.mutation == 'cpu-unaligned': - functions['avd_av1_set_prob'] = functions['avd_av1_set_prob'].replace('AVD_AV1_CDFS_OFFSET(', 'UNALIGNED_CDFS_OFFSET(') - macros += '\n#define UNALIGNED_CDFS_OFFSET(dst,cl) ((dst)-ALIGN(cl,AVD_ALIGN)-ALIGN(AVD_CDFS_SIZE,AVD_ALIGN))\n' + functions = {n: function(src, n) for n in ['avd_av1_set_prob', 'avd_av1_set_seg', 'avd_av1_seg_size', 'avd_color_size', 'avd_av1_run_preamble', 'avd_av1_adjust_decoded_fmt']} + unaligned = ('\n#define UNALIGNED_CDFS_OFFSET(dst,cl) ((dst)-ALIGN(cl,AVD_ALIGN)-ALIGN(AVD_CDFS_SIZE,AVD_ALIGN))\n' + '#define UNALIGNED_SEG_OFFSET(dst,cl,sl) (UNALIGNED_CDFS_OFFSET(dst,cl)-(sl))\n') + # Each mutation names the extracted text it rewrites; a stale one is refused, never run as a no-op. + mutations = { + 'round-up': (None, 'ALIGN_DOWN((dst), AVD_ALIGN)', 'ALIGN((dst), AVD_ALIGN)'), + 'cpu-unaligned': ('avd_av1_set_prob', 'AVD_AV1_CDFS_OFFSET(', 'UNALIGNED_CDFS_OFFSET('), + 'seg-cpu-unaligned': ('avd_av1_set_seg', 'AVD_AV1_SEG_OFFSET(dst->', 'UNALIGNED_SEG_OFFSET(dst->'), + 'seg-any-size': ('avd_av1_set_seg', 'DIV_ROUND_UP(ref->av1.width + 1, 8)', 'DIV_ROUND_UP(frame->frame_width_minus_1 + 1, 8)'), + 'ref-index-upper': ('avd_av1_run_preamble', 'idx < 0 || idx >= V4L2_AV1_TOTAL_REFS_PER_FRAME', 'idx < 0'), + 'frame-width-unchecked': ('avd_av1_run_preamble', 'run->frame->frame_width_minus_1 + 1 > ctx->decoded_fmt.fmt.pix_mp.width ||', '0 ||'), + } + if args.mutation: + target, old, new = mutations[args.mutation] + body = macros if target is None else functions[target] + if old not in body: + parser.error(args.mutation + ' mutation no longer matches the driver source') + body = body.replace(old, new) + if target is None: + macros = body + else: + functions[target] = body + macros += unaligned header = text['avd-av1-entropymode.h'].replace('#include ', '') defaults = re.sub(r'^#include .*$', '', text['avd-av1-entropymode.c'], flags=re.M) alignment = re.search(r'^#define AVD_ALIGN\s+\d+', text['avd.h'], re.M).group() From b198230304158ba6c2d777c9dbef641511dd3e3d Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 30 Oct 2026 17:44:58 +1100 Subject: [PATCH 34/40] tools/asahi: follow the HT capability log in the MT7932 association test The association capability event gained a dynamic debug line for the generated HT capabilities (b23d29d6f661). It looks the element up with cfg80211_find_elem(), which the host fixture did not provide, so the extracted mt_link_event() stopped compiling. Define WLAN_EID_HT_CAPABILITY, give struct element its real id and length header, and add a cfg80211_find_elem() that walks only the IEs it is given. The capability event had no case before, so add one where a generated HT element is exported and the lookup reads the driver's copy, and one where a malformed element fails the attempt without any lookup. The four existing mutations are unchanged and still killed. Assisted-by: LLM Signed-off-by: Ryan Murray Co-Authored-By: Claude Opus 5.5 --- tools/asahi/mt7932-association-events-tests.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tools/asahi/mt7932-association-events-tests.py b/tools/asahi/mt7932-association-events-tests.py index 3f352376a1c82..436ab4d00b75f 100644 --- a/tools/asahi/mt7932-association-events-tests.py +++ b/tools/asahi/mt7932-association-events-tests.py @@ -52,6 +52,8 @@ def function(source, name): #define NL80211_CHAN_WIDTH_40 1 #define NL80211_CHAN_WIDTH_80 2 #define WLAN_REASON_UNSPECIFIED 1 +#define WLAN_EID_HT_CAPABILITY 45 +struct element {u8 id,datalen,data[];}; #define WLAN_EID_EXTENSION 255 #define WLAN_EID_EXT_HE_CAPABILITY 35 #define dev_info(...) ((void)0) @@ -74,7 +76,6 @@ def function(source, name): static void complete_all(struct completion*c){c->done=UINT_MAX;} struct ieee80211_channel {int center_freq;}; struct cfg80211_chan_def {struct ieee80211_channel *chan;int width,center_freq1;}; -struct element {u8 data[32];}; struct test_band {struct {bool ht_supported;}ht_cap;}; struct test_wiphy {struct test_band *bands[NUM_NL80211_BANDS];}; static struct test_wiphy wiphy; @@ -108,6 +109,13 @@ def function(source, name): static bool cfg80211_chandef_usable(void*w,const struct cfg80211_chan_def*d,int flags){return usable;} static bool mt7932_assoc_phy_valid(const void*b,const void*p,size_t n){return true;} static const struct element *cfg80211_find_ext_elem(int id,const void*p,size_t n){return NULL;} +/* The HT lookup feeds a debug log; it walks only the exported request IEs. */ +static unsigned ht_lookups;static const void *ht_found; +static const struct element *cfg80211_find_elem(int id,const void*p,size_t n){ + const u8*b=p;size_t o=0;assert(id==WLAN_EID_HT_CAPABILITY&&p&&n<=512);ht_lookups++;ht_found=NULL; + while(n-o>=2&&b[o+1]<=n-o-2){if(b[o]==id)return ht_found=b+o;o+=2+b[o+1];} + return NULL; +} static void netif_carrier_off(void*n){} static void netif_stop_queue(void*n){} static void schedule_work(void*w){scheduled++;} @@ -209,6 +217,12 @@ def function(source, name): reset(&m,SUCCESS);u8 rejected[52]={};put_unaligned_le16(1,rejected); receive(&m,0xee,0,rejected,sizeof(rejected),0x40); assert(m.connect_error==-ECONNREFUSED&&m.assoc_start.done&&m.assoc_done.done&&!m.peer_valid);cases++; + // The generated request IEs are exported once; the HT log reads the copy. + reset(&m,SUCCESS);u8 ies[32]={};put_unaligned_le32(28,ies);ies[4]=WLAN_EID_HT_CAPABILITY;ies[5]=26;for(unsigned i=0;i<26;i++)ies[6+i]=i+1;ht_lookups=0; + receive(&m,0xee,0,ies,sizeof(ies),0x5e); + assert(!m.connect_error&&m.assoc_request_seen&&m.assoc_request_ie_len==28&&!memcmp(m.assoc_request_ies,ies+4,28)&&ht_lookups==1&&ht_found==m.assoc_request_ies);cases++; + reset(&m,SUCCESS);ies[5]=25;ht_lookups=0;receive(&m,0xee,0,ies,sizeof(ies),0x5e); + assert(m.connect_error==-EPROTO&&!m.assoc_request_seen&&!ht_lookups&&m.assoc_start.done&&m.assoc_done.done);cases++; printf("PASS %u association event/wait/calibration ownership controls\n",cases); } ''' From f5d693d086629d4d6ec1cfd3504f42a6a1261d30 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 30 Oct 2026 17:49:27 +1100 Subject: [PATCH 35/40] tools/asahi: follow 6 GHz policy, USB runtime PM and RX wake in the Neo stability test Several driver changes left this harness behind. The MT7932 6 GHz station support (12010e055535) made channels band-qualified, added channels6, PSD and VLP handling to the regulatory notifier and a band argument to mt7932_policy_permits(), so the harness stopped compiling. The DWC3 glue gained a runtime PM reference (c8372a0ec637), a host-only role mark (a50abc4f7b41) and a set_role that can fail (3949e3640d94). DockChannel no longer makes the receive IRQ a wake IRQ and requests it with IRQF_NO_SUSPEND instead (f98122b072d6), so the wake case and its mutation tested behavior the driver no longer has. Give the fixture the nl80211 band, 6 GHz channels with frequency, PSD and VLP flags, a recording cfg80211 rule lookup and the missing helpers. The regulatory case now checks band-qualified permission where channel numbers overlap, that only 6 GHz channels consult the rule, that the VLP-client channel stays out of the domain and the others lose NO_IR, that the policy pass lifts NO_IR only under a rule without it, and that the disable bitmap spans every channel. The USB case checks that the runtime PM reference is dropped exactly once per stop and kept while the reset assert fails, that a refused PHY role stops the core before any host or gadget start, and that only a host-only host start marks the host role before the core comes up. The wake case now checks that the device is a wakeup source without a wake IRQ, and runs the RX IRQ request statement from dockchannel_init() to check its flags. The wake-irq mutation now restores the old wake IRQ, and new rx-suspend, usb-rpm, vlp-client and six-band mutations join it. Every mutation is refused if it no longer matches the driver. All seven mutations and both old-code controls are killed. Assisted-by: LLM Signed-off-by: Ryan Murray Co-Authored-By: Claude Opus 5.5 --- tools/asahi/neo-stability-tests.py | 167 ++++++++++++++++++++++------- 1 file changed, 127 insertions(+), 40 deletions(-) diff --git a/tools/asahi/neo-stability-tests.py b/tools/asahi/neo-stability-tests.py index f65cab986c626..ff02d627e5711 100644 --- a/tools/asahi/neo-stability-tests.py +++ b/tools/asahi/neo-stability-tests.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # SPDX-License-Identifier: GPL-2.0-only OR MIT -"""Host controls for retained domains, wake IRQs, USB roles and country policy.""" +"""Host controls for retained domains, wake IRQs, USB roles and band-qualified country policy.""" import argparse import hashlib import json @@ -34,7 +34,16 @@ def function(source, name): #include #include #define SHA256_DIGEST_SIZE 32 -typedef uint8_t u8; typedef uint16_t u16; typedef uint32_t u32; typedef int32_t s32; +typedef uint8_t u8; typedef uint16_t u16; typedef uint32_t u32; typedef uint64_t u64; typedef int8_t s8; typedef int32_t s32; +enum nl80211_band {NL80211_BAND_2GHZ,NL80211_BAND_5GHZ,NL80211_BAND_60GHZ,NL80211_BAND_6GHZ,NL80211_BAND_S1GHZ,NL80211_BAND_LC,NUM_NL80211_BANDS}; +#define min(a,b) ((a)<(b)?(a):(b)) +#define min_t(t,a,b) ((t)(a)<(t)(b)?(t)(a):(t)(b)) +#define ARRAY_SIZE(a) (sizeof(a)/sizeof((a)[0])) +#define BIT_ULL(n) (1ULL<<(n)) +#define GENMASK_ULL(h,l) (((~0ULL)>>(63-(h)))&(~0ULL<<(l))) +#define ERR_PTR(e) ((void*)(intptr_t)(e)) +#define IS_ERR(p) ((uintptr_t)(p)>=(uintptr_t)-4095) +#define MHZ_TO_KHZ(f) ((f)*1000) #define READ_ONCE(x) (x) #define WRITE_ONCE(x,v) ((x)=(v)) #define lockdep_assert_held(x) ((void)0) @@ -61,12 +70,24 @@ def function(source, name): static int devm_add_action_or_reset(struct device*d,void(*f)(void*),void*p){if(action_error){f(p);return action_error;}cleanup=f;cleanup_data=p;return 0;} static int devm_device_init_wakeup(struct device*d){wakeup_calls++;if(!wakeup_error)d->wakeup=true;return wakeup_error;} static int devm_pm_set_wake_irq(struct device*d,int i){irq_calls++;if(!irq_error)d->wake_irq=i;return irq_error;} +#define IRQF_NO_SUSPEND 0x4000 +#define IRQF_NO_AUTOEN 0x80000 +static unsigned long rx_flags;static int rx_irq_requested; +static int dockchannel_rx_irq(int i,void*p){return 0;} +static int dockchannel_rx_irq_thread(int i,void*p){return 0;} +static int devm_request_threaded_irq(struct device*d,int irq,int(*h)(int,void*),int(*t)(int,void*),unsigned long f,const char*n,void*p){ + assert(h==dockchannel_rx_irq&&t==dockchannel_rx_irq_thread&&!strcmp(n,"apple-dockchannel-rx"));rx_irq_requested=irq;rx_flags=f;return 0;} enum dwc3_apple_state {DWC3_APPLE_PROBE_PENDING,DWC3_APPLE_NO_CABLE,DWC3_APPLE_HOST,DWC3_APPLE_DEVICE,DWC3_APPLE_SUSPENDED}; -struct dwc3 {int num_usb3_ports,num_usb2_ports;int usb3_generic_phy[2],usb2_generic_phy[2],usb3_phy,usb2_phy;}; -struct dwc3_apple {struct device*dev;struct dwc3 dwc;int lock,reset;enum dwc3_apple_state state;bool fixed_hub,core_active,core_exit_pending,reset_pending;unsigned usb3_power_off_pending,usb2_power_off_pending;}; -static int core_error,host_error,gadget_error,reset_error; -static int dwc3_apple_core_start(struct dwc3_apple*a,enum dwc3_apple_state s){if(core_error)return core_error;a->core_active=true;a->reset_pending=true;return 0;} -static void dwc3_apple_set_role(struct dwc3_apple*a,enum dwc3_apple_state s){} +#define DWC3_GCTL_PRTCAP_HOST 1 +struct dwc3 {int num_usb3_ports,num_usb2_ports;int usb3_generic_phy[2],usb2_generic_phy[2],usb3_phy,usb2_phy;int current_dr_role;}; +struct dwc3_apple {struct device*dev;struct dwc3 dwc;int lock,reset;enum dwc3_apple_state state;bool fixed_hub,host_only,rpm_held,core_active,core_exit_pending,reset_pending;unsigned usb3_power_off_pending,usb2_power_off_pending;}; +static int core_error,host_error,gadget_error,reset_error,role_error,start_role; +static unsigned rpm_puts; +/* A started core holds the glue runtime PM reference until core_stop drops it. */ +static int dwc3_apple_core_start(struct dwc3_apple*a,enum dwc3_apple_state s){start_role=a->dwc.current_dr_role;if(core_error)return core_error;a->core_active=true;a->reset_pending=true;a->rpm_held=true;return 0;} +static int dwc3_apple_set_role(struct dwc3_apple*a,enum dwc3_apple_state s){assert(a->core_active);return role_error;} +static void pm_runtime_mark_last_busy(struct device*d){} +static void pm_runtime_put_autosuspend(struct device*d){rpm_puts++;} static int dwc3_host_init(struct dwc3*d){return host_error;} static int dwc3_gadget_init(struct dwc3*d){return gadget_error;} static void dwc3_host_exit(struct dwc3*d){role_exits++;} @@ -93,16 +114,24 @@ def function(source, name): #define WLAN_REASON_DEAUTH_LEAVING 3 #define __assign_bit(i,b,v) ((b)[(i)/64]=((b)[(i)/64]&~(1ULL<<((i)%64)))|((uint64_t)!!(v)<<((i)%64))) #define for_each_set_bit(i,b,n) for((i)=0;(i)<(n);(i)++) if((b)[(i)/64]&(1ULL<<((i)%64))) -#define bitmap_zero(b,n) memset((b),0,8) -struct ieee80211_channel {unsigned flags,hw_value;int max_power;}; +#define bitmap_zero(b,n) memset((b),0,((n)+63)/64*8) +#define IEEE80211_CHAN_NO_6GHZ_VLP_CLIENT 128 +#define NL80211_RRF_NO_IR (1<<7) +struct ieee80211_channel {unsigned flags,hw_value;int max_power;enum nl80211_band band;int center_freq;s8 psd;}; +static u64 get_unaligned_le64(const u8*p){return get_unaligned_le32(p)|(u64)get_unaligned_le32(p+4)<<32;} +static const void *memchr_inv(const void*p,int c,size_t n){const u8*b=p;for(size_t i=0;i=5955000&&khz<=7115000&&(khz/1000-5955)%20==0);return rule6_absent?ERR_PTR(-ERANGE):&rule6;} static unsigned rtnl_depth,wiphy_depth,schedules; static void rtnl_lock(void){assert(!rtnl_depth++);} static void rtnl_unlock(void){assert(rtnl_depth--==1);} static void wiphy_lock(struct wiphy*w){assert(rtnl_depth&& !wiphy_depth++);} static void wiphy_unlock(struct wiphy*w){assert(wiphy_depth--==1);} -struct mt7932 {struct ieee80211_channel channels[MT7932_CHANNELS_2G],channels5[MT7932_CHANNELS_5G];struct wiphy*wiphy;struct net_device*netdev;struct {struct device dev;}*pdev;struct mt7932_reg_snapshot reg_desired,reg_deferred;unsigned reg_generation,response_lock,startup_work;bool reg_pending,stopping,interface_registered,connecting,connected,disconnecting,reg_deferral,data_ready;int data_lock;int connect_error,assoc_start,assoc_done,discovery_done;uint64_t policy_disabled[1];}; +struct mt7932 {struct ieee80211_channel channels[MT7932_CHANNELS_2G],channels5[MT7932_CHANNELS_5G],channels6[MT7932_CHANNELS_6G];struct wiphy*wiphy;struct net_device*netdev;struct {struct device dev;}*pdev;struct mt7932_reg_snapshot reg_desired,reg_deferred;unsigned reg_generation,response_lock,startup_work;bool reg_pending,stopping,interface_registered,connecting,connected,disconnecting,reg_deferral,data_ready;int data_lock;int connect_error,assoc_start,assoc_done,discovery_done;uint64_t policy_disabled[(MT7932_CHANNELS+63)/64];}; #define spin_lock(l) ((void)(l)) #define spin_unlock(l) ((void)(l)) static void mt_data_kick_locked(struct mt7932*m){assert(0 && "unexpected TX boundary");} @@ -117,9 +146,11 @@ def function(source, name): int main(int argc,char**argv){ assert(argc==2);struct device d={};struct dockchannel dc={.dev=&d,.rx_irq=42}; if(!strcmp(argv[1],"wake")){ - assert(!dockchannel_init_wakeup(&dc)&&d.wakeup&&d.wake_irq==42); - wakeup_error=-ENOMEM;assert(dockchannel_init_wakeup(&dc)==-ENOMEM&&irq_calls==1); - wakeup_error=0;irq_error=-EINVAL;assert(dockchannel_init_wakeup(&dc)==-EINVAL); + /* The FIFO's device is a wakeup source; the receive IRQ is not a wake IRQ. */ + assert(!dockchannel_init_wakeup(&dc)&&d.wakeup&&!d.wake_irq&&!irq_calls&&wakeup_calls==1); + wakeup_error=-ENOMEM;d.wakeup=false;assert(dockchannel_init_wakeup(&dc)==-ENOMEM&&!d.wakeup&&!irq_calls); + /* It keeps receiving through system sleep, so the client filters wake data. */ + assert(!dockchannel_request_rx(&d,&dc)&&rx_irq_requested==42&&rx_flags==(IRQF_NO_AUTOEN|IRQF_NO_SUSPEND)); }else if(!strcmp(argv[1],"sep")){ domain_count=1;assert(!sep_pm_keep_domains(&d)&&!attach_calls); domain_count=2;attach_error=-EAGAIN;assert(sep_pm_keep_domains(&d)==-EAGAIN&&!pd0.syscore); @@ -127,39 +158,78 @@ def function(source, name): cleanup(cleanup_data);assert(!pd0.syscore&&!pd1.syscore); action_error=-ENOMEM;assert(sep_pm_keep_domains(&d)==-ENOMEM&&!pd0.syscore&&!pd1.syscore); }else if(!strcmp(argv[1],"usb")){ - for(unsigned fixed=0;fixed<2;fixed++)for(unsigned role=DWC3_APPLE_HOST;role<=DWC3_APPLE_DEVICE;role++){ - struct dwc3_apple a={.dev=&d,.fixed_hub=fixed,.state=DWC3_APPLE_PROBE_PENDING}; - assert(!dwc3_apple_init(&a,role)&&d.syscore&&a.state==role); - assert(!dwc3_apple_exit(&a)&&!d.syscore&&a.state==DWC3_APPLE_NO_CABLE); - unsigned n=core_exits;assert(!dwc3_apple_exit(&a)&&core_exits==n); - core_error=-EIO;assert(dwc3_apple_init(&a,role)==-EIO&&!d.syscore);core_error=0; - host_error=gadget_error=-ENOMEM;n=core_exits;assert(dwc3_apple_init(&a,role)==-ENOMEM&&!d.syscore&&core_exits==n+1);host_error=gadget_error=0; - assert(!dwc3_apple_init(&a,role)&&d.syscore);reset_error=-EIO;assert(dwc3_apple_exit(&a)==-EIO&&d.syscore&&a.reset_pending); - reset_error=0;n=core_exits;unsigned exits=role_exits; - assert(!dwc3_apple_exit(&a)&&!d.syscore&&!a.reset_pending); + for(unsigned fixed=0;fixed<2;fixed++)for(unsigned host_only=0;host_only<2;host_only++)for(unsigned role=DWC3_APPLE_HOST;role<=DWC3_APPLE_DEVICE;role++){ + struct dwc3_apple a={.dev=&d,.fixed_hub=fixed,.host_only=host_only,.state=DWC3_APPLE_PROBE_PENDING}; + unsigned puts=rpm_puts; + assert(!dwc3_apple_init(&a,role)&&d.syscore&&a.state==role&&a.rpm_held); + /* Only a host-only host start marks the role before the core comes up. */ + assert(start_role==(host_only&&role==DWC3_APPLE_HOST?DWC3_GCTL_PRTCAP_HOST:0)); + assert(!dwc3_apple_exit(&a)&&!d.syscore&&a.state==DWC3_APPLE_NO_CABLE&&!a.rpm_held&&rpm_puts==puts+1); + unsigned n=core_exits;assert(!dwc3_apple_exit(&a)&&core_exits==n&&rpm_puts==puts+1); + core_error=-EIO;assert(dwc3_apple_init(&a,role)==-EIO&&!d.syscore&&!a.rpm_held);core_error=0; + host_error=gadget_error=-ENOMEM;n=core_exits;assert(dwc3_apple_init(&a,role)==-ENOMEM&&!d.syscore&&core_exits==n+1&&!a.rpm_held&&rpm_puts==puts+2);host_error=gadget_error=0; + /* A refused PHY role stops the core again before any host or gadget start. */ + role_error=-EIO;n=core_exits;unsigned exits=role_exits; + assert(dwc3_apple_init(&a,role)==-EIO&&!d.syscore&&!a.core_active&&!a.rpm_held&&core_exits==n+1&&role_exits==exits&&a.state==DWC3_APPLE_NO_CABLE&&rpm_puts==puts+3);role_error=0; + assert(!dwc3_apple_init(&a,role)&&d.syscore);reset_error=-EIO;assert(dwc3_apple_exit(&a)==-EIO&&d.syscore&&a.reset_pending&&a.rpm_held&&rpm_puts==puts+3); + reset_error=0;n=core_exits;exits=role_exits; + assert(!dwc3_apple_exit(&a)&&!d.syscore&&!a.reset_pending&&!a.rpm_held&&rpm_puts==puts+4); assert(core_exits==n&&role_exits==exits); } }else if(!strcmp(argv[1],"regulatory")){ + enum {N25=MT7932_CHANNELS_2G+MT7932_CHANNELS_5G,SIX13=N25+3,VLP=N25+5}; struct mt7932 m={.reg_generation=7};struct wiphy w={.priv=&m};m.wiphy=&w; struct mt7932_policy p={};u8 tables[9][1020]={}; - for(unsigned i=0;i<9;i++){p.table[i+1]=tables[i];tables[i][4]=0;} - for(unsigned i=0;iband=ihw_value=imax_power=20; memset(tables[t]+44+j*122,0xc4,122);tables[t][44+j*122]=c->hw_value; if(i!=10)tables[t][45+j*122]=1; } - assert(!mt7932_policy_permits(&p,11)&&mt7932_policy_permits(&p,10)&&mt7932_policy_permits(&p,165)&&!mt7932_policy_permits(&p,14)); - struct regulatory_request request={.alpha2={'U','S'}};mt_regulatory_notify(&w,&request);m.reg_pending=false; - unsigned generation=m.reg_generation;mt_policy_disable(&m,&p,generation-1);assert(!m.channels[10].flags&&!m.policy_disabled[0]); - mt_policy_disable(&m,&p,generation);assert(m.channels[10].flags==1&&m.policy_disabled[0]==(1ULL<<10)); - struct mt7932_reg_snapshot reg=m.reg_desired;mt7932_policy_filter(®,&p);assert(reg.length==12+(MT7932_CHANNELS-1)*8&®.domain[8]==12&®.domain[9]==MT7932_CHANNELS_5G); - for(unsigned i=0;iband=NL80211_BAND_6GHZ;c->hw_value=mt7932_channel_6g(k);c->center_freq=5950+5*c->hw_value; + c->max_power=14;c->psd=-1;c->flags=IEEE80211_CHAN_NO_IR|IEEE80211_CHAN_PSD|(N25+k==VLP?IEEE80211_CHAN_NO_6GHZ_VLP_CLIENT:0); + } + assert(!mt7932_policy_permits(&p,NL80211_BAND_2GHZ,11)&&mt7932_policy_permits(&p,NL80211_BAND_2GHZ,10)&&mt7932_policy_permits(&p,NL80211_BAND_5GHZ,165)&&!mt7932_policy_permits(&p,NL80211_BAND_2GHZ,14)); + /* Channel numbers mean nothing without their band. */ + assert(mt7932_policy_permits(&p,NL80211_BAND_2GHZ,13)&&!mt7932_policy_permits(&p,NL80211_BAND_6GHZ,13)&&mt7932_policy_permits(&p,NL80211_BAND_6GHZ,1)); + assert(!mt7932_policy_permits(&p,NL80211_BAND_5GHZ,1)&&!mt7932_policy_permits(&p,NL80211_BAND_2GHZ,36)&&!mt7932_policy_permits(&p,NL80211_BAND_6GHZ,3)&&!mt7932_policy_permits(&p,NUM_NL80211_BANDS,1)); + struct regulatory_request request={.alpha2={'U','S'}};rule_lookups=0;mt_regulatory_notify(&w,&request);m.reg_pending=false; + assert(rule_lookups==MT7932_CHANNELS_6G); + /* The VLP-client channel stays out of the firmware domain; the others lose NO_IR there. */ + assert(m.reg_desired.length==12+(MT7932_CHANNELS-1)*8&&m.reg_desired.domain[8]==13&&m.reg_desired.domain[9]==MT7932_CHANNELS_5G&&m.reg_desired.domain[10]==MT7932_CHANNELS_6G-1); + for(unsigned i=0;ihw_value&&!f); + else{unsigned k=i-N25+(i>=VLP);assert(get_unaligned_le16(e)==mt7932_channel_6g(k)&&f==IEEE80211_CHAN_PSD);} + } + for(unsigned k=0;k=N25-1?c!=13:true);} + /* A rule that keeps NO_IR, or no rule at all, leaves the channel passive. */ + m.channels6[0].flags|=IEEE80211_CHAN_NO_IR;m.channels6[1].flags|=IEEE80211_CHAN_NO_IR; + rule6.flags=NL80211_RRF_NO_IR;mt_policy_disable(&m,&p,generation);assert(m.channels6[0].flags&IEEE80211_CHAN_NO_IR); + rule6.flags=0;rule6_absent=true;mt_policy_disable(&m,&p,generation);assert(m.channels6[1].flags&IEEE80211_CHAN_NO_IR); + rule6_absent=false;mt_policy_disable(&m,&p,generation);assert(!(m.channels6[0].flags&IEEE80211_CHAN_NO_IR)&&!(m.channels6[1].flags&IEEE80211_CHAN_NO_IR)); + m.channels[10].flags=0;m.channels6[3].flags=IEEE80211_CHAN_NO_IR|IEEE80211_CHAN_PSD;mt_regulatory_notify(&w,&request); + assert(m.reg_generation==generation&&m.channels[10].flags==1&&(m.channels6[3].flags&IEEE80211_CHAN_DISABLED)); + m.channels[10].flags=0;m.channels6[3].flags=IEEE80211_CHAN_NO_IR|IEEE80211_CHAN_PSD;request.alpha2[0]='C';request.alpha2[1]='A';mt_regulatory_notify(&w,&request); + assert(m.reg_generation==generation+1&&!m.policy_disabled[0]&&!m.policy_disabled[1]); m.reg_pending=false;m.connected=true;unsigned applied=m.reg_generation; put_unaligned_le32(IEEE80211_CHAN_NO_IR,m.reg_desired.domain+16); mt_regulatory_notify(&w,&request); @@ -178,7 +248,7 @@ def main(): parser.add_argument('--output', type=Path, required=True) parser.add_argument('--old-usb', action='store_true') parser.add_argument('--old-regulatory', action='store_true') - parser.add_argument('--mutation', choices=['sep-syscore', 'wake-irq', 'stale-policy']) + parser.add_argument('--mutation', choices=['sep-syscore', 'wake-irq', 'rx-suspend', 'stale-policy', 'usb-rpm', 'vlp-client', 'six-band']) args = parser.parse_args() args.output.mkdir(parents=True, exist_ok=True) files = { @@ -199,16 +269,33 @@ def main(): ROOT/'drivers/net/wireless/mediatek/mt7932/regulatory.h'] header = '\n'.join(re.sub(r'^#include .*\n', '', path.read_text(), flags=re.M) for path in headers) + # The receive IRQ request statement from dockchannel_init(), run against a recording stub. + dock = (ROOT/'drivers/soc/apple/dockchannel.c').read_text() + request = re.search(r'\tret = devm_request_threaded_irq\(dev, dockchannel->rx_irq,.*?\);\n', dock, re.S) + if not request: + raise ValueError('dockchannel RX IRQ request') + bodies['dockchannel_request_rx'] = ('static int dockchannel_request_rx(struct device *dev, struct dockchannel *dockchannel)\n' + '{\n\tint ret;\n\n' + request[0] + '\treturn ret;\n}\n') at = PREFIX.index('struct mt7932 {') code = PREFIX[:at] + header + PREFIX[at:] + '\n'.join(bodies.values()) + MAIN if not args.old_usb: code = code.replace(',DWC3_APPLE_SUSPENDED', '') - if args.mutation == 'sep-syscore': - code = code.replace('dev_pm_syscore_device(list->pd_devs[i], true);', 'dev_pm_syscore_device(list->pd_devs[i], false);') - if args.mutation == 'wake-irq': - code = code.replace('return devm_pm_set_wake_irq(dockchannel->dev, dockchannel->rx_irq);', 'return 0;') - if args.mutation == 'stale-policy': - code = code.replace('generation != READ_ONCE(m->reg_generation)', 'false') + mutations = { + 'sep-syscore': ('dev_pm_syscore_device(list->pd_devs[i], true);', 'dev_pm_syscore_device(list->pd_devs[i], false);'), + 'wake-irq': ('return devm_device_init_wakeup(dockchannel->dev);', + 'return devm_device_init_wakeup(dockchannel->dev) ?: devm_pm_set_wake_irq(dockchannel->dev, dockchannel->rx_irq);'), + 'rx-suspend': ('IRQF_NO_AUTOEN | IRQF_NO_SUSPEND', 'IRQF_NO_AUTOEN'), + 'stale-policy': ('generation != READ_ONCE(m->reg_generation)', 'false'), + 'usb-rpm': ('pm_runtime_put_autosuspend(appledwc->dev);', '(void)0;'), + 'vlp-client': ('if (flags & IEEE80211_CHAN_NO_6GHZ_VLP_CLIENT)', 'if (0)'), + 'six-band': ('(policy->six_channels & BIT_ULL((channel - 1) / 4))', 'true'), + } + if args.mutation: + old, new = mutations[args.mutation] + # A mutation that no longer matches the driver would test nothing. + if code.count(old) != 1: + parser.error(args.mutation + ' mutation no longer matches the driver source') + code = code.replace(old, new) source = args.output/'controls.c'; source.write_text(code) binary = args.output/'controls' subprocess.run(['cc', '-std=gnu11', '-g', '-Wall', '-Werror', '-Wno-unused-function', From 7ae165ae01385549d4cf57e6cbf2b232fe22f2f8 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 30 Oct 2026 18:00:01 +1100 Subject: [PATCH 36/40] selftests: apple-afkv2: follow compact EPIC and reply lengths in the HPD, sender and EDID tests Since the HPD reply check (e8f48a64e1b1), driver work reached this tree that the extracted units never followed, so three of them stopped compiling. The 14.7 external role closes its AV client on an HPD low (cadaaeb5e040). Compact EPIC on desktop 26.x endpoints (8e82151640fc, 99870822214e) gave the sender a compact path, the HPD request a status word reply and the legacy EDID reader a 2 KiB request. The sender gained a legacy reply-length interface on top of afk_send_command_common() (e6c10627c55e), and the legacy EDID reader now bounds the EDID by the reported reply length (48cbf951457b). HPD: count dcpavserv_v14_close() calls and check that only an HPD low closes the AV client, before the request goes out. On compact EPIC check that a zero status is accepted without an echo and that any nonzero status is refused. Sender: extract afk_send_command_common(), the reply-length wrapper and the compact status allocator. Legacy reply lengths go through the real completer, and the test checks the bounded copy with zero fill, the oversized refusal, the U32 overflow and NULL guards, and that AFK-v2 refuses the interface without sending. Compact requests check the status echo mapping and that no DMA or tag slot is used. EDID: model the reply-length call and the compact buffer size. The legacy reader keeps its checksum tolerance but now refuses a truncated reply, a wrong max_size echo, partial blocks and a short block count. Its compact request asks for 2 KiB and expects that size echoed. Add EOVERFLOW and EOPNOTSUPP to the host errno header. Assisted-by: LLM Signed-off-by: Ryan Murray Co-Authored-By: Claude Opus 5.5 --- .../drivers/gpu/apple-afkv2/extract-edid.py | 46 +++++-- .../drivers/gpu/apple-afkv2/extract-hpd.py | 79 ++++++++++-- .../drivers/gpu/apple-afkv2/extract-send.py | 119 ++++++++++++++---- .../apple-afkv2/host-include/linux/errno.h | 2 + 4 files changed, 202 insertions(+), 44 deletions(-) diff --git a/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-edid.py b/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-edid.py index 0d554d1c82b5d..bcced2f684cfa 100644 --- a/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-edid.py +++ b/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-edid.py @@ -29,12 +29,17 @@ def function(name): #define kzalloc(n,f) calloc(1,n) #define cpu_to_le64(x) (x) #define le64_to_cpu(x) (x) +#define READ_ONCE(x) (x) +#define KUNIT_STATIC_STUB_REDIRECT(...) do {} while (0) +#define SZ_2K 2048U +#define EDID_COMPACT_BUF_SIZE (EDID_LEADING_DATA_SIZE + SZ_2K) #define EDID_EXT_BLOCK_COUNT_OFFSET 0x7e #define ERR_PTR(x) ((void*)(intptr_t)(x)) #define IS_ERR(p) ((uintptr_t)(p)>(uintptr_t)-4096) static void cleanup_free(void *p) { free(*(void**)p); } #define __free(x) __attribute__((cleanup(cleanup_free))) -struct apple_epic_service { int unused; }; +struct ep { bool compact; }; +struct apple_epic_service { struct ep *ep; }; struct drm_edid { size_t size; }; struct dpavserv_copy_edid_cmd { u64 max_size;u8 pad1[24];u64 used_size;u8 pad2[8]; }; struct dpavserv_copy_edid_resp { u64 max_size;u8 pad1[24];u64 used_size;u8 pad2[8];u8 data[]; }; @@ -42,26 +47,36 @@ def function(name): static bool truncated; static const struct drm_edid *drm_edid_alloc(const void *p,size_t n) { struct drm_edid *e=malloc(sizeof(*e));(void)p;e->size=n;return e; } -static int afk_service_call(struct apple_epic_service *s,u16 group,u32 command, +/* The request asks for the buffer the transport can carry: 2 KiB on compact EPIC. */ +static size_t request_size(const struct apple_epic_service *s, bool inline_call) +{ return !inline_call && s->ep->compact ? EDID_COMPACT_BUF_SIZE : EDID_BUF_SIZE; } +static int edid_call(struct apple_epic_service *s,bool inline_call,u16 group,u32 command, const void *data,size_t data_len,size_t data_pad,void *output,size_t output_len,size_t output_pad) { const struct dpavserv_copy_edid_cmd *cmd=data; - (void)s;(void)data_pad;(void)output_pad; - assert(group==1 && command==7 && data_len==48 && cmd->max_size==EDID_BUF_SIZE); + size_t want=request_size(s,inline_call); + assert(group==1 && command==7 && data_len==48 && cmd->max_size==want && data_pad==want); + assert(output_len==48+want && output_pad==0); assert(output_len>=sizeof(fixture));memcpy(output,fixture,sizeof(fixture));return 0; } static int afk_service_call_len(struct apple_epic_service *s,u16 group,u32 command, const void *data,size_t data_len,size_t data_pad,void *output,size_t output_len, size_t output_pad,size_t *received) -{ int ret=afk_service_call(s,group,command,data,data_len,data_pad,output,output_len,output_pad); +{ int ret=edid_call(s,true,group,command,data,data_len,data_pad,output,output_len,output_pad); *received=sizeof(fixture)-(truncated?1:0);return ret; } +/* Legacy EPIC: the reply length the firmware reported. */ +static int afk_service_call_with_reply_len(struct apple_epic_service *s,u16 group,u32 command, + const void *data,size_t data_len,size_t data_pad,void *output,size_t output_len, + size_t output_pad,size_t *reply_len) +{ int ret=edid_call(s,false,group,command,data,data_len,data_pad,output,output_len,output_pad); + *reply_len=sizeof(fixture)-(truncated?1:0);return ret; } static void fix_checksum(u8 *p) { unsigned int i;u8 sum=0;p[127]=0;for(i=0;i<127;i++)sum+=p[i];p[127]=-sum; } ''' suffix = r''' int main(void) { - struct apple_epic_service service={0};const struct drm_edid *e; + struct ep ep={0};struct apple_epic_service service={.ep=&ep};const struct drm_edid *e; u8 *base=fixture+56,*extension=base+128; afkv2_put64(fixture,EDID_BUF_SIZE);afkv2_put64(fixture+32,8+256); memset(base+1,255,6);base[126]=1;fix_checksum(base); @@ -72,8 +87,23 @@ def function(name): e=dcpavserv_read_edid_inline(&service);assert(!IS_ERR(e) && e->size==256);free((void*)e); truncated=true; e=dcpavserv_read_edid_inline(&service);assert(IS_ERR(e)); - e=dcpavserv_read_edid_legacy(&service);assert(!IS_ERR(e));free((void*)e); - puts("PASS: actual legacy EDID checksum tolerance preserved; inline checksum/actual-length validation enforced"); + /* The legacy reader now bounds the EDID by the reported reply length too. */ + e=dcpavserv_read_edid_legacy(&service);assert(IS_ERR(e)); + truncated=false; + /* Its max_size echo must match the request, and the EDID must be whole blocks. */ + afkv2_put64(fixture,EDID_BUF_SIZE-1); + e=dcpavserv_read_edid_legacy(&service);assert(IS_ERR(e)); + afkv2_put64(fixture,EDID_BUF_SIZE);afkv2_put64(fixture+32,8+200); + e=dcpavserv_read_edid_legacy(&service);assert(IS_ERR(e)); + afkv2_put64(fixture+32,8+128); + e=dcpavserv_read_edid_legacy(&service);assert(IS_ERR(e)); /* base block announces one extension */ + afkv2_put64(fixture+32,8+256); + /* Compact EPIC asks for 2 KiB and expects that size echoed. */ + ep.compact=true; + e=dcpavserv_read_edid_legacy(&service);assert(IS_ERR(e)); + afkv2_put64(fixture,EDID_COMPACT_BUF_SIZE); + e=dcpavserv_read_edid_legacy(&service);assert(!IS_ERR(e) && e->size==256);free((void*)e); + puts("PASS: actual legacy EDID checksum tolerance preserved with reply-length, echo and block bounds (legacy and compact); inline checksum/actual-length validation enforced"); } ''' pathlib.Path(sys.argv[2]).write_text(prefix + function("dcpavserv_read_edid_legacy") + function("dcpavserv_read_edid_inline") + suffix) diff --git a/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-hpd.py b/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-hpd.py index 6957d92f390e2..b21cba6c7665c 100644 --- a/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-hpd.py +++ b/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-hpd.py @@ -1,6 +1,9 @@ #!/usr/bin/env python3 # SPDX-License-Identifier: GPL-2.0-only OR MIT -"""Exercise the production DPTX HPD request with mocked service-call replies.""" +"""Exercise the production DPTX HPD request with mocked service-call replies. + +Covers the legacy, AFK-v2 and compact EPIC replies and the 14.7 AV client close. +""" import pathlib import re import sys @@ -44,12 +47,19 @@ def function(source, name): #define EPIC_SUBTYPE_STD_SERVICE 0xc0 #define dev_warn(...) do {} while (0) #define dev_dbg(...) do {} while (0) +#define READ_ONCE(x) (x) +static u32 get_unaligned_le32(const void *p) +{ + const u8 *b = p; + + return b[0] | b[1] << 8 | b[2] << 16 | (u32)b[3] << 24; +} struct epic_service_call { u8 pad0[2]; u16 group; u32 command, data_len, magic; u8 pad1[48]; }; struct dcp { void *dev; }; struct ops { const char *name; }; -struct ep { struct dcp *dcp; int endpoint; bool inline_v2; }; +struct ep { struct dcp *dcp; int endpoint; bool inline_v2; bool compact; }; struct apple_epic_service { struct ep *ep; struct ops *ops; u32 channel; }; int afk_service_call_timeout_len(struct apple_epic_service *service, u16 group, u32 command, const void *data, size_t data_len, size_t data_pad, void *output, @@ -67,7 +77,16 @@ def function(source, name): */ static bool wire_v2; static size_t declared, arrived; -static u32 echo_flip; +static u32 echo_flip, compact_status; +/* An HPD low closes the 14.7 external AV client first; the stub counts it. */ +static struct dcp *expected_dcp; +static unsigned int v14_closes, closes_at_send; +/* Kept even if a mutant drops the call, so the count check is what fails. */ +__attribute__((unused)) static void dcpavserv_v14_close(struct dcp *dcp) +{ + assert(dcp == expected_dcp); + v14_closes++; +} static int afk_send_command_timeout_len(struct apple_epic_service *s, u8 type, const void *payload, size_t payload_len, void *output, size_t output_len, u32 *retcode, unsigned int timeout_ms, size_t *received) @@ -76,15 +95,21 @@ def function(source, name): u8 *body = (u8 *)output + sizeof(*call); u32 echo; - (void)s; (void)payload; (void)timeout_ms; + (void)payload; (void)timeout_ms; assert(type == EPIC_SUBTYPE_STD_SERVICE && payload_len == output_len); assert(output_len == sizeof(*call) + 32 && call->data_len == 32); assert(call->group == 8 && call->command == 8); + closes_at_send = v14_closes; if (wire_v2 && !received && arrived < output_len - sizeof(*call)) return -EMSGSIZE; memcpy(&echo, body + 16, sizeof(echo)); echo ^= echo_flip; memcpy(body + 16, &echo, sizeof(echo)); + /* Compact EPIC answers with an IOReturn status in the first word. */ + if (s->ep->compact) { + for (int i = 0; i < 4; i++) + body[i] = compact_status >> (8 * i); + } call->data_len = declared; *retcode = 0; if (received) @@ -94,12 +119,24 @@ def function(source, name): ''' suffix = r''' static int failures; +static struct ep *the_ep; static void expect(bool ok, const char *what, bool hpd, int ret) { - printf("%s %s: %s hpd=%d declared=%zu arrived=%zu ret=%d\n", ok ? "ok" : "FAIL", - wire_v2 ? "afk-v2" : "legacy", what, hpd, declared, arrived, ret); + printf("%s %s%s: %s hpd=%d declared=%zu arrived=%zu ret=%d\n", ok ? "ok" : "FAIL", + wire_v2 ? "afk-v2" : "legacy", the_ep->compact ? " compact" : "", what, hpd, + declared, arrived, ret); failures += !ok; } +/* Every request closes the AV client exactly when it lowers HPD, and before it is sent. */ +static int set_hpd(struct apple_epic_service *service, int hpd) +{ + unsigned int before = v14_closes; + int ret = dptxport_set_hpd_timeout(service, hpd, 1000); + + expect(v14_closes == before + !hpd && closes_at_send == before + !hpd, + "AV client closed before an HPD low only", hpd, ret); + return ret; +} int main(void) { static const size_t lengths[] = { 0, 4, 8, 12, 16, 20, 32, 36, 65535 }; @@ -108,6 +145,9 @@ def function(source, name): struct ep ep = { .dcp = &dcp }; struct apple_epic_service service = { .ep = &ep, .ops = &ops, .channel = 1 }; + the_ep = &ep; + expected_dcp = &dcp; + for (int v2 = 0; v2 < 2; v2++) { wire_v2 = v2; for (size_t i = 0; i < sizeof(lengths) / sizeof(lengths[0]); i++) { @@ -115,7 +155,7 @@ def function(source, name): int ret; declared = lengths[i]; arrived = 32; echo_flip = 0; - ret = dptxport_set_hpd_timeout(&service, hpd, 1000); + ret = set_hpd(&service, hpd); if (declared < sizeof(struct dcpdptx_hotplug_cmd)) expect(ret < 0, "short reply refused", hpd, ret); else if (declared > 32) @@ -128,19 +168,38 @@ def function(source, name): int ret; declared = 20; arrived = 32; echo_flip = 1; - ret = dptxport_set_hpd_timeout(&service, hpd, 1000); + ret = set_hpd(&service, hpd); expect(ret == -EINVAL, "wrong echo refused", hpd, ret); if (!wire_v2) continue; declared = 20; arrived = 16; echo_flip = 0; - ret = dptxport_set_hpd_timeout(&service, hpd, 1000); + ret = set_hpd(&service, hpd); expect(ret < 0, "reply cut short in transit refused", hpd, ret); } + /* Compact EPIC: the status word decides, there is no echo to compare. */ + ep.compact = true; + for (int hpd = 0; hpd < 2; hpd++) { + int ret; + + declared = 20; arrived = 32; echo_flip = 0; compact_status = 0; + ret = set_hpd(&service, hpd); + expect(ret == 0, "zero status accepted", hpd, ret); + echo_flip = 1; + ret = set_hpd(&service, hpd); + expect(ret == 0, "echo word ignored", hpd, ret); + echo_flip = 0; compact_status = 0xe00002c2; + ret = set_hpd(&service, hpd); + expect(ret == -EIO, "firmware error status refused", hpd, ret); + compact_status = 0x00000100; + ret = set_hpd(&service, hpd); + expect(ret == -EIO, "any nonzero status byte refused", hpd, ret); + } + ep.compact = false; compact_status = 0; } printf("%d HPD reply checks failed\n", failures); if (failures) return 1; - puts("PASS: extracted production DPTX HPD request: short, oversized, wrong-echo and complete replies, legacy and AFK-v2"); + puts("PASS: extracted production DPTX HPD request: short, oversized, wrong-echo and complete replies, legacy, AFK-v2 and compact status, AV client close on HPD low"); return 0; } ''' diff --git a/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-send.py b/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-send.py index 70dbd5d3a69f4..eb55dd281456f 100644 --- a/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-send.py +++ b/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-send.py @@ -1,25 +1,27 @@ #!/usr/bin/env python3 # SPDX-License-Identifier: GPL-2.0-only OR MIT -"""Compile the real command sender against synthetic ring and DMA operations.""" +"""Compile the real command sender against synthetic ring and DMA operations. + +Covers AFK-v2, legacy reply-length and compact EPIC requests. +""" import pathlib import re import sys source = pathlib.Path(sys.argv[1]).read_text() -match = re.search(r"static int afk_send_command_timeout_len\([^;]*?\)\n\{", source) -if not match: - raise RuntimeError("production command sender not found") -start, end, depth = match.start(), match.end(), 1 -while depth: - depth += (source[end] == "{") - (source[end] == "}") - end += 1 -reply_match = re.search(r"static void afk_recv_handle_reply\([^;]*?\)\n\{", source) -if not reply_match: - raise RuntimeError("production reply completer not found") -reply_end, reply_depth = reply_match.end(), 1 -while reply_depth: - reply_depth += (source[reply_end] == "{") - (source[reply_end] == "}") - reply_end += 1 + + +def function(pattern, what): + match = re.search(r"^" + pattern + r"\([^;]*?\)\n\{", source, re.M) + if not match: + raise RuntimeError("production " + what + " not found") + end, depth = match.end(), 1 + while depth: + depth += (source[end] == "{") - (source[end] == "}") + end += 1 + return source[match.start():end] + "\n" + + prefix = r''' #undef NDEBUG #include @@ -27,7 +29,10 @@ #include #include #include "afkv2-codec.h" +#include "afk_reply.h" #define SZ_1M 1048576U +#define U32_MAX 0xffffffffU +#define MSEC_PER_SEC 1000U #define GFP_KERNEL 0 #define MAX_PENDING_CMDS 16 #define AFKV2_INTERFACES (1U << 16) @@ -55,10 +60,13 @@ struct cmd_info { u16 tag; u8 type; size_t reply_length; void *rxbuf,*txbuf; dma_addr_t rxbuf_dma,txbuf_dma; size_t rxlen,txlen; u32 retcode; + size_t reply_len; bool reply_len_valid; bool done,free_on_ack; int *completion; }; -struct dcp { void *dev; bool quiescing; }; -struct apple_dcp_afkep { struct dcp *dcp; bool wire_v2,inline_v2; int endpoint,lock;u8 *command_tags; }; +struct dcp { void *dev; bool quiescing,external; void *rtk; }; +struct afk_ringbuffer { bool ready; }; +struct apple_dcp_afkep { struct dcp *dcp; bool wire_v2,inline_v2,compact; int endpoint,lock;u8 *command_tags; + struct afk_ringbuffer txbfr,rxbfr; }; struct ops { const char *name; }; struct apple_epic_service { struct apple_dcp_afkep *ep; struct ops *ops; unsigned int channel; unsigned long cmd_map[1]; @@ -68,6 +76,25 @@ static unsigned int frees, sends; static int wire_fail, timeout, reply_status; static size_t owned_reply_size=32888; +/* Legacy replies can go through the real completer, as from the ring. */ +static bool real_reply; +static u32 legacy_rxlen; +static void afk_recv_handle_reply(struct apple_dcp_afkep *ep,u32 channel,u16 tag,void *payload,size_t payload_size); +/* Compact EPIC goes inline through the ring; this mock stands for that transport. */ +static bool compact_echo; +static u32 compact_reply,compact_request; +static size_t compact_copied; +static unsigned int compact_sends; +static int afk_send_compact_command(struct apple_epic_service *s,u8 type,const void *payload,size_t payload_len, + void *output,size_t output_len,u32 request_status,u32 *retcode,size_t *copied,unsigned long timeout) +{ + (void)timeout; + assert(s==active && type==0xc0 && payload_len==112 && ((const u8 *)payload)[0]==0x5a && output_len==32888); + compact_sends++;compact_request=request_status; + *retcode=compact_echo?request_status:compact_reply; + memset(output,0x33,compact_copied);*copied=compact_copied; + return 0; +} static struct apple_epic_service *afk_service_get(struct apple_epic_service *s) { if(!s->enabled) return NULL; s->users++; return s; } static void afk_service_put(struct apple_epic_service *s) { assert(s->users); s->users--; } @@ -100,6 +127,14 @@ } if(wire_fail)return -ENOSPC; if(timeout)return 0; + if(real_reply) { + struct epic_cmd reply={.retcode=reply_status,.rxlen=legacy_rxlen}; + assert(!ep->wire_v2); + memset(c->rxbuf,0x77,legacy_rxlenrxlen?legacy_rxlen:c->rxlen); + afk_recv_handle_reply(ep,channel,tag,&reply,sizeof(reply)); + assert(c->done && c->reply_len_valid && c->reply_len==legacy_rxlen && *c->completion==1); + return 0; + } c->reply_length=ep->inline_v2?4:owned_reply_size;c->retcode=reply_status; memset(c->rxbuf,0x77,c->reply_length);c->done=true;*c->completion=1; return 0; @@ -139,6 +174,39 @@ ep.wire_v2=false;owned_reply_size=4; assert(!afk_send_command_timeout_len(&s,0xc0,request,112,output,32888,&status,1000,&received)); assert(received==32888 && s.users==1); + /* Legacy EPIC reply lengths: bounded by the buffer, rest of the output zeroed. */ + real_reply=true;legacy_rxlen=4;size_t rlen=99;memset(output,0x19,32888); + assert(!afk_send_command_with_reply_len(&s,0xc0,request,112,output,32888,&status,&rlen)); + assert(rlen==4 && status==0 && output[0]==0x77 && output[3]==0x77 && !output[4] && !output[32887]); + assert(!s.cmd_map[0] && s.users==1); + legacy_rxlen=32889;rlen=99; + assert(afk_send_command_with_reply_len(&s,0xc0,request,112,output,32888,&status,&rlen)==-EMSGSIZE); + assert(rlen==0 && !s.cmd_map[0] && s.users==1); + assert(afk_send_command_with_reply_len(&s,0xc0,request,112,output,32888,&status,NULL)==-EINVAL); + unsigned int before_sends=sends; + assert(afk_send_command_with_reply_len(&s,0xc0,request,(size_t)U32_MAX+1,output,32888,&status,&rlen)==-EOVERFLOW); + /* AFK-v2 reports reply lengths its own way and refuses this interface. */ + ep.wire_v2=true; + assert(afk_send_command_with_reply_len(&s,0xc0,request,112,output,32888,&status,&rlen)==-EOPNOTSUPP); + assert(sends==before_sends && s.users==1 && !s.cmd_map[0]); + real_reply=false; + /* Compact EPIC: success is the reply echoing the request's own status. */ + ep.compact=true;unsigned int before_frees=frees;status=99;received=99; + compact_echo=true;compact_copied=20; + assert(!afk_send_command_timeout_len(&s,0xc0,request,112,output,32888,&status,1000,&received)); + assert(status==0 && received==20 && output[0]==0x33 && compact_sends==1); + u32 first=compact_request; + compact_echo=false;compact_reply=0xe00002c2; + assert(!afk_send_command_timeout_len(&s,0xc0,request,112,output,32888,&status,1000,&received)); + assert(status==0xe00002c2 && compact_request==((((first>>8)+1)&0xff)<<8)); + compact_reply=0; + assert(!afk_send_command_timeout_len(&s,0xc0,request,112,output,32888,&status,1000,&received)); + assert(compact_request && status==~0U); + compact_echo=true;compact_copied=8;rlen=99; + assert(!afk_send_command_with_reply_len(&s,0xc0,request,112,output,32888,&status,&rlen) && rlen==8 && !status); + assert(compact_sends==4 && sends==before_sends && frees==before_frees && s.users==1 && !s.cmd_map[0]); + ep.compact=false; + dcp.external=true;ep.txbfr.ready=ep.rxbfr.ready=true; ep.wire_v2=true;owned_reply_size=32888;timeout=1; assert(afk_send_command_timeout_len(&s,0xc0,request,112,output,32888,&status,1000,&received)==-ETIMEDOUT); assert(s.cmd_map[0] && s.cmds[0].free_on_ack && !s.cmds[0].completion); @@ -155,14 +223,13 @@ s.enabled=true;s.torndown=false;timeout=0; assert(!afk_send_command_timeout_len(&s,0xc0,request,112,output,32888,&status,1000,&received)); assert(!s.cmd_map[0] && s.users==1);free(output); - puts("PASS: extracted production sender, 128-byte command envelope / 32888-byte reply, short OOB, strict/legacy callers and actual late-ACK ownership"); + puts("PASS: extracted production sender, 128-byte command envelope / 32888-byte reply, short OOB, strict/legacy callers, legacy reply lengths, compact status and actual late-ACK ownership"); } ''' -tag_match = re.search(r"static u16 afk_command_tag\([^;]*?\)\n\{", source) -if not tag_match: - raise RuntimeError("production interface tag allocator not found") -tag_end, tag_depth = tag_match.end(), 1 -while tag_depth: - tag_depth += (source[tag_end] == "{") - (source[tag_end] == "}") - tag_end += 1 -pathlib.Path(sys.argv[2]).write_text(prefix + source[tag_match.start():tag_end] + source[start:end] + source[reply_match.start():reply_end] + suffix) +bodies = [function(r"static u32 afk_compact_next_status", "compact status allocator"), + function(r"static u16 afk_command_tag", "interface tag allocator"), + function(r"static int afk_send_command_common", "command sender"), + function(r"static int afk_send_command_timeout_len", "length-aware sender"), + function(r"int afk_send_command_with_reply_len", "legacy reply-length sender"), + function(r"static void afk_recv_handle_reply", "reply completer")] +pathlib.Path(sys.argv[2]).write_text(prefix + "".join(bodies) + suffix) diff --git a/tools/testing/selftests/drivers/gpu/apple-afkv2/host-include/linux/errno.h b/tools/testing/selftests/drivers/gpu/apple-afkv2/host-include/linux/errno.h index 23e11801f1e76..e7a1050305b68 100644 --- a/tools/testing/selftests/drivers/gpu/apple-afkv2/host-include/linux/errno.h +++ b/tools/testing/selftests/drivers/gpu/apple-afkv2/host-include/linux/errno.h @@ -8,3 +8,5 @@ #define ENODEV 19 #define ETIMEDOUT 110 #define ENOMEM 12 +#define EOVERFLOW 75 +#define EOPNOTSUPP 95 From 4513ada9721b43ba6d0d0cd6807a9ddab736863a Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 30 Oct 2026 18:01:17 +1100 Subject: [PATCH 37/40] ci: run every host test on each push and pull request The host tests extract production code from this tree and build it on the host. Nothing ran them automatically, so a driver change could break one while the kernel build stayed green, and by 2026-10-30 seventeen of them had stopped passing. Add tools/asahi/run-host-tests.py. It runs every tools/asahi test under python3 -I in its own directory, together with its mutation and old-code controls, plus the Rust and shell tests there and the apple-dcp, apple- afkv2 (gcc and clang, with sanitizers), asahi-g15 and asahi-m4 selftests. A mutation only counts as killed when the built control reports the failure, so a mutant that just stops compiling fails the run. A tools/asahi test that is not registered in the runner also fails it, so new tests cannot be skipped by accident. Add a workflow that runs it on ubuntu-24.04 with pinned Python 3.14, rustc 1.96.0, clang 18 and gcc 13, for every push and pull request on integration/aurora and aurora-wip. The checkout keeps the commit history without trees so the old-code controls can read their revisions. Assisted-by: LLM Signed-off-by: Ryan Murray Co-Authored-By: Claude Opus 5.5 --- .github/workflows/host-tests.yml | 87 +++++++++++ tools/asahi/run-host-tests.py | 250 +++++++++++++++++++++++++++++++ 2 files changed, 337 insertions(+) create mode 100644 .github/workflows/host-tests.yml create mode 100755 tools/asahi/run-host-tests.py diff --git a/.github/workflows/host-tests.yml b/.github/workflows/host-tests.yml new file mode 100644 index 0000000000000..73a3c67c1842b --- /dev/null +++ b/.github/workflows/host-tests.yml @@ -0,0 +1,87 @@ +name: Host tests + +# Every host-side test of the Aurora drivers: tools/asahi (with their mutation +# and old-code controls) and the apple-dcp, apple-afkv2, asahi-g15 and asahi-m4 +# selftests. They extract production code from this tree and build it on the +# host, so a driver change can break them while the kernel build stays green. + +on: + push: + branches: [integration/aurora, aurora-wip] + pull_request: + branches: [integration/aurora, aurora-wip] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: host-tests-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +env: + RUST_VERSION: '1.96.0' + CLANG_VERSION: '18' + GCC_VERSION: '13' + +jobs: + host-tests: + runs-on: ubuntu-24.04 + timeout-minutes: 20 + steps: + # Negative controls read older revisions with `git show`, so keep the + # whole history. Trees and blobs of old commits load on demand. + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + filter: tree:0 + + - uses: actions/setup-python@v5 + with: + python-version: '3.14' + + - name: Cache the Rust toolchain + uses: actions/cache@v4 + with: + path: ~/.rustup/toolchains/${{ env.RUST_VERSION }}-x86_64-unknown-linux-gnu + key: rustup-${{ runner.os }}-${{ env.RUST_VERSION }} + + - name: Pin the host toolchains + run: | + rustup toolchain install "$RUST_VERSION" --profile minimal --no-self-update + echo "RUSTUP_TOOLCHAIN=$RUST_VERSION" >> "$GITHUB_ENV" + need= + for p in clang-$CLANG_VERSION gcc-$GCC_VERSION flex bison bc; do + command -v "$p" >/dev/null || need="$need $p" + done + dpkg -s "libclang-rt-$CLANG_VERSION-dev" >/dev/null 2>&1 || need="$need libclang-rt-$CLANG_VERSION-dev" + if [ -n "$need" ]; then + sudo apt-get update -q + sudo apt-get install -y -q $need + fi + # The tests call clang, cc, gcc and rustc by name. + bin="$RUNNER_TEMP/toolchain" + mkdir -p "$bin" + ln -sf "$(command -v clang-$CLANG_VERSION)" "$bin/clang" + ln -sf "$(command -v gcc-$GCC_VERSION)" "$bin/gcc" + ln -sf "$(command -v gcc-$GCC_VERSION)" "$bin/cc" + echo "$bin" >> "$GITHUB_PATH" + + - name: Show the toolchains + run: | + python3 --version + rustc --version + clang --version | head -1 + cc --version | head -1 + git --version + + - name: Run the host tests + run: python3 -I tools/asahi/run-host-tests.py --jobs "$(nproc)" --work "$RUNNER_TEMP/host-tests" + + - name: Keep the logs of a failed run + if: failure() + uses: actions/upload-artifact@v4 + with: + name: host-test-logs + path: ${{ runner.temp }}/host-tests/**/log + retention-days: 14 diff --git a/tools/asahi/run-host-tests.py b/tools/asahi/run-host-tests.py new file mode 100755 index 0000000000000..bcc42fe042c0f --- /dev/null +++ b/tools/asahi/run-host-tests.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +"""Run every Aurora host test: tools/asahi and the GPU/display selftests. + +Each tools/asahi test runs under `python3 -I` with its own output directory, +once as a control and once per mutation or old-code negative control listed +below. A mutation run is only accepted as a kill when the test reports one +(an assertion, an abort or a panic of the built control), so a mutant that +merely stops compiling is a failure, not a pass. + +Every tools/asahi/*-tests.{py,rs,sh} file must appear here. A new test that +is not registered fails the run, so it cannot be silently skipped. + +Negative controls read older revisions with `git show`, so the checkout needs +the commits (a full or treeless clone, not a depth-1 one). + +Usage: tools/asahi/run-host-tests.py [--jobs N] [--work DIR] [--only SUBSTR] +""" +import argparse +import concurrent.futures +import os +import re +import shlex +import subprocess +import sys +import tempfile +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +ASAHI = ROOT / 'tools/asahi' +GPU = ROOT / 'tools/testing/selftests/drivers/gpu' + +PASS = 'pass' +KILL = 'kill' + +# A killed mutant shows the failing control: a C assertion or abort (-6), or +# a Rust panic (exit 101). A Python traceback means the harness itself broke. +KILL_SIGN = re.compile(r'Assertion|": -6\b|"exit": 101\b|panicked at') +BROKEN_SIGN = re.compile(r'Traceback \(most recent call last\)') + + +def out(flag='--out'): + return [flag, '{out}'] + + +def mutations(flag, names, base, expect=KILL): + return [(base + [flag, n], expect) for n in names] + + +# test file -> [(arguments, expectation)]. '{out}' is a fresh directory. +ASAHI_TESTS = { + 'apple-dart-fw-tests.py': [(out(), PASS)], + 'apple-dart-range-tests.py': [([], PASS)], + 'atc-usb3-tests.py': [(out(), PASS), (out() + ['--mutations'], PASS)], + 'avd-av1-tail-tests.py': [(out(), PASS)] + + [(out() + ['--case', c], PASS) for c in ('copy', 'seed', 'aligned')] + + mutations('--mutation', ('round-up', 'cpu-unaligned', 'seg-cpu-unaligned', + 'seg-any-size', 'ref-index-upper', 'frame-width-unchecked'), out()), + 'avd-codec-tail-tests.py': [(out('--output'), PASS), (out('--output') + ['--mutations'], PASS)], + # The old revision is the one before the J493 hardware-table ID match. + 'bcm4377-id-tests.py': [(out() + ['--old-ref', '87ddae1397f6f70817cb57df76477a86bdf96424'], PASS)], + # These mutations are caught inside the test, which then exits 0. + 'bcm4377-resume-tests.py': [(out(), PASS)] + + mutations('--mutation', ('epoch', 'close', 'producer-fence', 'active-fence', 'board'), out(), PASS), + 'brcmf-assoc-tests.py': [(out('--output'), PASS)] + + mutations('--mutation', ('short-ie', 'header-length', 'skb-capacity', 'cmplt-header'), out('--output')), + 'brcmf-bss-tests.py': [(out(), PASS)], + 'brcmf-pcie-dma-tests.py': [(out('--output'), PASS)] + + mutations('--mutation', ('narrow-stream', 'ignore-aperture', 'revision-wide'), out('--output')), + 'brcmf-roam-tests.py': [(out(), PASS)], + 'brcmfmac-scan-timeout-tests.py': [(out('--output'), PASS)], + 'clpc-gpu-demand-tests.py': [([], PASS)], + # The old policy always disabled compute preemption. + 'compute-preempt-tests.py': [(out(), PASS), + (out() + ['--old-ref', '6dbfbcb21eb09d07ae59037a1ceca8cc3500841f'], PASS)], + 'dcp14-backlight-tests.py': [([], PASS)], + 'dcp14-panel-tests.py': [([], PASS)], + 'devcoredump-init-tests.py': [([], PASS)], + 'display26-tests.py': [([], PASS)], + # The old revision publishes the fd before copy_to_user() succeeds. + 'dma-heap-fd-tests.py': [(out() + ['--old-ref', 'cb732d027aa18e1fcf9d2797f47d20b179ebc59c'], PASS)], + 'g15-input-error-tests.py': [(out('--output'), PASS), (out('--output') + ['--mutations'], PASS), + (out('--output') + ['--ref', 'ede99b3c1990b4a4a8c045fdf627a2bd0f14446a'], PASS)], + 'g15-maintained-handoff-tests.py': [(out('--output'), PASS), (out('--output') + ['--mutations'], PASS)], + 'g15-pb-growth-tests.py': [(out('--output'), PASS), (out('--output') + ['--mutations'], PASS)], + 'g15-probe-init-tests.py': [(out('--output'), PASS), (out('--output') + ['--mutations'], PASS)], + 'g15-queue-credit-tests.py': [(out('--output'), PASS), (out('--output') + ['--mutations'], PASS)], + 'g15-retention-tests.py': [(out('--output'), PASS), (out('--output') + ['--mutations'], PASS)], + 'g15-stats-drain-tests.py': [(out('--output'), PASS), (out('--output') + ['--mutations'], PASS)], + 'g15-zero-words-tests.py': [([], PASS), (['--mutations'], PASS)], + 'g17-admission-deadline-tests.py': [([], PASS)], + 'g17-admission-order-tests.py': [([], PASS)], + 'g17-capacity-fault-tests.py': [([], PASS)], + 'g17-compute-constructor-tests.py': [([], PASS)], + 'g17-compute-deadline-tests.py': [([], PASS)], + 'g17-compute-growth-deadline-tests.py': [([], PASS)], + 'g17-firmware-selection-tests.py': [([], PASS)], + 'g17-growth-gate-tests.py': [([], PASS)], + 'g17-growth-tests.py': [([], PASS)], + 'g17-owner-release-order-tests.py': [([], PASS)], + 'g17-pool-release-tests.py': [([], PASS)], + 'g17-resource-tests.py': [([], PASS)], + 'g17-shared-pool-tests.py': [([], PASS)], + 'g17-uninstalled-cleanup-tests.py': [([], PASS)], + 'g17-unpublished-tests.py': [([], PASS)], + 'mt7932-association-events-tests.py': [(out(), PASS)], + 'mt7932-connect-result-tests.py': [(out('--output'), PASS)], + 'mt7932-owe-retry-tests.py': [(out(), PASS)], + 'mt7932-policy-6ghz-tests.py': [([], PASS)], + 'neo-auxiliary-tests.py': [(out('--output'), PASS), (out('--output') + ['--mutation'], KILL), + (out('--output') + ['--old'], KILL)], + 'neo-stability-tests.py': [(out('--output'), PASS)] + + mutations('--mutation', ('sep-syscore', 'wake-irq', 'rx-suspend', 'stale-policy', + 'usb-rpm', 'vlp-client', 'six-band'), out('--output')) + + [(out('--output') + ['--old-usb'], KILL), (out('--output') + ['--old-regulatory'], KILL)], + 'pmp-j613-hwmon-tests.py': [([], PASS)], + 'qspi-nvram-tests.py': [(out('--output'), PASS)], + 'rtkit-oslog-tests.py': [(out(), PASS)] + + mutations('--mutate', ('admission', 'channel', 'counter', 'reset', 'buffer'), out()), + 't6030-clock-tests.py': [(out(), PASS)], + 't6030-display-admission-tests.py': [(out('--output'), PASS), (out('--output') + ['--mutations'], PASS)], + 't6030-display-linkage-tests.py': [(out('--output') + ['--conf', '{conf}'], PASS)], + 't6030-pmp-graph-tests.py': [(out('--output'), PASS), (out('--output') + ['--mutations'], PASS)], + 't6030-pmp-supplier-tests.py': [(out('--output'), PASS), (out('--output') + ['--mutations'], PASS)], + 't6030-wiring-tests.py': [(out(), PASS), (out() + ['--old-classifier', 'tb'], PASS), + (out() + ['--old-classifier', 'dcp'], PASS)], + 't6040-calibration-tests.py': [([], PASS)], + 'tb-dp-repair-tests.py': [(out('--output'), PASS), (out('--output') + ['--mutations'], PASS)], + 'tbnet-refill-tests.py': [(out(), PASS)] + + mutations('--mutant', ('refill-count', 'no-retry', 'no-cancel', 'worker-allocation'), out()), + 'tiled-split-tests.py': [(out(), PASS), (out() + ['--mutations'], PASS)], + 'tunnel-route-tests.py': [(out(), PASS)], +} + +# Rust unit-test files built with `rustc --test`, and shell drivers. +ASAHI_RUST = ['m3-batching-tests.rs', 'm3-runtime-tests.rs', 'pmp-bootargs-tests.rs', 'pmp-power-tests.rs'] +ASAHI_SHELL = ['g17-ray-query-timeout-tests.sh'] + +# Python that runs on the host but imports its neighbours, so it keeps the +# script directory on sys.path (-E -s instead of -I). +SUITES = [ + ('apple-dcp', GPU / 'apple-dcp', ['python3', '-E', '-s', '-B', 'run.py', '--output', '{out}']), + ('asahi-g15', GPU / 'asahi-g15', ['python3', '-E', '-s', '-B', 'run.py', '--output', '{out}']), + ('asahi-m4', GPU / 'asahi-m4', ['python3', '-E', '-s', '-B', 'run-host-tests.py']), + ('apple-afkv2-gcc', GPU / 'apple-afkv2', ['sh', 'run-afkv2-tests.sh']), + ('apple-afkv2-clang', GPU / 'apple-afkv2', ['sh', 'run-afkv2-tests.sh']), + ('t8140-external-bw', GPU, ['python3', '-I', '-B', 'test_t8140_external_bw.py']), +] + + +def jobs(work, conf): + found = {p.name for p in ASAHI.iterdir() + if re.search(r'-tests\.(py|rs|sh)$', p.name) and p.name != Path(__file__).name} + known = set(ASAHI_TESTS) | set(ASAHI_RUST) | set(ASAHI_SHELL) + missing = sorted(found - known) + gone = sorted(known - found) + if missing or gone: + for name in missing: + print(f'not registered in {Path(__file__).name}: tools/asahi/{name}', file=sys.stderr) + for name in gone: + print(f'registered but missing: tools/asahi/{name}', file=sys.stderr) + sys.exit(2) + result = [] + # The suites take longest, so start them first. + for name, cwd, cmd in SUITES: + d = work / name + # Only ASan catches an unbounded legacy reply copy, so keep it on. + env = {'AFKV2_SANITIZERS': '1', 'HOSTCC': name.rsplit('-', 1)[1]} if name.startswith('apple-afkv2') else {} + result.append((name, [a.replace('{out}', str(d / 'out')) for a in cmd], cwd, PASS, d, env)) + for name, runs in ASAHI_TESTS.items(): + for index, (args, expect) in enumerate(runs): + label = name.removesuffix('.py') + ('' if not args or args == out() or args == out('--output') + else ' ' + ' '.join(a for a in args if a not in ('{out}', '--out', '--output', '{conf}', '--conf'))) + d = work / f'{name}.{index}' + cmd = ['python3', '-I', '-B', str(ASAHI / name)] + [a.replace('{out}', str(d / 'out')).replace('{conf}', str(conf)) for a in args] + result.append((label, cmd, ASAHI, expect, d, {})) + for name in ASAHI_RUST: + d = work / name + binary = d / 'tests' + cmd = ['sh', '-c', f'{os.environ.get("RUSTC", "rustc")} --edition=2021 --test {shlex.quote(str(ASAHI / name))} ' + f'-o {shlex.quote(str(binary))} && {shlex.quote(str(binary))}'] + result.append((name.removesuffix('.rs'), cmd, ASAHI, PASS, d, {})) + for name in ASAHI_SHELL: + d = work / name + result.append((name.removesuffix('.sh'), ['sh', str(ASAHI / name), str(d / 'out')], ROOT, PASS, d, {})) + return result + + +def run(job, timeout): + label, cmd, cwd, expect, d, extra = job + (d / 'tmp').mkdir(parents=True, exist_ok=True) + env = dict(os.environ, TMPDIR=str(d / 'tmp'), **extra) + start = time.monotonic() + try: + p = subprocess.run(cmd, cwd=cwd, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + text=True, errors='replace', timeout=timeout) + rc, text = p.returncode, p.stdout + except subprocess.TimeoutExpired as e: + rc, text = 'timeout', (e.stdout or b'').decode(errors='replace') if isinstance(e.stdout, bytes) else (e.stdout or '') + took = time.monotonic() - start + (d / 'log').write_text(f'$ {shlex.join(cmd)}\n{text}\nexit: {rc}\n') + if expect == PASS: + ok = rc == 0 + why = '' if ok else f'exit {rc}' + else: + ok = rc not in (0, 'timeout') and KILL_SIGN.search(text) and not BROKEN_SIGN.search(text) + why = '' if ok else (f'mutant survived (exit {rc})' if rc == 0 else f'exit {rc} without a control failure') + return label, bool(ok), why, took, d / 'log' + + +def main(): + ap = argparse.ArgumentParser(description=__doc__.split('\n', 1)[0]) + ap.add_argument('--jobs', type=int, default=os.cpu_count() or 2) + ap.add_argument('--work', type=Path, help='keep logs and build products here (default: a temporary directory)') + ap.add_argument('--conf', type=Path, help='a built scripts/kconfig/conf (default: build one)') + ap.add_argument('--only', help='run only jobs whose name contains this text') + ap.add_argument('--timeout', type=int, default=900) + args = ap.parse_args() + + work = args.work or Path(tempfile.mkdtemp(prefix='aurora-host-tests-')) + work = work.resolve() + work.mkdir(parents=True, exist_ok=True) + conf = args.conf + if conf is None: + build = work / 'kconfig' + subprocess.run(['make', '-s', '-C', str(ROOT), f'O={build}', 'ARCH=arm64', 'allnoconfig'], + check=True, stdout=subprocess.DEVNULL) + conf = build / 'scripts/kconfig/conf' + todo = jobs(work, conf.resolve()) + if args.only: + todo = [j for j in todo if args.only in j[0]] + print(f'{len(todo)} runs, {args.jobs} at a time, logs in {work}', flush=True) + failed = [] + start = time.monotonic() + with concurrent.futures.ThreadPoolExecutor(args.jobs) as pool: + for label, ok, why, took, log in pool.map(lambda j: run(j, args.timeout), todo): + print(f'{"ok" if ok else "FAIL"} {label} ({took:.1f}s){"" if ok else ": " + why}', flush=True) + if not ok: + failed.append((label, log)) + print(f'{len(todo) - len(failed)}/{len(todo)} passed in {time.monotonic() - start:.0f}s') + for label, log in failed: + text = log.read_text(errors='replace').splitlines() + print(f'\n=== {label}: last lines of {log}') + print('\n'.join(text[-40:])) + return 1 if failed else 0 + + +if __name__ == '__main__': + sys.exit(main()) From cd893cd22b0418b7f9ece702ecf68148776a4e04 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 30 Oct 2026 18:08:58 +1100 Subject: [PATCH 38/40] selftests: asahi-m4: find newer UAPI headers in the tree for the layout probe The C layout probe includes the tree's asahi_drm.h, which pulls in drm_mode.h and so . That header only exists in recent kernel UAPI exports. On a host with older ones, such as Ubuntu 24.04's 6.8 linux-libc-dev, the probe stopped compiling and test-uapi failed. Search the tree's include/uapi after the system headers. Headers the host already has still come from the host, and only the missing ones come from the tree. The measured layout is unchanged. Assisted-by: LLM Signed-off-by: Ryan Murray Co-Authored-By: Claude Opus 5.5 --- tools/testing/selftests/drivers/gpu/asahi-m4/abi.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tools/testing/selftests/drivers/gpu/asahi-m4/abi.py b/tools/testing/selftests/drivers/gpu/asahi-m4/abi.py index 38c6d7806eabd..7b9bbce635e6e 100644 --- a/tools/testing/selftests/drivers/gpu/asahi-m4/abi.py +++ b/tools/testing/selftests/drivers/gpu/asahi-m4/abi.py @@ -104,7 +104,9 @@ def c_layout(header, bindings, output, tag): 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')) + - ['-std=gnu11', '-Wall', '-Werror', '-D__user='], + ['-std=gnu11', '-Wall', '-Werror', '-D__user=', + # Fall back to the tree for UAPI headers the host lacks. + '-idirafter', str(header.parents[1])], source, output / tag) From 24f1fbfd8b3ffc03ee7e7b47f3a1955ce18af4d8 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 30 Oct 2026 18:08:58 +1100 Subject: [PATCH 39/40] ci: host tests: provide ld.lld and use the pinned rustc everywhere The first run on ubuntu-24.04 found two gaps in the toolchain setup. The apple-dcp usb-kconfig case configures the tree with LLVM=1, which needs ld.lld. The g15-pb-growth and g15-stats-drain tests ask rustup for their own toolchain unless HOSTRUSTC names a compiler. Install lld 18 next to clang 18, and point HOSTRUSTC at the pinned rustc. Assisted-by: LLM Signed-off-by: Ryan Murray Co-Authored-By: Claude Opus 5.5 --- .github/workflows/host-tests.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/host-tests.yml b/.github/workflows/host-tests.yml index 73a3c67c1842b..f16fb33953f7c 100644 --- a/.github/workflows/host-tests.yml +++ b/.github/workflows/host-tests.yml @@ -50,19 +50,23 @@ jobs: run: | rustup toolchain install "$RUST_VERSION" --profile minimal --no-self-update echo "RUSTUP_TOOLCHAIN=$RUST_VERSION" >> "$GITHUB_ENV" + # Tests that would pick their own rustup toolchain use this one. + echo "HOSTRUSTC=rustc" >> "$GITHUB_ENV" need= for p in clang-$CLANG_VERSION gcc-$GCC_VERSION flex bison bc; do command -v "$p" >/dev/null || need="$need $p" done + command -v "ld.lld-$CLANG_VERSION" >/dev/null || need="$need lld-$CLANG_VERSION" dpkg -s "libclang-rt-$CLANG_VERSION-dev" >/dev/null 2>&1 || need="$need libclang-rt-$CLANG_VERSION-dev" if [ -n "$need" ]; then sudo apt-get update -q sudo apt-get install -y -q $need fi - # The tests call clang, cc, gcc and rustc by name. + # The tests call clang, ld.lld, cc and gcc by name. bin="$RUNNER_TEMP/toolchain" mkdir -p "$bin" ln -sf "$(command -v clang-$CLANG_VERSION)" "$bin/clang" + ln -sf "$(command -v ld.lld-$CLANG_VERSION)" "$bin/ld.lld" ln -sf "$(command -v gcc-$GCC_VERSION)" "$bin/gcc" ln -sf "$(command -v gcc-$GCC_VERSION)" "$bin/cc" echo "$bin" >> "$GITHUB_PATH" @@ -72,6 +76,7 @@ jobs: python3 --version rustc --version clang --version | head -1 + ld.lld --version cc --version | head -1 git --version From 23827fe87f4ffbabf5b7907a5907f9316286ce55 Mon Sep 17 00:00:00 2001 From: Ryan <81541646+RyanTheTide@users.noreply.github.com> Date: Sat, 31 Oct 2026 00:09:47 +1100 Subject: [PATCH 40/40] soc: apple: mailbox: Do not sleep past free FIFO space A sleeping sender that finds the send FIFO full waits on tx_empty for the send-empty interrupt, and gives up after 500 ms. Two things make it miss free space and time out with room in the FIFO: - The interrupt reports an empty FIFO. A firmware that takes some of the queued messages but not all of them frees space without raising it. - Two senders can wait at once, for example a driver starting its endpoints while the RTKit work item acknowledges a syslog message. The handler masks the interrupt and calls complete(), which wakes only one of them. The other sleeps on with the interrupt masked. A sender that timed out also masked it, stranding any sender still waiting. On the J616s (M4 Pro) this delayed the AOP start on some warm boots. One endpoint start failed 504 ms after it was issued, while the AOP firmware was logging over syslog: 2.891389 aop: ep 0x28 init sent 3.395337 aop: Failed to send management message type 0x05: -110 3.395341 aop: Retrying unsent endpoint start 0x29 3.395354 aop: ep 0x2b init sent 3.402107 aop: syslog message: ... AOP IS READY The retry and the next three requests went out within 20 us, so the FIFO had room when the first send gave up, and the firmware reported ready 7 ms later. In an earlier boot the same stall made an AFK init time out after the firmware had reported ready, and the probe failed. Wake every waiter with complete_all(), and check the FIFO again every 2 ms while waiting, until the same 500 ms deadline. A woken sender that still finds the FIFO full rearms the interrupt before it waits, as before. A sender that times out now leaves the interrupt unmasked for the others. tx_irq_unmasked still records the state, so the enable stays balanced: the handler masks it when it fires, and provider removal masks it otherwise. Atomic senders, which poll, are unchanged. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/soc/apple/mailbox.c | 58 ++++++++++++++++++------------- include/linux/soc/apple/mailbox.h | 4 +-- 2 files changed, 35 insertions(+), 27 deletions(-) diff --git a/drivers/soc/apple/mailbox.c b/drivers/soc/apple/mailbox.c index 902f872ff994e..2591560a9b716 100644 --- a/drivers/soc/apple/mailbox.c +++ b/drivers/soc/apple/mailbox.c @@ -119,6 +119,11 @@ static_assert(APPLE_ASCWRAP_V6_MBOX_OFFSET + APPLE_ASC_MBOX_I2A_RECV0 == #define APPLE_MBOX_MSG1_MSG GENMASK(31, 0) #define APPLE_MBOX_TX_TIMEOUT 500 +/* + * The send-empty interrupt reports an empty FIFO, not free space. A sender + * that waits for it also checks the FIFO this often. + */ +#define APPLE_MBOX_TX_RECHECK_MS 2 #define APPLE_G17_MBOX_RX_BUDGET 64 #define APPLE_G17_MBOX_ATOMIC_TX_US 1000 @@ -330,7 +335,16 @@ int apple_mbox_send(struct apple_mbox *mbox, const struct apple_mbox_msg msg, while (mbox_ctrl & mbox->hw->control_full) { now = jiffies; if (time_after_eq(now, deadline)) { + u32 i2a = readl_relaxed(mbox->regs + mbox->hw->i2a_control); + spin_unlock_irqrestore(&mbox->tx_lock, flags); + if (mbox->irq_send_empty >= 0) + dev_err(mbox->dev, + "TX wait failed a2i=%#x i2a=%#x full=%#x send_irq=%d recv_irq=%d msg=%#llx/%#x\n", + mbox_ctrl, i2a, mbox->hw->control_full, + mbox->irq_send_empty, + mbox->irq_recv_not_empty, + msg.msg0, msg.msg1); return -ETIMEDOUT; } remaining = deadline - now; @@ -383,31 +397,19 @@ int apple_mbox_send(struct apple_mbox *mbox, const struct apple_mbox_msg msg, } spin_unlock_irqrestore(&mbox->tx_lock, flags); + /* + * The firmware can take messages without emptying the FIFO, + * which raises no interrupt, so check it again after a short + * wait; the deadline above ends the loop. Other senders may be + * waiting on the same interrupt: a sender that gives up leaves + * it unmasked for them. The handler masks it when it fires, + * and removal masks it otherwise. + */ t = wait_for_completion_interruptible_timeout( - &mbox->tx_empty, remaining); - if (t <= 0) { - u32 a2i = 0, i2a = 0; - bool removing; - - /* A late IRQ may already have masked this under tx_lock. */ - spin_lock_irqsave(&mbox->tx_lock, flags); - removing = READ_ONCE(mbox->removing); - if (!removing) { - a2i = readl_relaxed(mbox->regs + mbox->hw->a2i_control); - i2a = readl_relaxed(mbox->regs + mbox->hw->i2a_control); - } - if (mbox->tx_irq_unmasked) { - disable_irq_nosync(mbox->irq_send_empty); - mbox->tx_irq_unmasked = false; - } - spin_unlock_irqrestore(&mbox->tx_lock, flags); - dev_err(mbox->dev, - "TX wait failed ret=%ld a2i=%#x i2a=%#x full=%#x send_irq=%d recv_irq=%d removing=%d msg=%#llx/%#x\n", - t, a2i, i2a, mbox->hw->control_full, - mbox->irq_send_empty, mbox->irq_recv_not_empty, - removing, msg.msg0, msg.msg1); - return t < 0 ? t : -ETIMEDOUT; - } + &mbox->tx_empty, + min(remaining, msecs_to_jiffies(APPLE_MBOX_TX_RECHECK_MS))); + if (t < 0) + return t; spin_lock_irqsave(&mbox->tx_lock, flags); if (unlikely(READ_ONCE(mbox->removing))) { @@ -443,7 +445,13 @@ static irqreturn_t apple_mbox_send_empty_irq(int irq, void *data) disable_irq_nosync(mbox->irq_send_empty); mbox->tx_irq_unmasked = false; } - complete(&mbox->tx_empty); + /* + * Wake every waiting sender, not just one. The interrupt is now + * masked, so a sender left asleep would only wake at its timeout, + * with room in the FIFO. A woken sender that still finds the FIFO + * full rearms the interrupt before it waits again. + */ + complete_all(&mbox->tx_empty); spin_unlock(&mbox->tx_lock); return IRQ_HANDLED; diff --git a/include/linux/soc/apple/mailbox.h b/include/linux/soc/apple/mailbox.h index 694d538d8c70b..4c27621f22e0a 100644 --- a/include/linux/soc/apple/mailbox.h +++ b/include/linux/soc/apple/mailbox.h @@ -87,8 +87,8 @@ struct apple_mbox { /* * Protected by tx_lock: set while irq_send_empty is unmasked, so that - * the IRQ handler, a failed send and provider removal mask it exactly - * once between them. + * the IRQ handler and provider removal mask it exactly once between + * them. */ bool tx_irq_unmasked; };