From 6f7faeecea0eccc8ee317ba163867df339d54fa5 Mon Sep 17 00:00:00 2001 From: Jonathan Goyvaerts Date: Mon, 19 Oct 2026 19:00:24 -0300 Subject: [PATCH 1/5] drm/asahi: g17: do not advertise soft faults DRM_ASAHI_FEATURE_SOFT_FAULTS was set whenever the fault_control module parameter kept its default, but the G17 runtime never programs fault_control into the firmware, so unmapped GPU reads are not guaranteed to return zero. Mesa trusts the bit: it points out-of-bounds vec3/vec4 vertex fetches at address 0 and skips the bounds check on bounded constant loads. On the M4 Pro those reads returned non-zero data and every robustness2 rgba32 vertex_attribute_fetch case failed; they pass with soft faults disabled in Mesa. Ask the GPU backend instead; G17 reports none. Signed-off-by: Jonathan Goyvaerts Assisted-by: LLM --- drivers/gpu/drm/asahi/file.rs | 3 +-- drivers/gpu/drm/asahi/g17/mod.rs | 9 +++++++++ drivers/gpu/drm/asahi/gpu.rs | 4 ++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/drivers/gpu/drm/asahi/file.rs b/drivers/gpu/drm/asahi/file.rs index 7f3043a991388..d0c6975819a54 100644 --- a/drivers/gpu/drm/asahi/file.rs +++ b/drivers/gpu/drm/asahi/file.rs @@ -18,7 +18,6 @@ use crate::{ gem, memacct, mmu, - module_parameters, queue, util::{ align, @@ -408,7 +407,7 @@ impl File { u64::from(uapi::drm_asahi_feature_DRM_ASAHI_FEATURE_INDEPENDENT_QUEUES); } - if module_parameters::fault_control.value() == 0xb { + if gpu.supports_soft_faults() { params.features |= uapi::drm_asahi_feature_DRM_ASAHI_FEATURE_SOFT_FAULTS as u64; } diff --git a/drivers/gpu/drm/asahi/g17/mod.rs b/drivers/gpu/drm/asahi/g17/mod.rs index 9129418f4a16f..c1f03f4acbc08 100644 --- a/drivers/gpu/drm/asahi/g17/mod.rs +++ b/drivers/gpu/drm/asahi/g17/mod.rs @@ -1130,6 +1130,15 @@ impl gpu::Gpu for Gpu { true } + /// The G17 runtime never programs fault_control into the firmware, so unmapped reads are not + /// guaranteed to return zero. Advertising soft faults made Mesa point out-of-bounds vec3/vec4 + /// vertex fetches at address 0 and skip bounds checks on bounded loads; on T6040 those reads + /// returned non-zero data (2026-10-19: every robustness2 rgba32 vertex_attribute_fetch case + /// failed; all pass with soft faults off). + fn supports_soft_faults(&self) -> bool { + false + } + fn queue_limits(&self) -> Option { Some(uapi::drm_asahi_queue_limits { max_queues: mmu::MAX_EXECUTION_CONTEXTS, diff --git a/drivers/gpu/drm/asahi/gpu.rs b/drivers/gpu/drm/asahi/gpu.rs index ed54c350d7dbc..1c5435ba93e7a 100644 --- a/drivers/gpu/drm/asahi/gpu.rs +++ b/drivers/gpu/drm/asahi/gpu.rs @@ -284,6 +284,10 @@ pub(crate) trait Gpu: Send + Sync { fn is_crashed(&self) -> bool; /// Fill in the hardware description reported to userspace. fn get_params(&self, params: &mut uapi::drm_asahi_params_global) -> Result; + /// Whether unmapped GPU reads return zero (fault_control programmed into the firmware). + fn supports_soft_faults(&self) -> bool { + crate::module_parameters::fault_control.value() == 0xb + } /// Whether user VMs retain a sticky error and support a userspace mirror. fn supports_vm_status(&self) -> bool { false From b0912f542fd0655f10631977a8c63abc51a85ada Mon Sep 17 00:00:00 2001 From: Jonathan Goyvaerts Date: Mon, 19 Oct 2026 12:03:41 -0300 Subject: [PATCH 2/5] drm/asahi: g17: reread a torn borrowed render snapshot poll() knows that individually sampled cursor words can straddle a firmware update: an implausible snapshot gets one immediate reread, and a second implausible one is only a retry. poll_observed(), which finishes a snapshot taken while the pair was borrowed, returned the plausibility check's EIO directly, so one torn sample quarantined the pair and the client saw a lost device. Under load (a Vulkan conformance run beside the desktop) this failed innocent clients, the compositor's full-screen composite among them, with no GPU fault and no recovery. Fall back to poll() when the borrowed snapshot does not pass. Signed-off-by: Jonathan Goyvaerts Assisted-by: LLM --- drivers/gpu/drm/asahi/g17/queue/render/retirement.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/queue/render/retirement.rs b/drivers/gpu/drm/asahi/g17/queue/render/retirement.rs index d0a2d566bdc8d..24aeed6bcd0ec 100644 --- a/drivers/gpu/drm/asahi/g17/queue/render/retirement.rs +++ b/drivers/gpu/drm/asahi/g17/queue/render/retirement.rs @@ -203,8 +203,12 @@ pub(crate) fn poll_observed( memory: &mut impl Memory, snapshot: Snapshot, ) -> Result { - let observation = observe(ticket, snapshot)?; - complete_poll(ticket, memory, snapshot, observation) + match observe(ticket, snapshot) { + Ok(observation) => complete_poll(ticket, memory, snapshot, observation), + // The borrowed snapshot can straddle a firmware cursor update like any other sample: + // take the ordinary poll's fresh snapshot and reread instead of failing the client. + Err(_) => poll(ticket, memory), + } } fn complete_poll( From 30ea4b13047377972cb75b78a4cbe5885cf46d1d Mon Sep 17 00:00:00 2001 From: Jonathan Goyvaerts Date: Mon, 19 Oct 2026 12:19:01 -0300 Subject: [PATCH 3/5] drm/asahi: g17: do not fail a blamed render that attribution cleared GPU recovery classifies guilt from the firmware's recovery table and its blamed QID, and attribution can move that blame: a blamed render that had not started while other renders had (or with no culprit at all) is innocent. service() still failed the blamed render's oldest packet with EIO. On the M4 Pro the firmware blamed the compositor's not-yet-started full-screen render while a Vulkan test faulted in another process: the compositor saw a lost device and aborted, taking the session down. classify() now also returns whether the blamed QID is still guilty after attribution; only then is its render failed. Otherwise its work is spared like every other innocent VM's. Log each recovery's reason, blamed queue and decision. Signed-off-by: Jonathan Goyvaerts Assisted-by: LLM --- drivers/gpu/drm/asahi/g17/recovery.rs | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/recovery.rs b/drivers/gpu/drm/asahi/g17/recovery.rs index d5495b8c24f74..c23a5f0c13d07 100644 --- a/drivers/gpu/drm/asahi/g17/recovery.rs +++ b/drivers/gpu/drm/asahi/g17/recovery.rs @@ -227,9 +227,17 @@ pub(super) fn service( } host.recovery_mut().pending = None; - let cancelled = classify(host, entries, request.blamed, &mut sources); + let (cancelled, blame) = classify(host, entries, request.blamed, &mut sources); if let Some(qid) = request.blamed { - host.settle_blamed_render(qid, EIO); + pr_warn!( + "G17: GPU recovery (reason {:?}): firmware blamed queue {}, {}\n", + sources.reason, + qid, + if blame { "failing its oldest render" } else { "attribution cleared it; spared" } + ); + if blame { + host.settle_blamed_render(qid, EIO); + } } if cancelled != 0 { host.settle_cancelled_renders(cancelled); @@ -242,7 +250,10 @@ pub(super) fn service( /// Resolves guilty QIDs before changing any completion's classification. Unknown ownership or /// too many distinct guilty VMs leaves all prior classifications intact. The oldest spared work /// of a QID makes later blame collateral to its previous recovery, rather than new VM guilt. -fn classify(queues: &mut Q, entries: Entries, blamed: Option, sources: &mut Sources) -> u128 { +/// Also returns whether the firmware's blamed QID is still guilty after attribution: a blamed +/// render that had not started while others had (or with no culprit at all) is innocent, and its +/// work is spared like that of every other innocent VM instead of failing with EIO. +fn classify(queues: &mut Q, entries: Entries, blamed: Option, sources: &mut Sources) -> (u128, bool) { let mut guilty_qids = entries.guilty | blamed.map_or(0, |qid| 1u128 << qid); let cancelled = entries.cancelled; let attribution = sources.attribute(blamed, |qid, stamp| queues.render_pass_started(qid, stamp)); @@ -252,8 +263,9 @@ fn classify(queues: &mut Q, entries: Entries, blamed: Option, sou Attribution::NoCulprit => guilty_qids = 0, } let no_culprit = attribution == Attribution::NoCulprit; + let blame = blamed.is_some_and(|qid| guilty_qids & (1u128 << qid) != 0); if guilty_qids == 0 && !no_culprit { - return 0; + return (0, blame); } let mut guilty: [Option>; MAX_GUILTY_VMS] = Default::default(); let mut count = 0; @@ -289,7 +301,7 @@ fn classify(queues: &mut Q, entries: Entries, blamed: Option, sou } } if unresolved || (count == 0 && collateral == 0 && !no_culprit) { - return 0; + return (0, blame); } let spared = queues.classify_work(&|status| { guilty[..count] @@ -302,5 +314,5 @@ fn classify(queues: &mut Q, entries: Entries, blamed: Option, sou }) .unwrap_or(Class::Spared) }); - cancelled & !(guilty_qids & !collateral) & spared + (cancelled & !(guilty_qids & !collateral) & spared, blame) } From e3d8d1b2fc1feada69ec028203f727f642a150ac Mon Sep 17 00:00:00 2001 From: Jonathan Goyvaerts Date: Mon, 19 Oct 2026 14:10:51 -0300 Subject: [PATCH 4/5] drm/asahi: g17: skip already-signalled successful input fences Almost every desktop render carries input fences, nearly all of them buffer fences that signalled long before the submit. Each one still became a scheduler dependency, so the render went through the drm_sched worker instead of inline publication: 0.25-0.5 ms before the GPU started, against about 0.07 ms for the submit ioctl itself. A fence that signalled without error orders nothing and has no error left to consume, exactly like the completed frontier producers the loop already skips. Skip it; a pending or failed input is kept and checked as before. Signed-off-by: Jonathan Goyvaerts Assisted-by: LLM --- drivers/gpu/drm/asahi/g17/queue/mod.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/drivers/gpu/drm/asahi/g17/queue/mod.rs b/drivers/gpu/drm/asahi/g17/queue/mod.rs index fb010ff459df1..84d1bd5a2fd71 100644 --- a/drivers/gpu/drm/asahi/g17/queue/mod.rs +++ b/drivers/gpu/drm/asahi/g17/queue/mod.rs @@ -277,6 +277,13 @@ impl Queue { // either order and must each consume an input's error status before running. for sync in inputs { if let Some(fence) = &sync.fence { + // An input that already signalled without error orders nothing and has no status + // left to consume (like a completed frontier producer above). Keeping it would + // send the render through the scheduler thread instead of inline publication: + // most compositor and client renders carry such already-signalled buffer fences. + if job::fence_status(fence) > 0 { + continue; + } dependencies.push(fence.clone(), GFP_KERNEL)?; checked_inputs.push(fence.clone(), GFP_KERNEL)?; } From 2321a1bc6ea6c8a62a275685c0542a293c9b5438 Mon Sep 17 00:00:00 2001 From: Jonathan Goyvaerts Date: Tue, 20 Oct 2026 17:24:15 -0300 Subject: [PATCH 5/5] drm/asahi: allow HIGH queue priority with CAP_SYS_NICE or DRM master queue_create refused DRM_ASAHI_PRIORITY_HIGH with EINVAL (a TODO to allow it with CAP_SYS_NICE), and the G17 scheduling table had no HIGH profile. Mesa works around it: the GL driver advertises HIGH context priority but creates a MEDIUM queue, and the Vulkan driver refuses HIGH global priority until the kernel accepts it. Allow HIGH for callers with CAP_SYS_NICE or the DRM master, as amdgpu and panthor do for raised queue priority, and refuse it with EACCES otherwise so userspace can fall back. REALTIME stays reserved. Add drm::File::is_current_master() for the check. On G17 HIGH maps to firmware priority class 1, the class the G13 tables use for HIGH, with the policy word of the user classes; its work rings are already set up at boot. The check is in the common queue_create, so the G13 and G14 SoCs accept HIGH too and submit it on their existing priority-1 pipes. That is untested there. Signed-off-by: Jonathan Goyvaerts Assisted-by: LLM --- drivers/gpu/drm/asahi/file.rs | 13 +++++++++++-- drivers/gpu/drm/asahi/g17/mod.rs | 5 +++-- drivers/gpu/drm/asahi/hw/t8140.rs | 3 +++ include/uapi/drm/asahi_drm.h | 3 ++- rust/bindings/bindings_helper.h | 1 + rust/kernel/drm/file.rs | 6 ++++++ 6 files changed, 26 insertions(+), 5 deletions(-) diff --git a/drivers/gpu/drm/asahi/file.rs b/drivers/gpu/drm/asahi/file.rs index d0c6975819a54..6219564d7734e 100644 --- a/drivers/gpu/drm/asahi/file.rs +++ b/drivers/gpu/drm/asahi/file.rs @@ -1213,11 +1213,20 @@ impl File { return Err(EINVAL); } - // TODO: Allow with CAP_SYS_NICE - if data.priority >= uapi::drm_asahi_priority_DRM_ASAHI_PRIORITY_HIGH { + // REALTIME stays reserved. HIGH needs CAP_SYS_NICE or DRM master, as amdgpu and + // panthor require for raised queue priority; EACCES lets userspace fall back to MEDIUM. + if data.priority == uapi::drm_asahi_priority_DRM_ASAHI_PRIORITY_REALTIME { cls_pr_debug!(Errors, "queue_create: Invalid priority\n"); return Err(EINVAL); } + // SAFETY: capable() only reads the calling task's credentials (ioctl process context). + if data.priority == uapi::drm_asahi_priority_DRM_ASAHI_PRIORITY_HIGH + && !unsafe { bindings::capable(bindings::CAP_SYS_NICE as _) } + && !file.is_current_master() + { + cls_pr_debug!(Errors, "queue_create: HIGH priority needs CAP_SYS_NICE or DRM master\n"); + return Err(EACCES); + } let queues_xa = file.inner().queues(); let resv = queues_xa.lock().reserve_limit(1..=u32::MAX, GFP_KERNEL)?; diff --git a/drivers/gpu/drm/asahi/g17/mod.rs b/drivers/gpu/drm/asahi/g17/mod.rs index c1f03f4acbc08..fcfd58d5d10d1 100644 --- a/drivers/gpu/drm/asahi/g17/mod.rs +++ b/drivers/gpu/drm/asahi/g17/mod.rs @@ -1236,10 +1236,11 @@ impl gpu::Gpu for Gpu { } /// Scheduling profile of a new logical queue. `file.rs` passes -/// `DRM_ASAHI_PRIORITY_REALTIME - priority` and refuses HIGH and REALTIME, so -/// MEDIUM arrives as 2 and LOW as 3. +/// `DRM_ASAHI_PRIORITY_REALTIME - priority` and refuses REALTIME, so HIGH +/// arrives as 1, MEDIUM as 2 and LOW as 3. fn scheduling_policy(priority: u32) -> Result { match priority { + 1 => Ok(hw::t8140::scheduling::HIGH), 2 => Ok(hw::t8140::scheduling::MEDIUM), 3 => Ok(hw::t8140::scheduling::LOW), _ => Err(EINVAL), diff --git a/drivers/gpu/drm/asahi/hw/t8140.rs b/drivers/gpu/drm/asahi/hw/t8140.rs index 41f04be56e28d..d04b00bbc8b78 100644 --- a/drivers/gpu/drm/asahi/hw/t8140.rs +++ b/drivers/gpu/drm/asahi/hw/t8140.rs @@ -384,6 +384,9 @@ pub(crate) mod qos { pub(crate) mod scheduling { use crate::g17::fw::queue::Policy; + /// `DRM_ASAHI_PRIORITY_HIGH`: class 1, as the G13 tables map HIGH; the + /// policy word of the user classes is kept (no macOS trace of class 1 yet). + pub(crate) const HIGH: Policy = Policy::new(1, 2, super::qos::CLASS, super::qos::SHARE); /// `DRM_ASAHI_PRIORITY_MEDIUM`. pub(crate) const MEDIUM: Policy = Policy::new(2, 2, super::qos::CLASS, super::qos::SHARE); /// `DRM_ASAHI_PRIORITY_LOW`. diff --git a/include/uapi/drm/asahi_drm.h b/include/uapi/drm/asahi_drm.h index a482ab38b5952..798079f86984d 100644 --- a/include/uapi/drm/asahi_drm.h +++ b/include/uapi/drm/asahi_drm.h @@ -812,7 +812,8 @@ enum drm_asahi_priority { /** * @DRM_ASAHI_PRIORITY_HIGH: High priority queue. * - * Reserved for future extension. + * Requires CAP_SYS_NICE or DRM master; otherwise queue creation fails + * with -EACCES. */ DRM_ASAHI_PRIORITY_HIGH = 2, diff --git a/rust/bindings/bindings_helper.h b/rust/bindings/bindings_helper.h index 752eb6e68a727..6224a16eff580 100644 --- a/rust/bindings/bindings_helper.h +++ b/rust/bindings/bindings_helper.h @@ -30,6 +30,7 @@ #include #include +#include #include #include #include diff --git a/rust/kernel/drm/file.rs b/rust/kernel/drm/file.rs index 8317ed97566e1..88709a7f9b36d 100644 --- a/rust/kernel/drm/file.rs +++ b/rust/kernel/drm/file.rs @@ -70,6 +70,12 @@ impl File { self.0.get() } + /// Whether this file holds the device's DRM master (the compositor or display server). + pub fn is_current_master(&self) -> bool { + // SAFETY: By the type invariants of `Self`, `self.as_raw()` is a valid open `drm_file`. + unsafe { bindings::drm_is_current_master(self.as_raw()) } + } + /// Tag recent feeder work for temporary capacity hints during syncobj waits. /// User scheduling attributes and CPU affinity remain unchanged. pub fn set_syncobj_wait_hint(&self, util_min: u32, duration_ms: u32) {