android_kernel_samsung_sm86.../qcom/opensource/graphics-kernel/kgsl_vbo.c
David Wronek 880d405719 Add 'qcom/opensource/graphics-kernel/' from commit 'b4fdc4c04295ac59109ae19d64747522740c3f14'
git-subtree-dir: qcom/opensource/graphics-kernel
git-subtree-mainline: 992813d9c1
git-subtree-split: b4fdc4c042
Change-Id:
repo: https://git.codelinaro.org/clo/la/platform/vendor/qcom/opensource/graphics-kernel
tag: GRAPHICS.LA.14.0.r1-07700-lanai.0
2024-10-06 16:44:56 +02:00

733 lines
18 KiB
C

// SPDX-License-Identifier: GPL-2.0-only
/*
* Copyright (c) 2020-2021, The Linux Foundation. All rights reserved.
* Copyright (c) 2023-2024 Qualcomm Innovation Center, Inc. All rights reserved.
*/
#include <linux/file.h>
#include <linux/interval_tree.h>
#include <linux/seq_file.h>
#include <linux/sync_file.h>
#include <linux/slab.h>
#include "kgsl_device.h"
#include "kgsl_mmu.h"
#include "kgsl_reclaim.h"
#include "kgsl_sharedmem.h"
#include "kgsl_trace.h"
struct kgsl_memdesc_bind_range {
struct kgsl_mem_entry *entry;
struct interval_tree_node range;
};
static struct kgsl_memdesc_bind_range *bind_to_range(struct interval_tree_node *node)
{
return container_of(node, struct kgsl_memdesc_bind_range, range);
}
static struct kgsl_memdesc_bind_range *bind_range_create(u64 start, u64 last,
struct kgsl_mem_entry *entry)
{
struct kgsl_memdesc_bind_range *range =
kzalloc(sizeof(*range), GFP_KERNEL);
if (!range)
return ERR_PTR(-ENOMEM);
range->range.start = start;
range->range.last = last;
range->entry = kgsl_mem_entry_get(entry);
if (!range->entry) {
kfree(range);
return ERR_PTR(-EINVAL);
}
atomic_inc(&entry->vbo_count);
return range;
}
static void bind_range_destroy(struct kgsl_memdesc_bind_range *range)
{
struct kgsl_mem_entry *entry = range->entry;
atomic_dec(&entry->vbo_count);
kgsl_mem_entry_put(entry);
kfree(range);
}
static u64 bind_range_len(struct kgsl_memdesc_bind_range *range)
{
return (range->range.last - range->range.start) + 1;
}
void kgsl_memdesc_print_vbo_ranges(struct kgsl_mem_entry *entry,
struct seq_file *s)
{
struct interval_tree_node *next;
struct kgsl_memdesc *memdesc = &entry->memdesc;
if (!(memdesc->flags & KGSL_MEMFLAGS_VBO))
return;
/*
* We are called in an atomic context so try to get the mutex but if we
* don't then skip this item
*/
if (!mutex_trylock(&memdesc->ranges_lock))
return;
next = interval_tree_iter_first(&memdesc->ranges, 0, ~0UL);
while (next) {
struct kgsl_memdesc_bind_range *range = bind_to_range(next);
seq_printf(s, "%5d %5d 0x%16.16lx-0x%16.16lx\n",
entry->id, range->entry->id, range->range.start,
range->range.last);
next = interval_tree_iter_next(next, 0, ~0UL);
}
mutex_unlock(&memdesc->ranges_lock);
}
static void kgsl_memdesc_remove_range(struct kgsl_mem_entry *target,
u64 start, u64 last, struct kgsl_mem_entry *entry)
{
struct interval_tree_node *node, *next;
struct kgsl_memdesc_bind_range *range;
struct kgsl_memdesc *memdesc = &target->memdesc;
mutex_lock(&memdesc->ranges_lock);
next = interval_tree_iter_first(&memdesc->ranges, start, last);
while (next) {
node = next;
range = bind_to_range(node);
next = interval_tree_iter_next(node, start, last);
/*
* If entry is null, consider it as a special request. Unbind
* the entire range between start and last in this case.
*/
if (!entry || range->entry->id == entry->id) {
if (kgsl_mmu_unmap_range(memdesc->pagetable,
memdesc, range->range.start, bind_range_len(range)))
continue;
interval_tree_remove(node, &memdesc->ranges);
trace_kgsl_mem_remove_bind_range(target,
range->range.start, range->entry,
bind_range_len(range));
if (!(memdesc->flags & KGSL_MEMFLAGS_VBO_NO_MAP_ZERO))
kgsl_mmu_map_zero_page_to_range(memdesc->pagetable,
memdesc, range->range.start, bind_range_len(range));
bind_range_destroy(range);
}
}
mutex_unlock(&memdesc->ranges_lock);
}
static int kgsl_memdesc_add_range(struct kgsl_mem_entry *target,
u64 start, u64 last, struct kgsl_mem_entry *entry, u64 offset)
{
struct interval_tree_node *node, *next;
struct kgsl_memdesc *memdesc = &target->memdesc;
struct kgsl_memdesc_bind_range *range =
bind_range_create(start, last, entry);
int ret = 0;
if (IS_ERR(range))
return PTR_ERR(range);
mutex_lock(&memdesc->ranges_lock);
/*
* If the VBO maps the zero page, then we can unmap the requested range
* in one call. Otherwise we have to figure out what ranges to unmap
* while walking the interval tree.
*/
if (!(memdesc->flags & KGSL_MEMFLAGS_VBO_NO_MAP_ZERO)) {
ret = kgsl_mmu_unmap_range(memdesc->pagetable, memdesc, start,
last - start + 1);
if (ret)
goto error;
}
next = interval_tree_iter_first(&memdesc->ranges, start, last);
while (next) {
struct kgsl_memdesc_bind_range *cur;
node = next;
cur = bind_to_range(node);
next = interval_tree_iter_next(node, start, last);
trace_kgsl_mem_remove_bind_range(target, cur->range.start,
cur->entry, bind_range_len(cur));
interval_tree_remove(node, &memdesc->ranges);
if (start <= cur->range.start) {
if (last >= cur->range.last) {
/* Unmap the entire cur range */
if (memdesc->flags & KGSL_MEMFLAGS_VBO_NO_MAP_ZERO) {
ret = kgsl_mmu_unmap_range(memdesc->pagetable, memdesc,
cur->range.start,
cur->range.last - cur->range.start + 1);
if (ret) {
interval_tree_insert(node, &memdesc->ranges);
goto error;
}
}
bind_range_destroy(cur);
continue;
}
/* Unmap the range overlapping cur */
if (memdesc->flags & KGSL_MEMFLAGS_VBO_NO_MAP_ZERO) {
ret = kgsl_mmu_unmap_range(memdesc->pagetable, memdesc,
cur->range.start,
last - cur->range.start + 1);
if (ret) {
interval_tree_insert(node, &memdesc->ranges);
goto error;
}
}
/* Adjust the start of the mapping */
cur->range.start = last + 1;
/* And put it back into the tree */
interval_tree_insert(node, &memdesc->ranges);
trace_kgsl_mem_add_bind_range(target,
cur->range.start, cur->entry, bind_range_len(cur));
} else {
if (last < cur->range.last) {
struct kgsl_memdesc_bind_range *temp;
/*
* The range is split into two so make a new
* entry for the far side
*/
temp = bind_range_create(last + 1, cur->range.last,
cur->entry);
/* FIXME: Uhoh, this would be bad */
BUG_ON(IS_ERR(temp));
interval_tree_insert(&temp->range,
&memdesc->ranges);
trace_kgsl_mem_add_bind_range(target,
temp->range.start,
temp->entry, bind_range_len(temp));
}
/* Unmap the range overlapping cur */
if (memdesc->flags & KGSL_MEMFLAGS_VBO_NO_MAP_ZERO) {
ret = kgsl_mmu_unmap_range(memdesc->pagetable, memdesc,
start,
min_t(u64, cur->range.last, last) - start + 1);
if (ret) {
interval_tree_insert(node, &memdesc->ranges);
goto error;
}
}
cur->range.last = start - 1;
interval_tree_insert(node, &memdesc->ranges);
trace_kgsl_mem_add_bind_range(target, cur->range.start,
cur->entry, bind_range_len(cur));
}
}
ret = kgsl_mmu_map_child(memdesc->pagetable, memdesc, start,
&entry->memdesc, offset, last - start + 1);
if (ret)
goto error;
/* Add the new range */
interval_tree_insert(&range->range, &memdesc->ranges);
trace_kgsl_mem_add_bind_range(target, range->range.start,
range->entry, bind_range_len(range));
mutex_unlock(&memdesc->ranges_lock);
return ret;
error:
bind_range_destroy(range);
mutex_unlock(&memdesc->ranges_lock);
return ret;
}
static void kgsl_sharedmem_vbo_put_gpuaddr(struct kgsl_memdesc *memdesc)
{
struct interval_tree_node *node, *next;
struct kgsl_memdesc_bind_range *range;
int ret = 0;
bool unmap_fail;
/*
* If the VBO maps the zero range then we can unmap the entire
* pagetable region in one call.
*/
if (!(memdesc->flags & KGSL_MEMFLAGS_VBO_NO_MAP_ZERO))
ret = kgsl_mmu_unmap_range(memdesc->pagetable, memdesc,
0, memdesc->size);
unmap_fail = ret;
/*
* FIXME: do we have a use after free potential here? We might need to
* lock this and set a "do not update" bit
*/
/* Now delete each range and release the mem entries */
next = interval_tree_iter_first(&memdesc->ranges, 0, ~0UL);
while (next) {
node = next;
range = bind_to_range(node);
next = interval_tree_iter_next(node, 0, ~0UL);
interval_tree_remove(node, &memdesc->ranges);
/* Unmap this range */
if (memdesc->flags & KGSL_MEMFLAGS_VBO_NO_MAP_ZERO)
ret = kgsl_mmu_unmap_range(memdesc->pagetable, memdesc,
range->range.start,
range->range.last - range->range.start + 1);
/* Put the child's refcount if unmap succeeds */
if (!ret)
bind_range_destroy(range);
else
kfree(range);
unmap_fail = unmap_fail || ret;
}
if (unmap_fail)
return;
/* Put back the GPU address */
kgsl_mmu_put_gpuaddr(memdesc->pagetable, memdesc);
memdesc->gpuaddr = 0;
memdesc->pagetable = NULL;
}
static struct kgsl_memdesc_ops kgsl_vbo_ops = {
.put_gpuaddr = kgsl_sharedmem_vbo_put_gpuaddr,
};
int kgsl_sharedmem_allocate_vbo(struct kgsl_device *device,
struct kgsl_memdesc *memdesc, u64 size, u64 flags)
{
size = PAGE_ALIGN(size);
/* Make sure that VBOs are supported by the MMU */
if (WARN_ON_ONCE(!kgsl_mmu_has_feature(device,
KGSL_MMU_SUPPORT_VBO)))
return -EOPNOTSUPP;
kgsl_memdesc_init(device, memdesc, flags);
memdesc->priv = 0;
memdesc->ops = &kgsl_vbo_ops;
memdesc->size = size;
/* Set up the interval tree and lock */
memdesc->ranges = RB_ROOT_CACHED;
mutex_init(&memdesc->ranges_lock);
return 0;
}
static bool kgsl_memdesc_check_range(struct kgsl_memdesc *memdesc,
u64 offset, u64 length)
{
return ((offset < memdesc->size) &&
(offset + length > offset) &&
(offset + length) <= memdesc->size);
}
static void kgsl_sharedmem_free_bind_op(struct kgsl_sharedmem_bind_op *op)
{
int i;
if (IS_ERR_OR_NULL(op))
return;
for (i = 0; i < op->nr_ops; i++) {
/* Decrement the vbo_count we added when creating the bind_op */
if (op->ops[i].entry)
atomic_dec(&op->ops[i].entry->vbo_count);
/* Release the reference on the child entry */
kgsl_mem_entry_put_deferred(op->ops[i].entry);
}
/* Release the reference on the target entry */
kgsl_mem_entry_put_deferred(op->target);
kvfree(op->ops);
kfree(op);
}
struct kgsl_sharedmem_bind_op *
kgsl_sharedmem_create_bind_op(struct kgsl_process_private *private,
u32 target_id, void __user *ranges, u32 ranges_nents,
u64 ranges_size)
{
struct kgsl_sharedmem_bind_op *op;
struct kgsl_mem_entry *target;
int ret, i;
/* There must be at least one defined operation */
if (!ranges_nents)
return ERR_PTR(-EINVAL);
/* Find the target memory entry */
target = kgsl_sharedmem_find_id(private, target_id);
if (!target)
return ERR_PTR(-ENOENT);
if (!(target->memdesc.flags & KGSL_MEMFLAGS_VBO)) {
kgsl_mem_entry_put(target);
return ERR_PTR(-EINVAL);
}
/* Make a container for the bind operations */
op = kzalloc(sizeof(*op), GFP_KERNEL);
if (!op) {
kgsl_mem_entry_put(target);
return ERR_PTR(-ENOMEM);
}
/*
* Make an array for the individual operations. Use __GFP_NOWARN and
* __GFP_NORETRY to make sure a very large request quietly fails
*/
op->ops = kvcalloc(ranges_nents, sizeof(*op->ops),
GFP_KERNEL | __GFP_NOWARN | __GFP_NORETRY);
if (!op->ops) {
kfree(op);
kgsl_mem_entry_put(target);
return ERR_PTR(-ENOMEM);
}
op->nr_ops = ranges_nents;
op->target = target;
/* Make sure process is pinned in memory before proceeding */
atomic_inc(&private->cmd_count);
ret = kgsl_reclaim_to_pinned_state(private);
if (ret)
goto err;
for (i = 0; i < ranges_nents; i++) {
struct kgsl_gpumem_bind_range range;
struct kgsl_mem_entry *entry;
u32 size;
size = min_t(u32, sizeof(range), ranges_size);
ret = -EINVAL;
if (copy_from_user(&range, ranges, size)) {
ret = -EFAULT;
goto err;
}
/* The offset must be page aligned */
if (!PAGE_ALIGNED(range.target_offset))
goto err;
/* The length of the operation must be aligned and non zero */
if (!range.length || !PAGE_ALIGNED(range.length))
goto err;
/* Make sure the range fits in the target */
if (!kgsl_memdesc_check_range(&target->memdesc,
range.target_offset, range.length))
goto err;
/*
* Special case: Consider child id 0 as a special request incase of
* unbind. This helps to unbind the specified range (could span multiple
* child buffers) without supplying backing physical buffer information.
*/
if (range.child_id == 0 && range.op == KGSL_GPUMEM_RANGE_OP_UNBIND) {
op->ops[i].entry = NULL;
op->ops[i].start = range.target_offset;
op->ops[i].last = range.target_offset + range.length - 1;
/* Child offset doesn't matter for unbind. set it to 0 */
op->ops[i].child_offset = 0;
op->ops[i].op = range.op;
ranges += ranges_size;
continue;
}
/* Get the child object */
op->ops[i].entry = kgsl_sharedmem_find_id(private,
range.child_id);
entry = op->ops[i].entry;
if (!entry) {
ret = -ENOENT;
goto err;
}
/* Keep the child pinned in memory */
atomic_inc(&entry->vbo_count);
/* Make sure the child is not a VBO */
if ((entry->memdesc.flags & KGSL_MEMFLAGS_VBO)) {
ret = -EINVAL;
goto err;
}
/*
* Make sure that only secure children are mapped in secure VBOs
* and vice versa
*/
if ((target->memdesc.flags & KGSL_MEMFLAGS_SECURE) !=
(entry->memdesc.flags & KGSL_MEMFLAGS_SECURE)) {
ret = -EPERM;
goto err;
}
/* Make sure the range operation is valid */
if (range.op != KGSL_GPUMEM_RANGE_OP_BIND &&
range.op != KGSL_GPUMEM_RANGE_OP_UNBIND)
goto err;
if (range.op == KGSL_GPUMEM_RANGE_OP_BIND) {
if (!PAGE_ALIGNED(range.child_offset))
goto err;
/* Make sure the range fits in the child */
if (!kgsl_memdesc_check_range(&entry->memdesc,
range.child_offset, range.length))
goto err;
} else {
/* For unop operations the child offset must be 0 */
if (range.child_offset)
goto err;
}
op->ops[i].entry = entry;
op->ops[i].start = range.target_offset;
op->ops[i].last = range.target_offset + range.length - 1;
op->ops[i].child_offset = range.child_offset;
op->ops[i].op = range.op;
ranges += ranges_size;
}
atomic_dec(&private->cmd_count);
init_completion(&op->comp);
kref_init(&op->ref);
return op;
err:
atomic_dec(&private->cmd_count);
kgsl_sharedmem_free_bind_op(op);
return ERR_PTR(ret);
}
void kgsl_sharedmem_bind_range_destroy(struct kref *kref)
{
struct kgsl_sharedmem_bind_op *op = container_of(kref,
struct kgsl_sharedmem_bind_op, ref);
kgsl_sharedmem_free_bind_op(op);
}
static void kgsl_sharedmem_bind_worker(struct work_struct *work)
{
struct kgsl_sharedmem_bind_op *op = container_of(work,
struct kgsl_sharedmem_bind_op, work);
int i;
for (i = 0; i < op->nr_ops; i++) {
if (op->ops[i].op == KGSL_GPUMEM_RANGE_OP_BIND)
kgsl_memdesc_add_range(op->target,
op->ops[i].start,
op->ops[i].last,
op->ops[i].entry,
op->ops[i].child_offset);
else
kgsl_memdesc_remove_range(op->target,
op->ops[i].start,
op->ops[i].last,
op->ops[i].entry);
}
/* Wake up any threads waiting for the bind operation */
complete_all(&op->comp);
if (op->callback)
op->callback(op);
/* Put the refcount we took when scheduling the worker */
kgsl_sharedmem_put_bind_op(op);
}
void kgsl_sharedmem_bind_ranges(struct kgsl_sharedmem_bind_op *op)
{
/* Take a reference to the operation while it is scheduled */
kref_get(&op->ref);
INIT_WORK(&op->work, kgsl_sharedmem_bind_worker);
schedule_work(&op->work);
}
struct kgsl_sharedmem_bind_fence {
struct dma_fence base;
spinlock_t lock;
int fd;
struct kgsl_sharedmem_bind_op *op;
};
static const char *bind_fence_get_driver_name(struct dma_fence *fence)
{
return "kgsl_sharedmem_bind";
}
static const char *bind_fence_get_timeline_name(struct dma_fence *fence)
{
return "(unbound)";
}
static void bind_fence_release(struct dma_fence *fence)
{
struct kgsl_sharedmem_bind_fence *bind_fence = container_of(fence,
struct kgsl_sharedmem_bind_fence, base);
kgsl_sharedmem_put_bind_op(bind_fence->op);
kfree(bind_fence);
}
static void
kgsl_sharedmem_bind_fence_callback(struct kgsl_sharedmem_bind_op *op)
{
struct kgsl_sharedmem_bind_fence *bind_fence = op->data;
dma_fence_signal(&bind_fence->base);
dma_fence_put(&bind_fence->base);
}
static const struct dma_fence_ops kgsl_sharedmem_bind_fence_ops = {
.get_driver_name = bind_fence_get_driver_name,
.get_timeline_name = bind_fence_get_timeline_name,
.release = bind_fence_release,
};
static struct kgsl_sharedmem_bind_fence *
kgsl_sharedmem_bind_fence(struct kgsl_sharedmem_bind_op *op)
{
struct kgsl_sharedmem_bind_fence *fence;
struct sync_file *sync_file;
int fd;
fence = kzalloc(sizeof(*fence), GFP_KERNEL);
if (!fence)
return ERR_PTR(-ENOMEM);
spin_lock_init(&fence->lock);
dma_fence_init(&fence->base, &kgsl_sharedmem_bind_fence_ops,
&fence->lock, dma_fence_context_alloc(1), 0);
fd = get_unused_fd_flags(O_CLOEXEC);
if (fd < 0) {
kfree(fence);
return ERR_PTR(fd);
}
sync_file = sync_file_create(&fence->base);
if (!sync_file) {
put_unused_fd(fd);
kfree(fence);
return ERR_PTR(-ENOMEM);
}
fd_install(fd, sync_file->file);
fence->fd = fd;
fence->op = op;
return fence;
}
long kgsl_ioctl_gpumem_bind_ranges(struct kgsl_device_private *dev_priv,
unsigned int cmd, void *data)
{
struct kgsl_process_private *private = dev_priv->process_priv;
struct kgsl_gpumem_bind_ranges *param = data;
struct kgsl_sharedmem_bind_op *op;
int ret;
/* If ranges_size isn't set, return the expected size to the user */
if (!param->ranges_size) {
param->ranges_size = sizeof(struct kgsl_gpumem_bind_range);
return 0;
}
/* FENCE_OUT only makes sense with ASYNC */
if ((param->flags & KGSL_GPUMEM_BIND_FENCE_OUT) &&
!(param->flags & KGSL_GPUMEM_BIND_ASYNC))
return -EINVAL;
op = kgsl_sharedmem_create_bind_op(private, param->id,
u64_to_user_ptr(param->ranges), param->ranges_nents,
param->ranges_size);
if (IS_ERR(op))
return PTR_ERR(op);
if (param->flags & KGSL_GPUMEM_BIND_ASYNC) {
struct kgsl_sharedmem_bind_fence *fence;
if (param->flags & KGSL_GPUMEM_BIND_FENCE_OUT) {
fence = kgsl_sharedmem_bind_fence(op);
if (IS_ERR(fence)) {
kgsl_sharedmem_put_bind_op(op);
return PTR_ERR(fence);
}
op->data = fence;
op->callback = kgsl_sharedmem_bind_fence_callback;
param->fence_id = fence->fd;
}
kgsl_sharedmem_bind_ranges(op);
if (!(param->flags & KGSL_GPUMEM_BIND_FENCE_OUT))
kgsl_sharedmem_put_bind_op(op);
return 0;
}
/*
* Schedule the work. All the resources will be released after
* the bind operation is done
*/
kgsl_sharedmem_bind_ranges(op);
ret = wait_for_completion_interruptible(&op->comp);
kgsl_sharedmem_put_bind_op(op);
return ret;
}