From mboxrd@z Thu Jan 1 00:00:00 1970 Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.remote-ip=100.103.45.18 ARC-Seal: i=1; d=subspace.kernel.org; s=arc20261015; a=rsa-sha256; cv=none; t=1793974205; b=4T44Xkul57ei6iL/z1ZHapKSAY7dQp4AEyH8ABeyOXk9yhS9vT9B7HojNoe1A8LMhqRl 2clVw04XOXk//OBdZGqhiq7VAvqqqzAsFnzpq2jRB3dRsA91dljS5nX2c3fbBUo4Hct8l qJi1ViSGuzDHCAxvJVPZ4yIyxFwfzwfKuUbP2ReONJ5k4HPMifInHu9MtBAcpWwUY2D+V 9lT2IaU5y5MX+5YYUOgT85+gMU9nP/B4nRSc3pC3d0g0rDExxSrX9p038997nGwOGI+oR teYQA/+FPaaX3O5CTq3p7jEFVEPFl+eOkbGDXxAMdIMrSiIZ5w1v0mFcCP9cF0MWwJg== ARC-Message-Signature: i=1; d=subspace.kernel.org; s=arc20261015; a=rsa-sha256; c=relaxed/relaxed; t=1793974205; h=Received:DKIM-Signature:From:To:Cc:Subject:Date:Message-ID:X-Mailer: In-Reply-To:References:MIME-Version:Content-Transfer-Encoding; bh=D93dXWNgvSaU+ooS0LjSFPCo+YFs3NfgktR+fkKyhuY=; b=AbkPorKfT5H4LeKuwZlZNASBsvWe2L+QG/kAWcYUc2OoDg3NqRR9NPCr/SVeeHZgOUdz QtPt/ocCQi7SfRNfzn7bA1s/P/i/BchjMHAvL4ZJrx9CeFnDt6XYKfmfTl8F5ivLDF67j 2TqBU9IsBq4DR2vTai7jaVAKueMEO3DxQjg0Ow5wJt1yDn5Ri3RNZqwOWMeanT//h6QTP GW3mMQrb/RRLaI2NVfbtkYPtq8txjjuee/Q/1SDg9ZhOv2jAAJXOGpLeqtuTmzvyN8QlI 9D0jj87eFmgn+5u9/kN1ouF4Q4k6knXdbZVLUypzFYoP1gPPtO3SJvIteAu0zKGRTBQ== ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass header.d=kernel.org header.i=@kernel.org header.a=rsa-sha256 header.s=k20260515 header.b="KY4Rrv/e"; arc=none smtp.remote-ip=100.103.45.18 Received: from smtp.kernel.org (quasi.space.kernel.org [100.103.45.18]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature ECDSA (prime256v1)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 73591606A329; Fri, 6 Nov 2026 14:10:04 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key, unprotected) header.d=kernel.org header.i=@kernel.org header.a=rsa-sha256 header.s=k20260515 header.b=KY4Rrv/e Received: by smtp.kernel.org (Postfix) with ESMTPSA id 68C491F008A3; Fri, 6 Nov 2026 14:09:58 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=kernel.org; s=k20260515; t=1793974204; bh=D93dXWNgvSaU+ooS0LjSFPCo+YFs3NfgktR+fkKyhuY=; h=From:To:Cc:Subject:Date:In-Reply-To:References; b=KY4Rrv/e0IKfj1dbdLEtA1ZYNcJ1ycE4uIOTGUZ+ZCObxILzu2CPzLNJbv1m6f2Yj a8jM0WEJM3bi+6N/vLI4I/GYCwp4e7gqAr/rKD3mUQPeBwo+TR3ao3QeJKKpztgvi5 NK7dIY0fgKvwRiSF03jEI3WzCxhWTlqwF+HjViY2k03E4fk6BlNYE2pi089CzgKL9S z2Pi2DB44b6jsY6vRrxucxtE/hWvVCxyl4oTqqkaaqtzl7pmFjIvxwrqb/cExKrp6y 8nk/xth3VRsJ6r7NTiD3fJQM7aGhc98ZsdBWMQl5LsYw8AfkpbzLUQTYFclgB+Q0cx STOCV2WyP9vWA== From: Philipp Stanner To: Sumit Semwal , =?UTF-8?q?Christian=20K=C3=B6nig?= , Philipp Stanner , Danilo Krummrich , Alice Ryhl , Miguel Ojeda , Boqun Feng , Gary Guo , =?UTF-8?q?Bj=C3=B6rn=20Roy=20Baron?= , Benno Lossin , Andreas Hindborg , Trevor Gross , Daniel Almeida , Tamir Duberstein , Alexandre Courbot , =?UTF-8?q?Onur=20=C3=96zkan?= , Matthew Brost , David Airlie , Simona Vetter , Tvrtko Ursulin Cc: linux-kernel@vger.kernel.org, linux-media@vger.kernel.org, dri-devel@lists.freedesktop.org, rust-for-linux@vger.kernel.org Subject: [PATCH 7/8] rust: drm: Add JobQueue Date: Fri, 6 Nov 2026 15:07:52 +0100 Message-ID: <20261106140752.520386-9-phasta@kernel.org> X-Mailer: git-send-email 2.56.0 In-Reply-To: <20261106140752.520386-2-phasta@kernel.org> References: <20261106140752.520386-2-phasta@kernel.org> X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Post: List-Help: List-Subscribe: List-Unsubscribe: List-Owner: Precedence: list MIME-Version: 1.0 Content-Transfer-Encoding: 8bit JobQueue is a pure Rust component that handles job submissions for GPUs with firmware scheduling, i.e., GPUs that support one exclusive ring per execution context. JobQueue's primary task is dependency handling, where the user can set dependencies, each in the form of a `dma_buf::Fence`, on a job and then submit it to the queue. Jobs are being executed in order, following the FIFO principle. Only jobs whose dependencies are all fulfilled get executed, otherwise the JobQueue pauses. A core design principle, just like with DmaFence, is to avoid all allocations in job submission paths, since this runs danger of deadlocking. Another design principle, new when compared to the older drafts, is the ownership relationship with fences: 1. The JobQueue owns the FenceContext exclusively. 2. Jobs are created on the JobQueue, ensuring correct fence sequence number ordering. 3. Jobs own the respective DriverFence. 4. Jobs are owned by the JobQueue. Consequently, a driver can only interact with fences and FenceContext indirectly through the JobQueue, and can also only complete jobs (and, hence, signal fences) through the JobQueue. Unfortunately, since jobs need a reference to the JobQueue for their fence dependency objects (DependencyWaker), this code can currently not be used in this form because some work on self-referential PinInit is required. See the respective comments in the Example. When a job shall be run, both its JobQueue::data and Job::data get borrowed back to it, together with the sequence number of this job, so that the driver can perform the appropriate interaction with the hardware. The submit_worker runs all ready jobs based on a credit count system, so that run_job() can be infallible. The same worker drops jobs (and their fences) that are already completed. Should this proof to be a performance issue, a second worker could trivially be added for this. Signed-off-by: Philipp Stanner Co-developed-by: Gary Guo (^ waiting for Gary to sign-off) --- rust/kernel/drm/job_queue.rs | 873 +++++++++++++++++++++++++++++++++++ rust/kernel/drm/mod.rs | 4 + 2 files changed, 877 insertions(+) create mode 100644 rust/kernel/drm/job_queue.rs diff --git a/rust/kernel/drm/job_queue.rs b/rust/kernel/drm/job_queue.rs new file mode 100644 index 000000000000..9ddcdda6e4d1 --- /dev/null +++ b/rust/kernel/drm/job_queue.rs @@ -0,0 +1,873 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2026 Red Hat Inc. + * Author: Philipp Stanner + */ + +//! DRM's job dependency manager and load balancer for GPUs with firmware +//! scheduling, i.e., GPUs that can spawn one ring exclusive per user context. +//! +//! JobQueue abstracts the entire handling of [`dma_buf::FenceContext`] and its +//! associated [`dma_buf::DriverFence`]s for the user. +//! +//! In this design, a JobQueue is always the owner of a [`dma_buf::FenceContext`] +//! which represents the timeline on the associated GPU ring. Jobs created on +//! this JobQueue always contain a [`dma_buf::DriverFence`] from this JobQueue, +//! where the fence sequence number corresponds with the job sequence number, +//! and the fence's generic payload data is also the job's payload data. This +//! allows for the dma_fence backend_ops callbacks interacting with the driver +//! through JobQueue. The trait [`JobQueueOps`] is the interface between driver +//! and fence backend_ops. +//! +//! [`Job`]s created on the [`JobQueue`] take over ownership of the user's data +//! and prepare all memory allocations. A later job submission through +//! [`Job::submit()`] sets up the fence and performs no fallible operations. +//! +//! Before jobs are submitted to a queue, dependencies in the form of +//! [`dma_buf::Fence`] can be set on them. Jobs are run by the JobQueue in +//! process context through a work item in FIFO order. Only jobs whose +//! dependency fences have all been signaled are run. If the front job's +//! dependency is not yet fulfilled, the JobQueue pauses until that's the case. +//! +//! If a dependency fence gets signaled with an error, what happens next depends +//! on the timing: +//! +//! a) If it signals with an error, or was already signaled with one, when one +//! tries to set a dependency through [`Job::add_dependency()`], the error is +//! returned to that function's caller, who can then decide how to proceed. +//! b) If it signals afterwards, JobQueue will invoke [`JobQueueOps::kill()`], +//! requesting from the driver that it tears down the JobQueue. +//! +//! JobQueue implements a credit count system, where the queue itself gets +//! assigned a capacity by the user, and jobs get assigned individual cost. Jobs +//! are only run if there is enough capacity; if not, running stops until that +//! is the case. These credits must accurately represent the capacity on the GPU +//! ring the queue was created for. +//! +//! The driver tells the JobQueue which jobs are finished through +//! [`JobQueue::complete_jobs_up_to_seqno()`]. This function is typically +//! invoked through the driver's GPU interrupt handler. +//! +//! Since jobs might contain data that must not drop in atomic context, and +//! since jobs' [`dma_buf::DriverFence`]s perform a call to +//! [`rcu::synchronize_rcu()`], they are dropped through a work item if [`Drop`] +//! is implemented for the job's payload data. +//! +//! From JobQueue's perspective, it is not necessary to call any of its API +//! functions with a driver lock held; this is actually discouraged to reduce +//! the probability of deadlocks. All JobQueue API functions are properly +//! ordered using the internal lock. +//! +//! JobQueue tells drivers to run jobs through [`JobQueueOps::run_job`], which +//! is called without the JobQueue's spinlock being held, i.e., the driver is +//! permitted to perform sleeping operations in the callback. JobQueue handles +//! races between `run_job` and [`JobQueue::complete_jobs_up_to_seqno()`]. +//! +//! This is pure Rust code that does not directly abstract C. + +#![allow(clippy::multiple_bound_locations)] // Clippy false positive, to be addressed by pin-init. + +use core::{ + convert::Infallible, + ops::Deref, // +}; + +use kernel::{ + dma_buf::dma_fence::{ + DriverFence, + DriverFenceAllocation, + Fence, + FenceCallback, + FenceCallbackRegistration, + FenceContext, + FenceContextOps, + FenceStatus, + }, + list::{ + impl_list_arc_safe, + impl_list_item, + List, + ListItem, + ListLinks, // + }, + new_srcu, + prelude::*, + str::CString, + sync::{ + aref::ARef, + atomic::{ + Atomic, + Relaxed, // + }, // + new_spinlock_irq, + SpinLockIrq, + }, // + sync::{ + atomic::AtomicFlag, + Srcu, // + }, + sync::{Arc, UniqueArc}, + workqueue::{ + self, + new_scoped_work, + ScopedWork, + ScopedWorkItem, + ScopedWorkRef, + ScopedQueue, // + }, // +}; + +/// A linked list where all elements are owned. +/// +/// This is a thin wrapper of `List` with all elements are `UniqueArc`. +struct UniqueList(List); + +impl Default for UniqueList { + fn default() -> Self { + Self::new() + } +} + +impl UniqueList { + fn new() -> Self { + Self(List::new()) + } + + fn push_front(&mut self, item: Pin>) { + self.0.push_front(item.into()) + } + + fn push_back(&mut self, item: Pin>) { + self.0.push_back(item.into()) + } + + fn pop_front(&mut self) -> Option>> { + Some( + Arc::into_unique_or_drop(self.0.pop_front()?.into_arc()) + .expect("UniqueList should only contain UniqueArc"), + ) + } +} + +struct DependencyWaker<'b, 'nr_of_deps, T: JobQueueOps + Send + Sync> { + nr_of_deps: &'nr_of_deps Atomic, + jq: &'b JobQueue, +} + +impl<'b, T: JobQueueOps + Send + Sync> FenceCallback for DependencyWaker<'b, '_, T> { + fn on_signal(&mut self, res: Result) { + if let Err(err) = res { + T::kill(self.jq.fctx.deref(), self.jq.id(), err); + return; + } + + // Last dependency kicks the worker. + if self.nr_of_deps.fetch_sub(1, Relaxed) == 1 { + // The drop-order of JobQueue ensures that all DependencyWakers are + // cancelled before the JobQueue itself is dropped. + self.jq.check_start_submit_worker(); + } + } +} + +/// A JobQueue job before it has been submitted. +/// +/// This is only useful for registering dependencies on and for submitting it. +pub struct Job<'a, T: JobQueueOps + Send + Sync> { + inner: Pin>>, + fence_allocation: DriverFenceAllocation<'a, DriverDataWrapper>, +} + +impl<'a, T: JobQueueOps + Send + Sync> Job<'a, T> { + /// Add a dependency for the job. + /// + /// A JobQueue will only run a job once `fence` has been signaled with `Ok(())`. + /// + /// This action can race with the fence being signaled. The returned Result + /// indicates success, obviously. Should an error be returned, this means + /// that the dependency fence signaled with an error, meaning the associated + /// action will not be completed. It is then up to the caller to decide + /// whether it can continue operating on this [`JobQueue`]. + pub fn add_dependency(&mut self, fence: &Fence) -> Result { + self.inner.as_mut().with_project(|job| { + // If the registration below gets registered successfully, a signaling + // fence will decrement the atomic. That could race. Thus, increment in + // advance. Ordering is ensured thanks to the dma_fence backend. + job.nr_of_deps.fetch_add(1, Relaxed); + + let dependency = try_pin_init!(Dependency { + links <- ListLinks::new(), + // SAFETY: The `registration` is not forgotten. It drops with JobQueue. + registration <- unsafe { + FenceCallbackRegistration::new( + fence, + DependencyWaker { + nr_of_deps: job.nr_of_deps, + jq: job.jq, + }, + ) + } + }); + match UniqueArc::pin_init(dependency, GFP_KERNEL) { + Err(e) => { + let _ = job.nr_of_deps.fetch_sub(1, Relaxed); + if e == ENOENT { + // Was already signaled. Return the fence's state. + match fence.status() { + FenceStatus::Signaled(res) => return res, + FenceStatus::Unsignaled => return Err(ENOENT), // Can never happen. + } + } + return Err(e); + } + Ok(dep) => { + job.dependencies.push_back(dep); + } + } + + Ok(()) + }) + } + + /// Submit the job. + /// + /// Returns a [`dma_buf::Fence`] on which the user can register callbacks. + /// The fence will be signaled once the job is completed through + /// [`JobQueue::complete_jobs_up_to_seqno`], which can happen immediately + /// after the job has been submitted. + /// + /// This function does all fence initialization under the JobQueue lock in + /// one go, including setting of the fence sequence number. Thus, it is + /// typically unnecessary (and advaised against) to call this function with + /// a driver lock held. + pub fn submit(self) -> ARef { + let jq = self.inner.jq; + jq.with_inner(|inner| { + // SAFETY: this is a lifetime transmute from `Job<'queue, T>` to + // `Job<'fctx, T>`. + // + // `job` internally holds a reference to `JobQueue`, while we are + // now moving `job` into `inner`, which may borrow from `fctx` but + // not the full `JobQueue`. So on surface this would be incorrect. + // However, as we drain `inner` inside `JobQueue::drop`, the + // reference is gone before any field is destructed. + let mut job = unsafe { core::mem::transmute::, Job<'_, T>>(self) }; + + // The lock orders setting of the sequence number, so no driver lock is needed. + let mut inner = inner.lock(); + let fence = job.fence_allocation.new_fence(); + let ret_fence = ARef::from(fence.as_fence()); + + *job.inner.as_mut().project().fence = Some(fence); + + inner.waiting_jobs.push_back(job.inner); + drop(inner); + jq.check_start_submit_worker(); + + ret_fence + }) + } +} + +#[pin_data] +struct Dependency<'b, 'nr_of_deps, T: JobQueueOps + Send + Sync> { + #[pin] + links: ListLinks, + #[pin] + registration: FenceCallbackRegistration>, +} + +impl_list_arc_safe! { + impl{'b, T: JobQueueOps + Send + Sync} ListArcSafe<0> for Dependency<'b, '_, T> { untracked; } +} +impl_list_item! { + impl{'b, T: JobQueueOps + Send + Sync} ListItem<0> for Dependency<'b, '_, T> { + using ListLinks { self.links }; } +} + +#[pin_data] +#[repr(C)] +struct JobInternal<'b, T: JobQueueOps + Send + Sync> { + #[pin] + links: ListLinks, + cost: u32, + fence: Option>>, + dependencies: UniqueList>, + nr_of_deps: Atomic, + jq: &'b JobQueue, +} + +impl_list_arc_safe! { + impl{'b, T: JobQueueOps + Send + Sync} ListArcSafe<0> for JobInternal<'b, T> { untracked; } +} +impl_list_item! { + impl{'b, T: JobQueueOps + Send + Sync} ListItem<0> for JobInternal<'b, T> { + using ListLinks { self.links }; } +} + +struct SubmitWorker<'a, 'fctx, T: JobQueueOps + Send + Sync> { + srcu: &'a Srcu, + revoked: &'a AtomicFlag, + inner: &'a SpinLockIrq>, + fctx: &'fctx FenceContext>, +} + +impl ScopedWorkItem for SubmitWorker<'_, '_, T> { + fn run(work: &ScopedWorkRef) { + // Synchronize against revocation in `JobQueue::drop`. + let _lock_guard = work.srcu.read_lock(); + if work.revoked.load(Relaxed) { + return; + } + + let mut jq = work.inner.lock(); + + while let Some(job) = jq.waiting_jobs.pop_front() { + // While we didn't hold the lock below for `run_job()`, it could be + // that a new job entered the waiting list which has an unfulfilled + // dependency. + if job.nr_of_deps.load(Relaxed) > 0 { + jq.waiting_jobs.push_front(job); + break; + } + let new_capacity = jq.capacity as i64 - i64::from(job.cost); + if new_capacity < 0 { + jq.waiting_jobs.push_front(job); + break; + } + *jq.as_mut().project().capacity = new_capacity as u64; + + let fence = job.fence.as_ref().expect("fence not present"); + let seqno = fence.as_fence().seqno(); + let runnable_job = JobRunnable { + seqno, + data: fence.deref(), + }; + + drop(jq); // Release lock. + T::run_job(work.fctx, runnable_job); + jq = work.inner.lock(); // Retake lock. + + // run_job() above needs to be called without lock protection. Thus, + // it is possible that the driver instructed completion right before + // we could re-take the lock. + if jq.last_completed >= seqno { + *jq.as_mut().project().capacity += u64::from(job.cost); + fence.signal(Ok(())); + + // Drop here directly if possible. Otherwise, let the DropWorker + // take care of it. + if core::mem::needs_drop::() { + jq.drop_list.push_back(job); + } + } else { + jq.running_jobs.push_back(job); + } + } + } +} + +struct DropWorker<'a, 'fctx, T: JobQueueOps + Send + Sync> { + srcu: &'a Srcu, + revoked: &'a AtomicFlag, + inner: &'a SpinLockIrq>, +} + +impl ScopedWorkItem for DropWorker<'_, '_, T> { + fn run(work: &ScopedWorkRef) { + // Synchronize against revocation in `JobQueue::drop`. + let _lock_guard = work.srcu.read_lock(); + if work.revoked.load(Relaxed) { + return; + } + + let mut jq_locked = work.inner.lock(); + + while let Some(job) = jq_locked.drop_list.pop_front() { + drop(jq_locked); + drop(job); + jq_locked = work.inner.lock(); + } + } +} + +#[pin_data] +struct JobQueueInner<'a, T: JobQueueOps + Send + Sync> { + #[pin] + /// List of jobs waiting to be run on the hardware. + waiting_jobs: UniqueList>, + #[pin] + /// List of jobs that are currently executing on the hardware. + running_jobs: UniqueList>, + /// The queue's current capacity. + capacity: u64, + /// The sequence number of the last completed job. + last_completed: u64, + #[pin] + /// Jobs that have finished executing and whose payload data `T` implements + /// `Drop`. These jobs are dropped in process context, which also allows for + /// their [`dma_buf::DriverFence`]s to implement the necessary RCU grace + /// period delay. + /// + /// If `Drop` is not implemented, this list is unused and the elements drop + /// directly in [`JobQueue::complete_jobs_up_to_seqno`]. + /// + /// Should the JobQueue drop and the [`DropWorker`] not yet have dropped all + /// jobs, that is no problem because the JobQueue itself drops in process + /// context. + drop_list: UniqueList>, +} + +/// A JobQueue. +/// +/// When you drop your JobQueue, it might still attempt for one RCU grace period +/// to submit more jobs via the [`JobQueueOps::run_job`] callback into your driver. +/// +/// Dropping a JobQueue results in all remaining waiting jobs' +/// fences being signaled with ECANCELED. All remaining running jobs' fences get +/// signaled with the error you provided in [`JobQueue::set_error()`], or +/// ECANCELED otherwise. +/// +/// A JobQueue must only ever drop in non-atomic / process context. +/// +/// # Examples +/// +/// ``` +/// use core::ops::Deref; +/// +/// use kernel::{ +/// str::CString, +/// dma_buf::*, +/// drm::{ +/// JobQueue, +/// JobQueueOps, +/// JobRunnable, +/// }, +/// }; +/// +/// let driver_name = CString::try_from_fmt(fmt!("dummy_driver"))?; +/// let timeline_name = CString::try_from_fmt(fmt!("dummy_timeline"))?; +/// +/// #[pin_data] +/// struct FenceContextData {} +/// +/// impl FenceContextData { +/// fn new() -> impl PinInit { +/// pin_init!(Self {}) +/// } +/// } +/// +/// impl FenceContextOps for FenceContextData { +/// type FenceDataType = (); +/// } +/// +/// let fctx_data = FenceContextData::new(); +/// +/// let fctx = KBox::pin_init( +/// FenceContext::new(0, c"dependency_driver", c"dependency_timeline", fctx_data), +/// GFP_KERNEL +/// )?; +/// +/// let dependency1 = fctx.new_fence_allocation(())?.new_fence(); +/// let dependency2 = fctx.new_fence_allocation(())?.new_fence(); +/// +/// #[pin_data] +/// struct JqData {} +/// +/// impl JqData { +/// fn new() -> impl PinInit { +/// pin_init!(Self {}) +/// } +/// } +/// +/// struct Payload { } +/// +/// impl JobQueueOps for JqData { +/// type JobDataType = Payload; +/// +/// fn run_job<'a>(&self, job: JobRunnable<'a, Payload>) { +/// pr_info!("Job Nr. {} gets run now.\n", job.seqno); +/// } +/// +/// fn kill(&self, jq_nr: u64, err: Error) { +/// pr_err!("The JQ Nr. {} became blocked forever because deps are blocked with err: {:?}\n", +/// jq_nr, err); +/// +/// // Here we would now schedule a driver work-item which invokes: +/// // `job_queue.set_error(err)` and then `drop(job_queue)`. +/// // +/// // We can't drop here right away because `kill()` is called with a +/// // fence lock held. +/// } +/// } +/// +/// let jq_data = JqData::new(); +/// let jq = KBox::pin_init( +/// JobQueue::new(driver_name, timeline_name, 1000, jq_data), +/// GFP_KERNEL +/// )?; +/// +/// +/// let mut job1 = jq.new_job(1, Payload { })?; +/// let mut job2 = jq.new_job(1, Payload { })?; +/// +/// job1.add_dependency(dependency1.as_fence())?; +/// job1.add_dependency(dependency2.as_fence())?; +/// +/// job2.add_dependency(dependency2.as_fence())?; +/// +/// struct Callback { +/// seqno: u64, +/// } +/// +/// impl FenceCallback for Callback { +/// fn on_signal(&mut self, res: Result) { +/// pr_info!("Job Nr. {} got completed.\n", self.seqno); +/// } +/// } +/// +/// let done_fence1 = job1.submit(); +/// let done_fence2 = job2.submit(); +/// +/// let cb = Callback { seqno: done_fence2.seqno() }; +/// // SAFETY: The registration is not leaked. +/// let reg = unsafe { FenceCallbackRegistration::new(&done_fence2, cb) }; +/// let reg = KBox::new(reg, GFP_KERNEL)?; +/// +/// dependency1.signal(Ok(())); +/// dependency2.signal(Ok(())); +/// +/// Ok::<(), Error>(()) +/// ``` +#[pin_data(PinnedDrop)] +pub struct JobQueue { + /// The queue's maximum available capacity. + max_capacity: u64, + #[pin] + #[uses('_: invariant)] + /// The work queue on which the JobQueue drops the user's payload data. + drop_queue: ScopedQueue<'inner>, + #[pin] + #[uses('_: invariant, 'revoked: invariant, 'srcu: invariant)] + /// A worker for dropping the queue's jobs. + drop_worker: ScopedWork>, + #[pin] + #[uses('_: invariant)] + /// The work queue on which the JobQueue runs jobs ([`JobQueueOps::run_job`]). + submit_queue: ScopedQueue<'submit_worker>, + #[pin] + #[uses('_: invariant)] + /// The worker submitting the queue's ready jobs. + submit_worker: ScopedWork>, + #[pin] + #[uses('fctx: invariant)] + /// The inner queue. + inner: SpinLockIrq>, + #[pin] + /// The [`dma_buf::FenceContext`] on which the queue creates + /// [`dma_buf::DriverFence`]s for the jobs. + fctx: FenceContext>, + /// A mechanism for preventing rescheduling of work items when the queue + /// drops. + revoked: AtomicFlag, + #[pin] + /// Needed for the `revoked` mechanism, since [`JobQueueOps::run_job`] might + /// sleep. + srcu: Srcu, + /// The error with which the JobQueue will signal still running jobs when it + /// drops. + err: Error, +} + +// SAFETY: The JobQueue is locked and pinned. +unsafe impl Sync for JobQueue {} +// SAFETY: The JobQueue is locked and pinned. +unsafe impl Send for JobQueue {} + +/// Object passed to the driver to run a job. +pub struct JobRunnable<'a, T> { + /// The job's sequence number. + pub seqno: u64, + /// The associated job data. + pub data: &'a T, +} + +/// Ops for the JobQueue. +pub trait JobQueueOps { + /// The type of the data passed over in [`JobQueue::new_job`]. + type JobDataType: Send + Sync; + + /// Instructs the driver to run this job. + /// + /// JobQueue calls this callback from process context without any locks + /// held, thus, the driver is permitted to sleep in its implementation. + /// + /// The driver must be aware that JobQueue trusts on its credit count system + /// for this operation to be infallible. + /// + /// This callback might be called while the driver is in the process of + /// dropping the JobQueue, but will never be called afterwards. + fn run_job<'a>(&self, job: JobRunnable<'a, Self::JobDataType>); + + /// Tells the driver to tear this JobQueue down. + /// + /// The JobQueue backend invokes this in case a job's dependency got + /// signaled with an error. This means that the dependency's associated + /// action on a GPU ring has failed and will not be recovered. + /// + /// Thus, it is necessary to tear this JobQueue down, too. Upon receiving + /// this ops, the driver should schedule a work_item in which it passes + /// through `err` with [`JobQueue::set_error()`] and then drops the + /// JobQueue. The JobQueue is robust against the driver continuing to call + /// [`JobQueue::submit()`], but these jobs will never run and will + /// ultimately be canceled with ECANCELED once tho JobQueue drops. + /// + /// The JobQueue might continue calling [`JobQueueOps::run_job()`] for jobs + /// with fulfilled dependencies, even after `kill()` was called. + /// + /// This ops is invoked in atomic context. Hence, you should schedule a + /// JobQueue teardown through an asynchronous path. + /// + /// The backend might call this function multiple times until the driver has + /// dropped the Jobqueue. + fn kill(&self, jq_id: u64, err: Error); +} + +#[pin_data] +struct DriverDataWrapper { + #[pin] + data: T, +} + +impl Deref for DriverDataWrapper { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.data + } +} + +impl DriverDataWrapper { + fn new(data: impl PinInit) -> impl PinInit + where + Error: From, + E: From, + { + try_pin_init!(Self { + data <- data, + }? E) + } +} + +impl FenceContextOps for DriverDataWrapper { + type FenceDataType = T::JobDataType; +} + +impl Deref for JobQueue { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.fctx.deref() + } +} +impl JobQueue { + /// Create a new JobQueue. + pub fn new( + // TODO: pass these as &CStr. Figure out how to get the lifetimes for PinInit right. + driver_name: CString, + timeline_name: CString, + capacity_param: u64, + data: impl PinInit, + ) -> impl PinInit + where + Error: From, + E: From, + { + try_pin_init!(Self { + srcu <- new_srcu!(), + revoked: AtomicFlag::new(false), + max_capacity: capacity_param, + err: ECANCELED, + // The initial sequence number always starts at 1, since hardware is + // initialized to 0. Also makes internal logic easier. + fctx <- FenceContext::new(1, &driver_name, &timeline_name, + DriverDataWrapper::new(data)), + inner <- new_spinlock_irq!(try_pin_init!(JobQueueInner { + capacity: capacity_param, + last_completed: 0, + waiting_jobs: UniqueList::new(), + running_jobs: UniqueList::new(), + drop_list: UniqueList::new(), + })), + // TODO: We need to set the WQ_MEM_RECLAIM bit! + // SAFETY: The queue is not leaked; its lifetime is tied to JobQueue. + submit_queue <- unsafe { ScopedQueue::new(c"JobQueueSubmitQueue") }, + submit_worker <- new_scoped_work!("JobQueueSubmitWorker", Ok(SubmitWorker { + srcu: Pin::get_ref(srcu), + revoked, + inner: Pin::get_ref(inner), + fctx: Pin::get_ref(fctx) + })), + // SAFETY: The queue is not leaked; its lifetime is tied to JobQueue. + drop_queue <- unsafe { ScopedQueue::new(c"JobQueueDropQueue") }, + drop_worker <- new_scoped_work!("JobQueueDropWorker", Ok(DropWorker { + srcu: Pin::get_ref(srcu), + revoked, + inner: Pin::get_ref(inner) + })), + }) + } + + /// Give the unique identifier for this JobQueue. + /// + /// This corresponds to the JobQueue's underlying [`dma_buf::FenceContext`]. + /// It is unique for all JobQueues in the entire system. + pub fn id(&self) -> u64 { + self.fctx.id() + } + + /// Create a new job. + /// + /// The `cost` specified here is what JobQueue subtracts from its capacity + /// to determine whether a job is runnable, i.e., has space on the GPU ring. + /// + /// The `data` contained by a job will be borrowed back to the driver once + /// the jobs shall be run through [`JobQueueOps::run_job`]. + pub fn new_job<'a>(&'a self, cost: u32, data: T::JobDataType) -> Result> { + if u64::from(cost) > self.max_capacity { + return Err(EINVAL); + } + + let fence_allocation = self.fctx.new_fence_allocation(data)?; + let job = try_pin_init!(JobInternal { + links <- ListLinks::new(), + cost, + fence: None, + nr_of_deps: Atomic::new(0), + dependencies: UniqueList::new(), + jq: &self, + }); + + let inner = UniqueArc::pin_init(job, GFP_KERNEL)?; + + Ok(Job { + inner, + fence_allocation, + }) + } + + fn check_start_submit_worker(&self) { + self.with_submit_worker(|submit_worker| { + // TODO this shouldn't be the system wq + // SAFETY: The worker is carried by the JobQueue and, thus, cannot be + // forgotten. + let _ = unsafe { workqueue::system_dfl().enqueue_scoped(submit_worker) }; + }) + } + + fn check_start_drop_worker(&self) { + self.with_drop_worker(|drop_worker| { + // TODO this shouldn't be the system wq + // Use the long-running wq for now, since here all the RCU delays manifest. + // SAFETY: The worker is carried by the JobQueue and, thus, cannot be + // forgotten. + let _ = unsafe { workqueue::system_long().enqueue_scoped(drop_worker) }; + }) + } + + /// Complete all the jobs in the queue's running_list, up to `seqno`. + /// + /// This signals the jobs' fences and schedules dropping of both the [`Job`] + /// and its [`DriverFence`]. + /// + /// This signals the associated fences with the `Ok(())` status. + /// + /// This function tells the implementation that there is now more capacity + /// for more jobs to be run. + pub fn complete_jobs_up_to_seqno(&self, seqno: u64) { + self.with_inner(|inner| { + let needs_drop = core::mem::needs_drop::(); + let mut jq_locked = inner.lock(); + let mut jq = jq_locked.as_mut().project(); + + if seqno < *jq.last_completed { + return; + } + + while let Some(mut job) = jq.running_jobs.pop_front() { + if job + .as_mut() + .project() + .fence + .as_mut() + .expect("fence not present") + .as_fence() + .seqno() + > seqno + { + jq.running_jobs.push_front(job); + break; + } + + *jq.capacity += u64::from(job.cost); + + let fence = job.fence.as_ref().expect("fence not present"); + fence.signal(Ok(())); + + if needs_drop { + jq.drop_list.push_back(job); + } + // Otherwise, job is dropped here directly. Its fence won't perform + // an RCU grace period wait. + } + + // This enables the SubmitWorker to complete a job that is not yet + // in 'running_jobs', in case of a race against the lock. + *jq.last_completed = seqno; + drop(jq_locked); + + self.check_start_submit_worker(); + if needs_drop { + self.check_start_drop_worker(); + } + }) + } + + /// Set the error with which this JobQueue will signal all running jobs' + /// fences. (The jobs on the waiting_list will all complete with ECANCELED + /// as their error) + /// + /// You should call this in the worker you kick in [`JobQueueOps::kill()`]. + /// + /// You can also call it when you yourself decide you want to drop this + /// JobQueue in another error path. + pub fn set_error(&mut self, error: Error) { + self.err = error; + } +} + +#[pinned_drop] +impl PinnedDrop for JobQueue { + fn drop(self: Pin<&mut Self>) { + // Revoke to prevent still existing DependencyWakers from kicking the + // SubmitWorker again before we could deregister them. + self.revoked.store(true, Relaxed); + self.srcu.synchronize(); + self.with_inner(|inner| { + let mut inner = inner.lock(); + let drop_list = core::mem::take(&mut inner.drop_list); + let waiting_jobs = core::mem::take(&mut inner.waiting_jobs); + let mut running_jobs = core::mem::take(&mut inner.running_jobs); + + while let Some(job) = running_jobs.pop_front() { + let fence = job.fence.as_ref().expect("fence not present"); + fence.signal(Err(self.err)); + } + + drop(inner); + drop(drop_list); + drop(waiting_jobs); + drop(running_jobs); + }) + } +} diff --git a/rust/kernel/drm/mod.rs b/rust/kernel/drm/mod.rs index fd6ed35bc35a..f2b7fbbce5bb 100644 --- a/rust/kernel/drm/mod.rs +++ b/rust/kernel/drm/mod.rs @@ -8,6 +8,7 @@ pub mod gem; pub mod gpuvm; pub mod ioctl; +pub mod job_queue; pub use self::device::Device; pub use self::device::DeviceContext; @@ -20,6 +21,9 @@ pub use self::driver::DriverInfo; pub use self::driver::Registration; pub use self::file::File; +pub use self::job_queue::JobQueue; +pub use self::job_queue::JobQueueOps; +pub use self::job_queue::JobRunnable; pub(crate) mod private { pub trait Sealed {} -- 2.56.0