diff options
Diffstat (limited to 'drivers/gpu/host1x/intr.c')
| -rw-r--r-- | drivers/gpu/host1x/intr.c | 354 | 
1 files changed, 354 insertions, 0 deletions
diff --git a/drivers/gpu/host1x/intr.c b/drivers/gpu/host1x/intr.c new file mode 100644 index 00000000000..2491bf82e30 --- /dev/null +++ b/drivers/gpu/host1x/intr.c @@ -0,0 +1,354 @@ +/* + * Tegra host1x Interrupt Management + * + * Copyright (c) 2010-2013, NVIDIA Corporation. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU General Public License, + * version 2, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License + * along with this program.  If not, see <http://www.gnu.org/licenses/>. + */ + +#include <linux/clk.h> +#include <linux/interrupt.h> +#include <linux/slab.h> +#include <linux/irq.h> + +#include <trace/events/host1x.h> +#include "channel.h" +#include "dev.h" +#include "intr.h" + +/* Wait list management */ + +enum waitlist_state { +	WLS_PENDING, +	WLS_REMOVED, +	WLS_CANCELLED, +	WLS_HANDLED +}; + +static void waiter_release(struct kref *kref) +{ +	kfree(container_of(kref, struct host1x_waitlist, refcount)); +} + +/* + * add a waiter to a waiter queue, sorted by threshold + * returns true if it was added at the head of the queue + */ +static bool add_waiter_to_queue(struct host1x_waitlist *waiter, +				struct list_head *queue) +{ +	struct host1x_waitlist *pos; +	u32 thresh = waiter->thresh; + +	list_for_each_entry_reverse(pos, queue, list) +		if ((s32)(pos->thresh - thresh) <= 0) { +			list_add(&waiter->list, &pos->list); +			return false; +		} + +	list_add(&waiter->list, queue); +	return true; +} + +/* + * run through a waiter queue for a single sync point ID + * and gather all completed waiters into lists by actions + */ +static void remove_completed_waiters(struct list_head *head, u32 sync, +			struct list_head completed[HOST1X_INTR_ACTION_COUNT]) +{ +	struct list_head *dest; +	struct host1x_waitlist *waiter, *next, *prev; + +	list_for_each_entry_safe(waiter, next, head, list) { +		if ((s32)(waiter->thresh - sync) > 0) +			break; + +		dest = completed + waiter->action; + +		/* consolidate submit cleanups */ +		if (waiter->action == HOST1X_INTR_ACTION_SUBMIT_COMPLETE && +		    !list_empty(dest)) { +			prev = list_entry(dest->prev, +					  struct host1x_waitlist, list); +			if (prev->data == waiter->data) { +				prev->count++; +				dest = NULL; +			} +		} + +		/* PENDING->REMOVED or CANCELLED->HANDLED */ +		if (atomic_inc_return(&waiter->state) == WLS_HANDLED || !dest) { +			list_del(&waiter->list); +			kref_put(&waiter->refcount, waiter_release); +		} else +			list_move_tail(&waiter->list, dest); +	} +} + +static void reset_threshold_interrupt(struct host1x *host, +				      struct list_head *head, +				      unsigned int id) +{ +	u32 thresh = +		list_first_entry(head, struct host1x_waitlist, list)->thresh; + +	host1x_hw_intr_set_syncpt_threshold(host, id, thresh); +	host1x_hw_intr_enable_syncpt_intr(host, id); +} + +static void action_submit_complete(struct host1x_waitlist *waiter) +{ +	struct host1x_channel *channel = waiter->data; + +	host1x_cdma_update(&channel->cdma); + +	/*  Add nr_completed to trace */ +	trace_host1x_channel_submit_complete(dev_name(channel->dev), +					     waiter->count, waiter->thresh); + +} + +static void action_wakeup(struct host1x_waitlist *waiter) +{ +	wait_queue_head_t *wq = waiter->data; +	wake_up(wq); +} + +static void action_wakeup_interruptible(struct host1x_waitlist *waiter) +{ +	wait_queue_head_t *wq = waiter->data; +	wake_up_interruptible(wq); +} + +typedef void (*action_handler)(struct host1x_waitlist *waiter); + +static action_handler action_handlers[HOST1X_INTR_ACTION_COUNT] = { +	action_submit_complete, +	action_wakeup, +	action_wakeup_interruptible, +}; + +static void run_handlers(struct list_head completed[HOST1X_INTR_ACTION_COUNT]) +{ +	struct list_head *head = completed; +	int i; + +	for (i = 0; i < HOST1X_INTR_ACTION_COUNT; ++i, ++head) { +		action_handler handler = action_handlers[i]; +		struct host1x_waitlist *waiter, *next; + +		list_for_each_entry_safe(waiter, next, head, list) { +			list_del(&waiter->list); +			handler(waiter); +			WARN_ON(atomic_xchg(&waiter->state, WLS_HANDLED) != +				WLS_REMOVED); +			kref_put(&waiter->refcount, waiter_release); +		} +	} +} + +/* + * Remove & handle all waiters that have completed for the given syncpt + */ +static int process_wait_list(struct host1x *host, +			     struct host1x_syncpt *syncpt, +			     u32 threshold) +{ +	struct list_head completed[HOST1X_INTR_ACTION_COUNT]; +	unsigned int i; +	int empty; + +	for (i = 0; i < HOST1X_INTR_ACTION_COUNT; ++i) +		INIT_LIST_HEAD(completed + i); + +	spin_lock(&syncpt->intr.lock); + +	remove_completed_waiters(&syncpt->intr.wait_head, threshold, +				 completed); + +	empty = list_empty(&syncpt->intr.wait_head); +	if (empty) +		host1x_hw_intr_disable_syncpt_intr(host, syncpt->id); +	else +		reset_threshold_interrupt(host, &syncpt->intr.wait_head, +					  syncpt->id); + +	spin_unlock(&syncpt->intr.lock); + +	run_handlers(completed); + +	return empty; +} + +/* + * Sync point threshold interrupt service thread function + * Handles sync point threshold triggers, in thread context + */ + +static void syncpt_thresh_work(struct work_struct *work) +{ +	struct host1x_syncpt_intr *syncpt_intr = +		container_of(work, struct host1x_syncpt_intr, work); +	struct host1x_syncpt *syncpt = +		container_of(syncpt_intr, struct host1x_syncpt, intr); +	unsigned int id = syncpt->id; +	struct host1x *host = syncpt->host; + +	(void)process_wait_list(host, syncpt, +				host1x_syncpt_load(host->syncpt + id)); +} + +int host1x_intr_add_action(struct host1x *host, u32 id, u32 thresh, +			   enum host1x_intr_action action, void *data, +			   struct host1x_waitlist *waiter, void **ref) +{ +	struct host1x_syncpt *syncpt; +	int queue_was_empty; + +	if (waiter == NULL) { +		pr_warn("%s: NULL waiter\n", __func__); +		return -EINVAL; +	} + +	/* initialize a new waiter */ +	INIT_LIST_HEAD(&waiter->list); +	kref_init(&waiter->refcount); +	if (ref) +		kref_get(&waiter->refcount); +	waiter->thresh = thresh; +	waiter->action = action; +	atomic_set(&waiter->state, WLS_PENDING); +	waiter->data = data; +	waiter->count = 1; + +	syncpt = host->syncpt + id; + +	spin_lock(&syncpt->intr.lock); + +	queue_was_empty = list_empty(&syncpt->intr.wait_head); + +	if (add_waiter_to_queue(waiter, &syncpt->intr.wait_head)) { +		/* added at head of list - new threshold value */ +		host1x_hw_intr_set_syncpt_threshold(host, id, thresh); + +		/* added as first waiter - enable interrupt */ +		if (queue_was_empty) +			host1x_hw_intr_enable_syncpt_intr(host, id); +	} + +	spin_unlock(&syncpt->intr.lock); + +	if (ref) +		*ref = waiter; +	return 0; +} + +void host1x_intr_put_ref(struct host1x *host, u32 id, void *ref) +{ +	struct host1x_waitlist *waiter = ref; +	struct host1x_syncpt *syncpt; + +	while (atomic_cmpxchg(&waiter->state, WLS_PENDING, WLS_CANCELLED) == +	       WLS_REMOVED) +		schedule(); + +	syncpt = host->syncpt + id; +	(void)process_wait_list(host, syncpt, +				host1x_syncpt_load(host->syncpt + id)); + +	kref_put(&waiter->refcount, waiter_release); +} + +int host1x_intr_init(struct host1x *host, unsigned int irq_sync) +{ +	unsigned int id; +	u32 nb_pts = host1x_syncpt_nb_pts(host); + +	mutex_init(&host->intr_mutex); +	host->intr_syncpt_irq = irq_sync; +	host->intr_wq = create_workqueue("host_syncpt"); +	if (!host->intr_wq) +		return -ENOMEM; + +	for (id = 0; id < nb_pts; ++id) { +		struct host1x_syncpt *syncpt = host->syncpt + id; + +		spin_lock_init(&syncpt->intr.lock); +		INIT_LIST_HEAD(&syncpt->intr.wait_head); +		snprintf(syncpt->intr.thresh_irq_name, +			 sizeof(syncpt->intr.thresh_irq_name), +			 "host1x_sp_%02d", id); +	} + +	host1x_intr_start(host); + +	return 0; +} + +void host1x_intr_deinit(struct host1x *host) +{ +	host1x_intr_stop(host); +	destroy_workqueue(host->intr_wq); +} + +void host1x_intr_start(struct host1x *host) +{ +	u32 hz = clk_get_rate(host->clk); +	int err; + +	mutex_lock(&host->intr_mutex); +	err = host1x_hw_intr_init_host_sync(host, DIV_ROUND_UP(hz, 1000000), +					    syncpt_thresh_work); +	if (err) { +		mutex_unlock(&host->intr_mutex); +		return; +	} +	mutex_unlock(&host->intr_mutex); +} + +void host1x_intr_stop(struct host1x *host) +{ +	unsigned int id; +	struct host1x_syncpt *syncpt = host->syncpt; +	u32 nb_pts = host1x_syncpt_nb_pts(host); + +	mutex_lock(&host->intr_mutex); + +	host1x_hw_intr_disable_all_syncpt_intrs(host); + +	for (id = 0; id < nb_pts; ++id) { +		struct host1x_waitlist *waiter, *next; + +		list_for_each_entry_safe(waiter, next, +			&syncpt[id].intr.wait_head, list) { +			if (atomic_cmpxchg(&waiter->state, +			    WLS_CANCELLED, WLS_HANDLED) == WLS_CANCELLED) { +				list_del(&waiter->list); +				kref_put(&waiter->refcount, waiter_release); +			} +		} + +		if (!list_empty(&syncpt[id].intr.wait_head)) { +			/* output diagnostics */ +			mutex_unlock(&host->intr_mutex); +			pr_warn("%s cannot stop syncpt intr id=%d\n", +				__func__, id); +			return; +		} +	} + +	host1x_hw_intr_free_syncpt_irq(host); + +	mutex_unlock(&host->intr_mutex); +}  |