Contributors: 2
Author Tokens Token Proportion Commits Commit Proportion
Tvrtko A. Ursulin 1410 99.79% 2 66.67%
Thomas Zimmermann 3 0.21% 1 33.33%
Total 1413 3


// SPDX-License-Identifier: GPL-2.0
/* Copyright (c) 2025 Valve Corporation */

#include "sched_tests.h"

/*
 * Here we implement the mock "GPU" (or the scheduler backend) which is used by
 * the DRM scheduler unit tests in order to exercise the core functionality.
 *
 * Test cases are implemented in a separate file.
 */

/**
 * drm_mock_sched_entity_new - Create a new mock scheduler entity
 *
 * @test: KUnit test owning the entity
 * @priority: Scheduling priority
 * @sched: Mock scheduler on which the entity can be scheduled
 *
 * Returns: New mock scheduler entity with allocation managed by the test
 */
struct drm_mock_sched_entity *
drm_mock_sched_entity_new(struct kunit *test,
			  enum drm_sched_priority priority,
			  struct drm_mock_scheduler *sched)
{
	struct drm_mock_sched_entity *entity;
	struct drm_gpu_scheduler *drm_sched;
	int ret;

	entity = kunit_kzalloc(test, sizeof(*entity), GFP_KERNEL);
	KUNIT_ASSERT_NOT_NULL(test, entity);

	drm_sched = &sched->base;
	ret = drm_sched_entity_init(&entity->base,
				    priority,
				    &drm_sched, 1,
				    NULL);
	KUNIT_ASSERT_EQ(test, ret, 0);

	entity->test = test;

	return entity;
}

/**
 * drm_mock_sched_entity_free - Destroys a mock scheduler entity
 *
 * @entity: Entity to destroy
 *
 * To be used from the test cases once done with the entity.
 */
void drm_mock_sched_entity_free(struct drm_mock_sched_entity *entity)
{
	drm_sched_entity_destroy(&entity->base);
}

static void drm_mock_sched_job_complete(struct drm_mock_sched_job *job)
{
	struct drm_mock_scheduler *sched =
		drm_sched_to_mock_sched(job->base.sched);

	lockdep_assert_held(&sched->lock);

	job->flags |= DRM_MOCK_SCHED_JOB_DONE;
	list_move_tail(&job->link, &sched->done_list);
	dma_fence_signal(&job->hw_fence);
	complete(&job->done);
}

static enum hrtimer_restart
drm_mock_sched_job_signal_timer(struct hrtimer *hrtimer)
{
	struct drm_mock_sched_job *job =
		container_of(hrtimer, typeof(*job), timer);
	struct drm_mock_scheduler *sched =
		drm_sched_to_mock_sched(job->base.sched);
	struct drm_mock_sched_job *next;
	ktime_t now = ktime_get();
	unsigned long flags;
	LIST_HEAD(signal);

	spin_lock_irqsave(&sched->lock, flags);
	list_for_each_entry_safe(job, next, &sched->job_list, link) {
		if (!job->duration_us)
			break;

		if (ktime_before(now, job->finish_at))
			break;

		sched->hw_timeline.cur_seqno = job->hw_fence.seqno;
		drm_mock_sched_job_complete(job);
	}
	spin_unlock_irqrestore(&sched->lock, flags);

	return HRTIMER_NORESTART;
}

/**
 * drm_mock_sched_job_new - Create a new mock scheduler job
 *
 * @test: KUnit test owning the job
 * @entity: Scheduler entity of the job
 *
 * Returns: New mock scheduler job with allocation managed by the test
 */
struct drm_mock_sched_job *
drm_mock_sched_job_new(struct kunit *test,
		       struct drm_mock_sched_entity *entity)
{
	struct drm_mock_sched_job *job;
	int ret;

	job = kunit_kzalloc(test, sizeof(*job), GFP_KERNEL);
	KUNIT_ASSERT_NOT_NULL(test, job);

	ret = drm_sched_job_init(&job->base,
				 &entity->base,
				 1,
				 NULL);
	KUNIT_ASSERT_EQ(test, ret, 0);

	job->test = test;

