xref: /linux/drivers/gpu/drm/scheduler/tests/mock_scheduler.c (revision 148ab873d8194cb44f29474da3b4428fa2e100d1)
1 // SPDX-License-Identifier: GPL-2.0
2 /* Copyright (c) 2025 Valve Corporation */
3 
4 #include <kunit/device.h>
5 
6 #include "sched_tests.h"
7 
8 /*
9  * Here we implement the mock "GPU" (or the scheduler backend) which is used by
10  * the DRM scheduler unit tests in order to exercise the core functionality.
11  *
12  * Test cases are implemented in a separate file.
13  */
14 
15 /**
16  * drm_mock_sched_entity_new - Create a new mock scheduler entity
17  *
18  * @test: KUnit test owning the entity
19  * @priority: Scheduling priority
20  * @sched: Mock scheduler on which the entity can be scheduled
21  *
22  * Returns: New mock scheduler entity with allocation managed by the test
23  */
24 struct drm_mock_sched_entity *
25 drm_mock_sched_entity_new(struct kunit *test,
26 			  enum drm_sched_priority priority,
27 			  struct drm_mock_scheduler *sched)
28 {
29 	struct drm_mock_sched_entity *entity;
30 	struct drm_gpu_scheduler *drm_sched;
31 	int ret;
32 
33 	entity = kunit_kzalloc(test, sizeof(*entity), GFP_KERNEL);
34 	KUNIT_ASSERT_NOT_NULL(test, entity);
35 
36 	drm_sched = &sched->base;
37 	ret = drm_sched_entity_init(&entity->base,
38 				    priority,
39 				    &drm_sched, 1,
40 				    NULL);
41 	KUNIT_ASSERT_EQ(test, ret, 0);
42 
43 	entity->test = test;
44 
45 	return entity;
46 }
47 
48 /**
49  * drm_mock_sched_entity_free - Destroys a mock scheduler entity
50  *
51  * @entity: Entity to destroy
52  *
53  * To be used from the test cases once done with the entity.
54  */
55 void drm_mock_sched_entity_free(struct drm_mock_sched_entity *entity)
56 {
57 	drm_sched_entity_destroy(&entity->base);
58 }
59 
60 static void drm_mock_sched_job_complete(struct drm_mock_sched_job *job)
61 {
62 	struct drm_mock_scheduler *sched =
63 		drm_sched_to_mock_sched(job->base.sched);
64 
65 	lockdep_assert_held(&sched->lock);
66 
67 	job->flags |= DRM_MOCK_SCHED_JOB_DONE;
68 	list_del(&job->link);
69 	dma_fence_signal_locked(&job->hw_fence);
70 	complete(&job->done);
71 }
72 
73 static enum hrtimer_restart
74 drm_mock_sched_job_signal_timer(struct hrtimer *hrtimer)
75 {
76 	struct drm_mock_sched_job *job =
77 		container_of(hrtimer, typeof(*job), timer);
78 	struct drm_mock_scheduler *sched =
79 		drm_sched_to_mock_sched(job->base.sched);
80 	struct drm_mock_sched_job *next;
81 	ktime_t now = ktime_get();
82 	unsigned long flags;
83 	LIST_HEAD(signal);
84 
85 	spin_lock_irqsave(&sched->lock, flags);
86 	list_for_each_entry_safe(job, next, &sched->job_list, link) {
87 		if (!job->duration_us)
88 			break;
89 
90 		if (ktime_before(now, job->finish_at))
91 			break;
92 
93 		sched->hw_timeline.cur_seqno = job->hw_fence.seqno;
94 		drm_mock_sched_job_complete(job);
95 	}
96 	spin_unlock_irqrestore(&sched->lock, flags);
97 
98 	return HRTIMER_NORESTART;
99 }
100 
101 /**
102  * drm_mock_sched_job_new - Create a new mock scheduler job
103  *
104  * @test: KUnit test owning the job
105  * @entity: Scheduler entity of the job
106  *
107  * Returns: New mock scheduler job with allocation managed by the test
108  */
109 struct drm_mock_sched_job *
110 drm_mock_sched_job_new(struct kunit *test,
111 		       struct drm_mock_sched_entity *entity)
112 {
113 	struct drm_mock_sched_job *job;
114 	int ret;
115 
116 	job = kunit_kzalloc(test, sizeof(*job), GFP_KERNEL);
117 	KUNIT_ASSERT_NOT_NULL(test, job);
118 
119 	ret = drm_sched_job_init(&job->base,
120 				 &entity->base,
121 				 1,
122 				 NULL,
123 				 1);
124 	KUNIT_ASSERT_EQ(test, ret, 0);
125 
126 	job->test = test;
127 
128 	init_completion(&job->done);
129 	INIT_LIST_HEAD(&job->link);
130 	hrtimer_setup(&job->timer, drm_mock_sched_job_signal_timer,
131 		      CLOCK_MONOTONIC, HRTIMER_MODE_ABS);
132 
133 	return job;
134 }
135 
136 static const char *drm_mock_sched_hw_fence_driver_name(struct dma_fence *fence)
137 {
138 	return "drm_mock_sched";
139 }
140 
141 static const char *
142 drm_mock_sched_hw_fence_timeline_name(struct dma_fence *fence)
143 {
144 	struct drm_mock_sched_job *job =
145 		container_of(fence, typeof(*job), hw_fence);
146 
147 	return (const char *)job->base.sched->name;
148 }
149 
150 static void drm_mock_sched_hw_fence_release(struct dma_fence *fence)
151 {
152 	struct drm_mock_sched_job *job =
153 		container_of(fence, typeof(*job), hw_fence);
154 
155 	hrtimer_cancel(&job->timer);
156 
157 	/* Containing job is freed by the kunit framework */
158 }
159 
160 static const struct dma_fence_ops drm_mock_sched_hw_fence_ops = {
161 	.get_driver_name = drm_mock_sched_hw_fence_driver_name,
162 	.get_timeline_name = drm_mock_sched_hw_fence_timeline_name,
163 	.release = drm_mock_sched_hw_fence_release,
164 };
165 
166 static struct dma_fence *mock_sched_run_job(struct drm_sched_job *sched_job)
167 {
168 	struct drm_mock_scheduler *sched =
169 		drm_sched_to_mock_sched(sched_job->sched);
170 	struct drm_mock_sched_job *job = drm_sched_job_to_mock_job(sched_job);
171 
172 	dma_fence_init(&job->hw_fence,
173 		       &drm_mock_sched_hw_fence_ops,
174 		       &sched->lock,
175 		       sched->hw_timeline.context,
176 		       atomic_inc_return(&sched->hw_timeline.next_seqno));
177 
178 	dma_fence_get(&job->hw_fence); /* Reference for the job_list */
179 
180 	spin_lock_irq(&sched->lock);
181 	if (job->duration_us) {
182 		ktime_t prev_finish_at = 0;
183 
184 		if (!list_empty(&sched->job_list)) {
185 			struct drm_mock_sched_job *prev =
186 				list_last_entry(&sched->job_list, typeof(*prev),
187 						link);
188 
189 			prev_finish_at = prev->finish_at;
190 		}
191 
192 		if (!prev_finish_at)
193 			prev_finish_at = ktime_get();
194 
195 		job->finish_at = ktime_add_us(prev_finish_at, job->duration_us);
196 	}
197 	list_add_tail(&job->link, &sched->job_list);
198 	if (job->finish_at)
199 		hrtimer_start(&job->timer, job->finish_at, HRTIMER_MODE_ABS);
200 	spin_unlock_irq(&sched->lock);
201 
202 	return &job->hw_fence;
203 }
204 
205 /*
206  * Normally, drivers would take appropriate measures in this callback, such as
207  * killing the entity the faulty job is associated with, resetting the hardware
208  * and / or resubmitting non-faulty jobs.
209  *
210  * For the mock scheduler, there are no hardware rings to be resetted nor jobs
211  * to be resubmitted. Thus, this function merely ensures that
212  *   a) timedout fences get signaled properly and removed from the pending list
213  *   b) the mock scheduler framework gets informed about the timeout via a flag
214  *   c) The drm_sched_job, not longer needed, gets freed
215  */
216 static enum drm_gpu_sched_stat
217 mock_sched_timedout_job(struct drm_sched_job *sched_job)
218 {
219 	struct drm_mock_scheduler *sched = drm_sched_to_mock_sched(sched_job->sched);
220 	struct drm_mock_sched_job *job = drm_sched_job_to_mock_job(sched_job);
221 	unsigned long flags;
222 
223 	if (job->flags & DRM_MOCK_SCHED_JOB_DONT_RESET) {
224 		job->flags |= DRM_MOCK_SCHED_JOB_RESET_SKIPPED;
225 		return DRM_GPU_SCHED_STAT_NO_HANG;
226 	}
227 
228 	spin_lock_irqsave(&sched->lock, flags);
229 	if (!dma_fence_is_signaled_locked(&job->hw_fence)) {
230 		list_del(&job->link);
231 		job->flags |= DRM_MOCK_SCHED_JOB_TIMEDOUT;
232 		dma_fence_set_error(&job->hw_fence, -ETIMEDOUT);
233 		dma_fence_signal_locked(&job->hw_fence);
234 	}
235 	spin_unlock_irqrestore(&sched->lock, flags);
236 
237 	dma_fence_put(&job->hw_fence);
238 	drm_sched_job_cleanup(sched_job);
239 	/* Mock job itself is freed by the kunit framework. */
240 
241 	return DRM_GPU_SCHED_STAT_RESET;
242 }
243 
244 static void mock_sched_free_job(struct drm_sched_job *sched_job)
245 {
246 	struct drm_mock_sched_job *job = drm_sched_job_to_mock_job(sched_job);
247 
248 	dma_fence_put(&job->hw_fence);
249 	drm_sched_job_cleanup(sched_job);
250 
251 	/* Mock job itself is freed by the kunit framework. */
252 }
253 
254 static void mock_sched_cancel_job(struct drm_sched_job *sched_job)
255 {
256 	struct drm_mock_scheduler *sched = drm_sched_to_mock_sched(sched_job->sched);
257 	struct drm_mock_sched_job *job = drm_sched_job_to_mock_job(sched_job);
258 	unsigned long flags;
259 
260 	hrtimer_cancel(&job->timer);
261 
262 	spin_lock_irqsave(&sched->lock, flags);
263 	if (!dma_fence_is_signaled_locked(&job->hw_fence)) {
264 		list_del(&job->link);
265 		dma_fence_set_error(&job->hw_fence, -ECANCELED);
266 		dma_fence_signal_locked(&job->hw_fence);
267 	}
268 	spin_unlock_irqrestore(&sched->lock, flags);
269 
270 	/*
271 	 * The GPU Scheduler will call drm_sched_backend_ops.free_job(), still.
272 	 * Mock job itself is freed by the kunit framework.
273 	 */
274 }
275 
276 static const struct drm_sched_backend_ops drm_mock_scheduler_ops = {
277 	.run_job = mock_sched_run_job,
278 	.timedout_job = mock_sched_timedout_job,
279 	.free_job = mock_sched_free_job,
280 	.cancel_job = mock_sched_cancel_job,
281 };
282 
283 /**
284  * drm_mock_sched_new - Create a new mock scheduler
285  *
286  * @test: KUnit test owning the job
287  * @timeout: Job timeout to set
288  *
289  * Returns: New mock scheduler with allocation managed by the test
290  */
291 struct drm_mock_scheduler *drm_mock_sched_new(struct kunit *test, long timeout)
292 {
293 	static unsigned int instance;
294 	struct drm_sched_init_args args = {
295 		.ops		= &drm_mock_scheduler_ops,
296 		.num_rqs	= DRM_SCHED_PRIORITY_COUNT,
297 		.credit_limit	= U32_MAX,
298 		.hang_limit	= 1,
299 		.timeout	= timeout,
300 		.name		= "drm-mock-scheduler",
301 	};
302 	struct drm_mock_scheduler *sched;
303 	struct device *dev;
304 	char name[64];
305 	int ret;
306 
307 	sched = kunit_kzalloc(test, sizeof(*sched), GFP_KERNEL);
308 	KUNIT_ASSERT_NOT_NULL(test, sched);
309 
310 	snprintf(name, sizeof(name), "%s-%u", args.name, instance++);
311 	dev = kunit_device_register(test, name);
312 	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, dev);
313 
314 	args.dev = dev;
315 
316 	ret = drm_sched_init(&sched->base, &args);
317 	KUNIT_ASSERT_EQ(test, ret, 0);
318 
319 	sched->test = test;
320 	sched->hw_timeline.context = dma_fence_context_alloc(1);
321 	atomic_set(&sched->hw_timeline.next_seqno, 0);
322 	INIT_LIST_HEAD(&sched->job_list);
323 	spin_lock_init(&sched->lock);
324 
325 	return sched;
326 }
327 
328 /**
329  * drm_mock_sched_fini - Destroys a mock scheduler
330  *
331  * @sched: Scheduler to destroy
332  *
333  * To be used from the test cases once done with the scheduler.
334  */
335 void drm_mock_sched_fini(struct drm_mock_scheduler *sched)
336 {
337 	drm_sched_fini(&sched->base);
338 }
339 
340 /**
341  * drm_mock_sched_advance - Advances the mock scheduler timeline
342  *
343  * @sched: Scheduler timeline to advance
344  * @num: By how many jobs to advance
345  *
346  * Advancing the scheduler timeline by a number of seqnos will trigger
347  * signalling of the hardware fences and unlinking the jobs from the internal
348  * scheduler tracking.
349  *
350  * This can be used from test cases which want complete control of the simulated
351  * job execution timing. For example submitting one job with no set duration
352  * would never complete it before test cases advances the timeline by one.
353  */
354 unsigned int drm_mock_sched_advance(struct drm_mock_scheduler *sched,
355 				    unsigned int num)
356 {
357 	struct drm_mock_sched_job *job, *next;
358 	unsigned int found = 0;
359 	unsigned long flags;
360 	LIST_HEAD(signal);
361 
362 	spin_lock_irqsave(&sched->lock, flags);
363 	if (WARN_ON_ONCE(sched->hw_timeline.cur_seqno + num <
364 			 sched->hw_timeline.cur_seqno))
365 		goto unlock;
366 	sched->hw_timeline.cur_seqno += num;
367 	list_for_each_entry_safe(job, next, &sched->job_list, link) {
368 		if (sched->hw_timeline.cur_seqno < job->hw_fence.seqno)
369 			break;
370 
371 		drm_mock_sched_job_complete(job);
372 		found++;
373 	}
374 unlock:
375 	spin_unlock_irqrestore(&sched->lock, flags);
376 
377 	return found;
378 }
379 
380 MODULE_DESCRIPTION("DRM mock scheduler and tests");
381 MODULE_LICENSE("GPL");
382