From 8d88d95e8a1a626d983acacc11e5bf200e407d02 Mon Sep 17 00:00:00 2001 From: AxxzyWasTaken Date: Fri, 30 Oct 2026 19:04:18 +0000 Subject: [PATCH] drm/asahi: g17: walk only uncovered ranges when validating a submission The per-queue Proof skips the page-table walk only when it covers every range a submission names. Otherwise the whole request set is walked again, and every GPUVM map step moves the mapping epoch, which discards all cached proofs. Mesa records each frame's commands into command-buffer chunks at fresh addresses, so nearly every submission names at least one range the proof has not seen. Each of those re-walks every range in the submission, including the render targets: thousands of pages per full-resolution attachment, under the GPUVM exec lock. Under an unchanged epoch the cached proofs still hold, so walk only the ranges they do not cover and record the whole request set as before. If the epoch moved between the cache check and the walk, walk everything, as before. A GPUVM map step only maps addresses nothing maps yet: GPUVM runs the unmap and remap steps for any overlap first, and those remain full mutations. Every range a proof covers therefore stays mapped across a map step, so it no longer advances the epoch. Cached validation still pauses while the step's leaf writes are in flight. Signed-off-by: AxxzyWasTaken --- drivers/gpu/drm/asahi/g17/validation.rs | 61 +++++++++++++++++++++++-- drivers/gpu/drm/asahi/mmu.rs | 15 +++++- 2 files changed, 72 insertions(+), 4 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/validation.rs b/drivers/gpu/drm/asahi/g17/validation.rs index b0f2861382ad7..9ee4b6b515d19 100644 --- a/drivers/gpu/drm/asahi/g17/validation.rs +++ b/drivers/gpu/drm/asahi/g17/validation.rs @@ -120,6 +120,17 @@ impl Proof { .all(|&request| self.requests.covers(request)) } + /// The requested ranges this proof does not already cover. + fn uncovered(&self, requests: &Requests) -> Requests { + let mut missing = Requests::new(); + for &request in requests.as_slice() { + if !self.requests.covers(request) { + missing.insert(request); + } + } + missing + } + fn record(&mut self, epoch: Option, requests: Requests) { // Alternating in-flight buffers must not evict each other's proofs. if epoch.is_some() && self.epoch == epoch && !requests.overflow { @@ -153,12 +164,22 @@ impl Proof { let optimistic = payload.validate(window, variant, &Space::Record(&range, &requests)); let requests = requests.into_inner(); if optimistic.is_ok() && !requests.overflow { - if self.covers(vm.mapping_validation_epoch(), &requests) { + let epoch = vm.mapping_validation_epoch(); + if self.covers(epoch, &requests) { return optimistic; } - let (covered, epoch) = vm.covers_ranges_batch(requests.as_slice()); + // Proofs from an unchanged epoch still hold: walk only the ranges they do not + // cover. A command buffer at a fresh address then no longer re-walks every + // render target it names. + let partial = self.epoch.is_some() && self.epoch == epoch; + let walk = if partial { self.uncovered(&requests) } else { requests }; + let (mut covered, mut walked) = vm.covers_ranges_batch(walk.as_slice()); + if covered && partial && walked != self.epoch { + // A mapping changed since the cached proofs were taken: walk everything. + (covered, walked) = vm.covers_ranges_batch(requests.as_slice()); + } if covered { - self.record(epoch, requests); + self.record(walked, requests); return optimistic; } self.epoch = None; @@ -280,6 +301,40 @@ mod tests { &requests((MAX_CACHED_RANGES as u64 * 2 * PAGE, 8, 1)))); } + #[test] + fn only_uncovered_ranges_are_walked_again() { + let mut proof = Proof::new(); + let mut proven = requests((PAGE, 4 * PAGE, 1)); + proven.insert((8 * PAGE, PAGE, 3)); + proof.record(Some(5), proven); + + let mut next = requests((PAGE + 16, 8, 1)); + next.insert((8 * PAGE, 8, 2)); + next.insert((20 * PAGE, PAGE, 1)); + next.insert((4 * PAGE, 2 * PAGE, 1)); + let missing = proof.uncovered(&next); + assert_eq!(missing.as_slice(), &[(20 * PAGE, PAGE, 1), (4 * PAGE, 2 * PAGE, 1)]); + + // Stronger access than proven is walked again. + assert_eq!(proof.uncovered(&requests((PAGE, 8, 2))).count, 1); + assert_eq!(proof.uncovered(&requests((8 * PAGE, 8, 3))).count, 0); + } + + #[test] + fn a_partial_walk_extends_the_proof() { + let mut proof = Proof::new(); + proof.record(Some(5), requests((PAGE, PAGE, 1))); + let mut next = requests((PAGE, 8, 1)); + next.insert((10 * PAGE, 8, 1)); + assert!(!proof.covers(Some(5), &next)); + // validate() walks proof.uncovered(&next), then records the whole request set. + assert_eq!(proof.uncovered(&next).as_slice(), &[(10 * PAGE, PAGE, 1)]); + proof.record(Some(5), next); + assert!(proof.covers(Some(5), &next)); + assert!(proof.covers(Some(5), &requests((PAGE + 8, 8, 1)))); + assert!(!proof.covers(Some(6), &next)); + } + #[test] fn invalid_rounding_and_full_cache_disable_reuse() { for range in [(0, 0, 1), (u64::MAX - 1, 1, 1), (0, 1, 0), (0, 1, 4)] { diff --git a/drivers/gpu/drm/asahi/mmu.rs b/drivers/gpu/drm/asahi/mmu.rs index f146b2762d812..122b42ccd1b6d 100644 --- a/drivers/gpu/drm/asahi/mmu.rs +++ b/drivers/gpu/drm/asahi/mmu.rs @@ -570,7 +570,7 @@ impl gpuvm::DriverGpuVm for VmInner { Ordering::Acquire, |value| value.checked_add(1), ).map_err(|_| EOVERFLOW)? + 1; - let _mutation = self.mapping_mutation(); + let _mutation = self.mapping_addition(); let (map_addr, map_length) = (op.addr(), op.length() as usize); let mut iova = op.addr(); let mut left = op.length() as usize; @@ -827,6 +827,19 @@ impl VmInner { self.tlbi_contexts_naming_root(addr, size); } + /// A mutation that only maps addresses nothing maps yet: GPUVM runs the unmap and remap + /// steps for any overlap first, and those are full mutations. Every range a cached proof + /// covers stays mapped, so the epoch keeps its value and the proofs stay valid; cached + /// validation still pauses until the leaf writes finish. + fn mapping_addition(&mut self) -> MappingMutation { + let state = self.mapping_epoch.clone(); + if let Some(state) = state.as_ref() { + state.writers.fetch_add(1, Ordering::Relaxed); + state.visible.swap(u64::MAX, Ordering::AcqRel); + } + MappingMutation(state) + } + fn mapping_mutation(&mut self) -> MappingMutation { let state = self.mapping_epoch.clone(); if let Some(state) = state.as_ref() {