	init_completion(&job->done);
	spin_lock_init(&job->lock);
	INIT_LIST_HEAD(&job->link);
	hrtimer_setup(&job->timer, drm_mock_sched_job_signal_timer,
		      CLOCK_MONOTONIC, HRTIMER_MODE_ABS);

	return job;
}

static const char *drm_mock_sched_hw_fence_driver_name(struct dma_fence *fence)
{
	return "drm_mock_sched";
}

static const char *
drm_mock_sched_hw_fence_timeline_name(struct dma_fence *fence)
{
	struct drm_mock_sched_job *job =
		container_of(fence, typeof(*job), hw_fence);

	return (const char *)job->base.sched->name;
}

static void drm_mock_sched_hw_fence_release(struct dma_fence *fence)
{
	struct drm_mock_sched_job *job =
		container_of(fence, typeof(*job), hw_fence);

	hrtimer_cancel(&job->timer);

	/* Containing job is freed by the kunit framework */
}

static const struct dma_fence_ops drm_mock_sched_hw_fence_ops = {
	.get_driver_name = drm_mock_sched_hw_fence_driver_name,
	.get_timeline_name = drm_mock_sched_hw_fence_timeline_name,
	.release = drm_mock_sched_hw_fence_release,
};

static struct dma_fence *mock_sched_run_job(struct drm_sched_job *sched_job)
{
	struct drm_mock_scheduler *sched =
		drm_sched_to_mock_sched(sched_job->sched);
	struct drm_mock_sched_job *job = drm_sched_job_to_mock_job(sched_job);

	dma_fence_init(&job->hw_fence,
		       &drm_mock_sched_hw_fence_ops,
		       &job->lock,
		       sched->hw_timeline.context,
		       atomic_inc_return(&sched->hw_timeline.next_seqno));

	dma_fence_get(&job->hw_fence); /* Reference for the job_list */

	spin_lock_irq(&sched->lock);
	if (job->duration_us) {
		ktime_t prev_finish_at = 0;

		if (!list_empty(&sched->job_list)) {
			struct drm_mock_sched_job *prev =
				list_last_entry(&sched->job_list, typeof(*prev),
						link);

			prev_finish_at = prev->finish_at;
		}

		if (!prev_finish_at)
			prev_finish_at = ktime_get();

		job->finish_at = ktime_add_us(prev_finish_at, job->duration_us);
	}
	list_add_tail(&job->link, &sched->job_list);
	if (job->finish_at)
		hrtimer_start(&job->timer, job->finish_at, HRTIMER_MODE_ABS);
	spin_unlock_irq(&sched->lock);

	return &job->hw_fence;
}

static enum drm_gpu_sched_stat
mock_sched_timedout_job(struct drm_sched_job *sched_job)
{
	struct drm_mock_sched_job *job = drm_sched_job_to_mock_job(sched_job);

	job->flags |= DRM_MOCK_SCHED_JOB_TIMEDOUT;

	return DRM_GPU_SCHED_STAT_NOMINAL;
}

static void mock_sched_free_job(struct drm_sched_job *sched_job)
{
	struct drm_mock_scheduler *sched =
			drm_sched_to_mock_sched(sched_job->sched);
	struct drm_mock_sched_job *job = drm_sched_job_to_mock_job(sched_job);
	unsigned long flags;

	/* Remove from the scheduler done list. */
	spin_lock_irqsave(&sched->lock, flags);
	list_del(&job->link);
	spin_unlock_irqrestore(&sched->lock, flags);
	dma_fence_put(&job->hw_fence);

	drm_sched_job_cleanup(sched_job);

	/* Mock job itself is freed by the kunit framework. */
}

static const struct drm_sched_backend_ops drm_mock_scheduler_ops = {
	.run_job = mock_sched_run_job,
	.timedout_job = mock_sched_timedout_job,
	.free_job = mock_sched_free_job
};

/**
 * drm_mock_sched_new - Create a new mock scheduler
 *
 * @test: KUnit test owning the job
 * @timeout: Job timeout to set
 *
 * Returns: New mock scheduler with allocation managed by the test
 */
