/*
* drivers/video/tegra/host/nvhost_channel.c
*
* Tegra Graphics Host Channel
*
* Copyright (c) 2010-2020, NVIDIA Corporation. All rights reserved.
*
* 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 .
*/
#include "nvhost_channel.h"
#include "dev.h"
#include "nvhost_acm.h"
#include "nvhost_job.h"
#include "nvhost_vm.h"
#include "chip_support.h"
#include "vhost/vhost.h"
#include
#include
#include
#include
#include
#if LINUX_VERSION_CODE > KERNEL_VERSION(4, 13, 0)
#include
#endif
#define NVHOST_CHANNEL_LOW_PRIO_MAX_WAIT 50
/* Memory allocation for all supported channels */
int nvhost_alloc_channels(struct nvhost_master *host)
{
struct nvhost_channel *ch;
int index, err, max_channels;
host->chlist = kzalloc(nvhost_channel_nb_channels(host) *
sizeof(struct nvhost_channel *), GFP_KERNEL);
if (host->chlist == NULL) {
nvhost_err(&host->dev->dev, "failed to allocate channel list");
return -ENOMEM;
}
mutex_init(&host->chlist_mutex);
mutex_init(&host->ch_alloc_mutex);
max_channels = nvhost_channel_nb_channels(host);
sema_init(&host->free_channels, max_channels);
for (index = 0; index < max_channels; index++) {
ch = kzalloc(sizeof(*ch), GFP_KERNEL);
if (!ch) {
dev_err(&host->dev->dev, "failed to alloc channels\n");
return -ENOMEM;
}
/* initialize data structures */
nvhost_set_chanops(ch);
mutex_init(&ch->submitlock);
ch->chid = nvhost_channel_get_id_from_index(host, index);
/* initialize channel cdma */
err = nvhost_cdma_init(host->dev, &ch->cdma);
if (err) {
dev_err(&host->dev->dev, "failed to initialize cdma\n");
return err;
}
/* initialize hw specifics */
err = channel_op(ch).init(ch, host);
if (err < 0) {
dev_err(&host->dev->dev, "failed to init channel %d\n",
ch->chid);
return err;
}
/* store the channel */
host->chlist[index] = ch;
/* initialise gather filter for the channel */
nvhost_channel_init_gather_filter(host->dev, ch);
}
return 0;
}
/*
* Must be called with the chlist_mutex held.
*
* Returns the allocated channel. If no channel could be allocated within a
* reasonable time, returns ERR_PTR(-EBUSY). In an internal error situation,
* returns NULL. Under normal conditions, this call will block till an existing
* channel is freed.
*/
static struct nvhost_channel* nvhost_channel_alloc(struct nvhost_master *host)
{
int index, max_channels, err;
mutex_unlock(&host->chlist_mutex);
err = down_timeout(&host->free_channels, msecs_to_jiffies(5000));
mutex_lock(&host->chlist_mutex);
if (err)
return ERR_PTR(-EBUSY);
max_channels = nvhost_channel_nb_channels(host);
index = find_first_zero_bit(host->allocated_channels, max_channels);
if (index >= max_channels) {
pr_err("%s: Free channels sema and allocated mask out of sync!\n",
__func__);
return NULL;
}
/* Reserve the channel */
set_bit(index, host->allocated_channels);
return host->chlist[index];
}
/*
* Must be called with the chlist_mutex held.
*/
static void nvhost_channel_free(struct nvhost_master *host,
struct nvhost_channel *ch)
{
int index = nvhost_channel_get_index_from_id(host, ch->chid);
int bit_set = test_and_clear_bit(index, host->allocated_channels);
if (!bit_set) {
pr_err("%s: Freeing already freed channel?\n", __func__);
WARN_ON(1);
return;
}
up(&host->free_channels);
}
int nvhost_channel_remove_identifier(struct nvhost_device_data *pdata,
void *identifier)
{
struct nvhost_master *host = nvhost_get_host(pdata->pdev);
struct nvhost_channel *ch = NULL;
int index, max_channels;
mutex_lock(&host->chlist_mutex);
max_channels = nvhost_channel_nb_channels(host);
for (index = 0; index < max_channels; index++) {
ch = host->chlist[index];
if (ch->identifier == identifier) {
ch->identifier = NULL;
mutex_unlock(&host->chlist_mutex);
return 0;
}
}
mutex_unlock(&host->chlist_mutex);
return 0;
}
/* Unmap channel from device and free all resources, deinit device */
static void nvhost_channel_unmap_locked(struct kref *ref)
{
struct nvhost_channel *ch = container_of(ref, struct nvhost_channel,
refcount);
struct nvhost_device_data *pdata;
struct nvhost_master *host;
int err;
if (!ch->dev) {
pr_err("%s: freeing unmapped channel\n", __func__);
return;
}
pdata = platform_get_drvdata(ch->dev);
host = nvhost_get_host(pdata->pdev);
err = nvhost_module_busy(host->dev);
if (err) {
WARN(1, "failed to power-up host1x. leaking syncpts");
goto err_module_busy;
}
/* turn off channel cdma */
channel_cdma_op().stop(&ch->cdma);
/* log this event */
dev_dbg(&ch->dev->dev, "channel %d un-mapped\n", ch->chid);
trace_nvhost_channel_unmap_locked(pdata->pdev->name, ch->chid,
pdata->num_mapped_chs);
/* first, mark syncpoint as unused by hardware */
nvhost_syncpt_mark_unused(&host->syncpt, ch->chid);
nvhost_module_idle(host->dev);
err_module_busy:
/* drop reference to the vm */
nvhost_vm_put(ch->vm);
nvhost_channel_free(host, ch);
ch->vm = NULL;
ch->dev = NULL;
ch->identifier = NULL;
}
/* Maps free channel with device */
int nvhost_channel_map_with_vm(struct nvhost_device_data *pdata,
struct nvhost_channel **channel,
void *identifier,
void *vm_identifier)
{
struct nvhost_master *host = NULL;
struct nvhost_channel *ch = NULL;
int max_channels = 0;
int index = 0;
/* use vm_identifier if provided */
vm_identifier = vm_identifier ?
vm_identifier : (void *)(uintptr_t)current->tgid;
if (!pdata) {
pr_err("%s: NULL device data\n", __func__);
return -EINVAL;
}
host = nvhost_get_host(pdata->pdev);
max_channels = nvhost_channel_nb_channels(host);
mutex_lock(&host->ch_alloc_mutex);
mutex_lock(&host->chlist_mutex);
/* check if the channel is still in use */
for (index = 0; index < max_channels; index++) {
ch = host->chlist[index];
if (ch->identifier == identifier
&& kref_get_unless_zero(&ch->refcount)) {
/* yes, client can continue using it */
*channel = ch;
mutex_unlock(&host->chlist_mutex);
mutex_unlock(&host->ch_alloc_mutex);
trace_nvhost_channel_remap(pdata->pdev->name, ch->chid,
pdata->num_mapped_chs,
identifier);
return 0;
}
}
ch = nvhost_channel_alloc(host);
if (PTR_ERR(ch) == -EBUSY) {
pr_err("%s: Timeout while allocating channel\n", __func__);
mutex_unlock(&host->chlist_mutex);
mutex_unlock(&host->ch_alloc_mutex);
return -EBUSY;
}
if (!ch) {
pr_err("%s: Couldn't find a free channel. Sema out of sync\n",
__func__);
mutex_unlock(&host->chlist_mutex);
mutex_unlock(&host->ch_alloc_mutex);
return -EINVAL;
}
/* Bind the reserved channel to the device */
ch->dev = pdata->pdev;
ch->identifier = identifier;
kref_init(&ch->refcount);
/* channel is allocated, release mutex */
mutex_unlock(&host->chlist_mutex);
/* allocate vm */
ch->vm = nvhost_vm_allocate(pdata->pdev,
vm_identifier);
if (!ch->vm) {
pr_err("%s: Couldn't allocate vm\n", __func__);
goto err_alloc_vm;
}
/* Handle logging */
trace_nvhost_channel_map(pdata->pdev->name, ch->chid,
pdata->num_mapped_chs,
identifier);
dev_dbg(&ch->dev->dev, "channel %d mapped\n", ch->chid);
mutex_unlock(&host->ch_alloc_mutex);
*channel = ch;
return 0;
err_alloc_vm:
/* re-acquire chlist mutex for freeing the channel */
mutex_lock(&host->chlist_mutex);
#if LINUX_VERSION_CODE > KERNEL_VERSION(4, 13, 0)
refcount_dec(&ch->refcount.refcount);
#else
atomic_dec(&ch->refcount.refcount);
#endif
ch->dev = NULL;
ch->identifier = NULL;
nvhost_channel_free(host, ch);
mutex_unlock(&host->chlist_mutex);
mutex_unlock(&host->ch_alloc_mutex);
return -ENOMEM;
}
int nvhost_channel_map(struct nvhost_device_data *pdata,
struct nvhost_channel **channel,
void *identifier)
{
return nvhost_channel_map_with_vm(pdata, channel, identifier, NULL);
}
EXPORT_SYMBOL(nvhost_channel_map);
/* Free channel memory and list */
int nvhost_channel_list_free(struct nvhost_master *host)
{
int i;
for (i = 0; i < nvhost_channel_nb_channels(host); i++)
kfree(host->chlist[i]);
dev_info(&host->dev->dev, "channel list free'd\n");
return 0;
}
int nvhost_channel_abort(struct nvhost_device_data *pdata,
void *identifier)
{
int index;
int max_channels;
struct nvhost_master *host = NULL;
struct nvhost_channel *ch = NULL;
host = nvhost_get_host(pdata->pdev);
max_channels = nvhost_channel_nb_channels(host);
mutex_lock(&host->ch_alloc_mutex);
mutex_lock(&host->chlist_mutex);
/* first check if channel is mapped */
for (index = 0; index < max_channels; index++) {
ch = host->chlist[index];
if (ch->identifier == identifier
&& kref_get_unless_zero(&ch->refcount))
break;
}
mutex_unlock(&host->chlist_mutex);
mutex_unlock(&host->ch_alloc_mutex);
if (index == max_channels) /* no channel mapped */
return 0;
cdma_op().handle_timeout(&ch->cdma, true);
mutex_lock(&host->chlist_mutex);
kref_put(&ch->refcount, nvhost_channel_unmap_locked);
mutex_unlock(&host->chlist_mutex);
return 0;
}
bool nvhost_channel_is_reset_required(struct nvhost_channel *ch)
{
bool reset_required = false;
struct nvhost_device_data *pdata = platform_get_drvdata(ch->dev);
struct nvhost_master *master = nvhost_get_host(pdata->pdev);
struct nvhost_syncpt *syncpt = &master->syncpt;
unsigned int owner;
bool ch_own, cpu_own;
/* if resources are allocated per channel instance, the channel does
* not necessaryly hold the mlock */
if (pdata->resource_policy == RESOURCE_PER_CHANNEL_INSTANCE) {
/* check the owner */
syncpt_op().mutex_owner(syncpt, pdata->modulemutexes[0],
&cpu_own, &ch_own, &owner);
/* if this channel owns the lock, we need to reset the engine */
if (ch_own && owner == ch->chid)
reset_required = true;
} else {
/* if we allocate the resource per channel, the module is always
* contamined */
reset_required = true;
}
return reset_required;
}
void nvhost_channel_init_gather_filter(struct platform_device *pdev,
struct nvhost_channel *ch)
{
if (channel_op(ch).init_gather_filter)
channel_op(ch).init_gather_filter(pdev, ch);
}
int nvhost_channel_submit(struct nvhost_job *job)
{
return channel_op(job->ch).submit(job);
}
EXPORT_SYMBOL(nvhost_channel_submit);
void nvhost_getchannel(struct nvhost_channel *ch)
{
struct nvhost_device_data *pdata = platform_get_drvdata(ch->dev);
struct nvhost_master *host = nvhost_get_host(pdata->pdev);
#if LINUX_VERSION_CODE > KERNEL_VERSION(4, 13, 0)
trace_nvhost_getchannel(pdata->pdev->name,
refcount_read(&ch->refcount.refcount), ch->chid);
#else
trace_nvhost_getchannel(pdata->pdev->name,
atomic_read(&ch->refcount.refcount), ch->chid);
#endif
mutex_lock(&host->chlist_mutex);
kref_get(&ch->refcount);
mutex_unlock(&host->chlist_mutex);
}
EXPORT_SYMBOL(nvhost_getchannel);
void nvhost_putchannel(struct nvhost_channel *ch, int cnt)
{
struct nvhost_device_data *pdata = platform_get_drvdata(ch->dev);
struct nvhost_master *host = nvhost_get_host(pdata->pdev);
#if LINUX_VERSION_CODE > KERNEL_VERSION(4, 13, 0)
int i;
trace_nvhost_putchannel(pdata->pdev->name,
refcount_read(&ch->refcount.refcount), ch->chid);
mutex_lock(&host->chlist_mutex);
for (i = 0; i < cnt; i++)
kref_put(&ch->refcount, nvhost_channel_unmap_locked);
mutex_unlock(&host->chlist_mutex);
#else
trace_nvhost_putchannel(pdata->pdev->name,
atomic_read(&ch->refcount.refcount), ch->chid);
/* Avoid race in case where one thread is acquiring a channel with
* the same identifier that we are dropping now; If we first drop
* the reference counter and then acquire the mutex, channel map
* routine may have acquired another channel with the same identifier.
* This may happen if all submits from one user get finished - and
* the very same user submits more work.
*
* In order to avoid above race, always acquire chlist mutex before
* entering channel unmap routine.
*/
mutex_lock(&host->chlist_mutex);
kref_sub(&ch->refcount, cnt, nvhost_channel_unmap_locked);
mutex_unlock(&host->chlist_mutex);
#endif
}
EXPORT_SYMBOL(nvhost_putchannel);
int nvhost_channel_suspend(struct nvhost_master *host)
{
int i;
for (i = 0; i < nvhost_channel_nb_channels(host); i++) {
struct nvhost_channel *ch = host->chlist[i];
if (channel_cdma_op().stop && ch->dev)
channel_cdma_op().stop(&ch->cdma);
}
return 0;
}
int nvhost_channel_nb_channels(struct nvhost_master *host)
{
return host->info.nb_channels;
}
int nvhost_channel_ch_base(struct nvhost_master *host)
{
return host->info.ch_base;
}
int nvhost_channel_ch_limit(struct nvhost_master *host)
{
return host->info.ch_limit;
}
int nvhost_channel_get_id_from_index(struct nvhost_master *host, int index)
{
return nvhost_channel_ch_base(host) + index;
}
int nvhost_channel_get_index_from_id(struct nvhost_master *host, int chid)
{
return chid - nvhost_channel_ch_base(host);
}
bool nvhost_channel_is_resource_policy_per_device(struct nvhost_device_data *pdata)
{
return (pdata->resource_policy == RESOURCE_PER_DEVICE);
}
EXPORT_SYMBOL(nvhost_channel_is_resource_policy_per_device);