struct drm_mock_scheduler *drm_mock_sched_new(struct kunit *test, long timeout)
{
	struct drm_sched_init_args args = {
		.ops		= &drm_mock_scheduler_ops,
		.num_rqs	= DRM_SCHED_PRIORITY_COUNT,
		.credit_limit	= U32_MAX,
		.hang_limit	= 1,
		.timeout	= timeout,
		.name		= "drm-mock-scheduler",
	};
	struct drm_mock_scheduler *sched;
	int ret;

	sched = kunit_kzalloc(test, sizeof(*sched), GFP_KERNEL);
	KUNIT_ASSERT_NOT_NULL(test, sched);

	ret = drm_sched_init(&sched->base, &args);
	KUNIT_ASSERT_EQ(test, ret, 0);

	sched->test = test;
	sched->hw_timeline.context = dma_fence_context_alloc(1);
	atomic_set(&sched->hw_timeline.next_seqno, 0);
	INIT_LIST_HEAD(&sched->job_list);
	INIT_LIST_HEAD(&sched->done_list);
	spin_lock_init(&sched->lock);

	return sched;
}

/**
 * drm_mock_sched_fini - Destroys a mock scheduler
 *
 * @sched: Scheduler to destroy
 *
 * To be used from the test cases once done with the scheduler.
 */
void drm_mock_sched_fini(struct drm_mock_scheduler *sched)
{
	struct drm_mock_sched_job *job, *next;
	unsigned long flags;
	LIST_HEAD(list);

	drm_sched_wqueue_stop(&sched->base);

	/* Force complete all unfinished jobs. */
	spin_lock_irqsave(&sched->lock, flags);
	list_for_each_entry_safe(job, next, &sched->job_list, link)
		list_move_tail(&job->link, &list);
	spin_unlock_irqrestore(&sched->lock, flags);

	list_for_each_entry(job, &list, link)
		hrtimer_cancel(&job->timer);

	spin_lock_irqsave(&sched->lock, flags);
	list_for_each_entry_safe(job, next, &list, link)
		drm_mock_sched_job_complete(job);
	spin_unlock_irqrestore(&sched->lock, flags);

	/*
	 * Free completed jobs and jobs not yet processed by the DRM scheduler
	 * free worker.
	 */
	spin_lock_irqsave(&sched->lock, flags);
	list_for_each_entry_safe(job, next, &sched->done_list, link)
		list_move_tail(&job->link, &list);
	spin_unlock_irqrestore(&sched->lock, flags);

	list_for_each_entry_safe(job, next, &list, link)
		mock_sched_free_job(&job->base);

	drm_sched_fini(&sched->base);
}

/**
 * drm_mock_sched_advance - Advances the mock scheduler timeline
 *
 * @sched: Scheduler timeline to advance
 * @num: By how many jobs to advance
 *
 * Advancing the scheduler timeline by a number of seqnos will trigger
 * signalling of the hardware fences and unlinking the jobs from the internal
 * scheduler tracking.
 *
 * This can be used from test cases which want complete control of the simulated
 * job execution timing. For example submitting one job with no set duration
 * would never complete it before test cases advances the timeline by one.
 */
unsigned int drm_mock_sched_advance(struct drm_mock_scheduler *sched,
				    unsigned int num)
{
	struct drm_mock_sched_job *job, *next;
	unsigned int found = 0;
	unsigned long flags;
	LIST_HEAD(signal);

	spin_lock_irqsave(&sched->lock, flags);
	if (WARN_ON_ONCE(sched->hw_timeline.cur_seqno + num <
			 sched->hw_timeline.cur_seqno))
		goto unlock;
	sched->hw_timeline.cur_seqno += num;
	list_for_each_entry_safe(job, next, &sched->job_list, link) {
		if (sched->hw_timeline.cur_seqno < job->hw_fence.seqno)
			break;

		drm_mock_sched_job_complete(job);
		found++;
	}
unlock:
	spin_unlock_irqrestore(&sched->lock, flags);

	return found;
}

MODULE_DESCRIPTION("DRM mock scheduler and tests");
MODULE_LICENSE("GPL");