mirror of
git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
synced 2025-08-05 16:54:27 +00:00

Reported-by: kernel test robot <lkp@intel.com> Closes: https://lore.kernel.org/oe-kbuild-all/202507032334.9SCwc952-lkp@intel.com/ Signed-off-by: Rob Clark <robin.clark@oss.qualcomm.com>
1531 lines
36 KiB
C
1531 lines
36 KiB
C
// SPDX-License-Identifier: GPL-2.0-only
|
|
/*
|
|
* Copyright (C) 2016 Red Hat
|
|
* Author: Rob Clark <robdclark@gmail.com>
|
|
*/
|
|
|
|
#include "drm/drm_file.h"
|
|
#include "drm/msm_drm.h"
|
|
#include "linux/file.h"
|
|
#include "linux/sync_file.h"
|
|
|
|
#include "msm_drv.h"
|
|
#include "msm_gem.h"
|
|
#include "msm_gpu.h"
|
|
#include "msm_mmu.h"
|
|
#include "msm_syncobj.h"
|
|
|
|
#define vm_dbg(fmt, ...) pr_debug("%s:%d: "fmt"\n", __func__, __LINE__, ##__VA_ARGS__)
|
|
|
|
static uint vm_log_shift = 0;
|
|
MODULE_PARM_DESC(vm_log_shift, "Length of VM op log");
|
|
module_param_named(vm_log_shift, vm_log_shift, uint, 0600);
|
|
|
|
/**
|
|
* struct msm_vm_map_op - create new pgtable mapping
|
|
*/
|
|
struct msm_vm_map_op {
|
|
/** @iova: start address for mapping */
|
|
uint64_t iova;
|
|
/** @range: size of the region to map */
|
|
uint64_t range;
|
|
/** @offset: offset into @sgt to map */
|
|
uint64_t offset;
|
|
/** @sgt: pages to map, or NULL for a PRR mapping */
|
|
struct sg_table *sgt;
|
|
/** @prot: the mapping protection flags */
|
|
int prot;
|
|
|
|
/**
|
|
* @queue_id: The id of the submitqueue the operation is performed
|
|
* on, or zero for (in particular) UNMAP ops triggered outside of
|
|
* a submitqueue (ie. process cleanup)
|
|
*/
|
|
int queue_id;
|
|
};
|
|
|
|
/**
|
|
* struct msm_vm_unmap_op - unmap a range of pages from pgtable
|
|
*/
|
|
struct msm_vm_unmap_op {
|
|
/** @iova: start address for unmap */
|
|
uint64_t iova;
|
|
/** @range: size of region to unmap */
|
|
uint64_t range;
|
|
|
|
/** @reason: The reason for the unmap */
|
|
const char *reason;
|
|
|
|
/**
|
|
* @queue_id: The id of the submitqueue the operation is performed
|
|
* on, or zero for (in particular) UNMAP ops triggered outside of
|
|
* a submitqueue (ie. process cleanup)
|
|
*/
|
|
int queue_id;
|
|
};
|
|
|
|
/**
|
|
* struct msm_vma_op - A MAP or UNMAP operation
|
|
*/
|
|
struct msm_vm_op {
|
|
/** @op: The operation type */
|
|
enum {
|
|
MSM_VM_OP_MAP = 1,
|
|
MSM_VM_OP_UNMAP,
|
|
} op;
|
|
union {
|
|
/** @map: Parameters used if op == MSM_VMA_OP_MAP */
|
|
struct msm_vm_map_op map;
|
|
/** @unmap: Parameters used if op == MSM_VMA_OP_UNMAP */
|
|
struct msm_vm_unmap_op unmap;
|
|
};
|
|
/** @node: list head in msm_vm_bind_job::vm_ops */
|
|
struct list_head node;
|
|
|
|
/**
|
|
* @obj: backing object for pages to be mapped/unmapped
|
|
*
|
|
* Async unmap ops, in particular, must hold a reference to the
|
|
* original GEM object backing the mapping that will be unmapped.
|
|
* But the same can be required in the map path, for example if
|
|
* there is not a corresponding unmap op, such as process exit.
|
|
*
|
|
* This ensures that the pages backing the mapping are not freed
|
|
* before the mapping is torn down.
|
|
*/
|
|
struct drm_gem_object *obj;
|
|
};
|
|
|
|
/**
|
|
* struct msm_vm_bind_job - Tracking for a VM_BIND ioctl
|
|
*
|
|
* A table of userspace requested VM updates (MSM_VM_BIND_OP_UNMAP/MAP/MAP_NULL)
|
|
* gets applied to the vm, generating a list of VM ops (MSM_VM_OP_MAP/UNMAP)
|
|
* which are applied to the pgtables asynchronously. For example a userspace
|
|
* requested MSM_VM_BIND_OP_MAP could end up generating both an MSM_VM_OP_UNMAP
|
|
* to unmap an existing mapping, and a MSM_VM_OP_MAP to apply the new mapping.
|
|
*/
|
|
struct msm_vm_bind_job {
|
|
/** @base: base class for drm_sched jobs */
|
|
struct drm_sched_job base;
|
|
/** @vm: The VM being operated on */
|
|
struct drm_gpuvm *vm;
|
|
/** @fence: The fence that is signaled when job completes */
|
|
struct dma_fence *fence;
|
|
/** @queue: The queue that the job runs on */
|
|
struct msm_gpu_submitqueue *queue;
|
|
/** @prealloc: Tracking for pre-allocated MMU pgtable pages */
|
|
struct msm_mmu_prealloc prealloc;
|
|
/** @vm_ops: a list of struct msm_vm_op */
|
|
struct list_head vm_ops;
|
|
/** @bos_pinned: are the GEM objects being bound pinned? */
|
|
bool bos_pinned;
|
|
/** @nr_ops: the number of userspace requested ops */
|
|
unsigned int nr_ops;
|
|
/**
|
|
* @ops: the userspace requested ops
|
|
*
|
|
* The userspace requested ops are copied/parsed and validated
|
|
* before we start applying the updates to try to do as much up-
|
|
* front error checking as possible, to avoid the VM being in an
|
|
* undefined state due to partially executed VM_BIND.
|
|
*
|
|
* This table also serves to hold a reference to the backing GEM
|
|
* objects.
|
|
*/
|
|
struct msm_vm_bind_op {
|
|
uint32_t op;
|
|
uint32_t flags;
|
|
union {
|
|
struct drm_gem_object *obj;
|
|
uint32_t handle;
|
|
};
|
|
uint64_t obj_offset;
|
|
uint64_t iova;
|
|
uint64_t range;
|
|
} ops[];
|
|
};
|
|
|
|
#define job_foreach_bo(obj, _job) \
|
|
for (unsigned i = 0; i < (_job)->nr_ops; i++) \
|
|
if ((obj = (_job)->ops[i].obj))
|
|
|
|
static inline struct msm_vm_bind_job *to_msm_vm_bind_job(struct drm_sched_job *job)
|
|
{
|
|
return container_of(job, struct msm_vm_bind_job, base);
|
|
}
|
|
|
|
static void
|
|
msm_gem_vm_free(struct drm_gpuvm *gpuvm)
|
|
{
|
|
struct msm_gem_vm *vm = container_of(gpuvm, struct msm_gem_vm, base);
|
|
|
|
drm_mm_takedown(&vm->mm);
|
|
if (vm->mmu)
|
|
vm->mmu->funcs->destroy(vm->mmu);
|
|
dma_fence_put(vm->last_fence);
|
|
put_pid(vm->pid);
|
|
kfree(vm->log);
|
|
kfree(vm);
|
|
}
|
|
|
|
/**
|
|
* msm_gem_vm_unusable() - Mark a VM as unusable
|
|
* @gpuvm: the VM to mark unusable
|
|
*/
|
|
void
|
|
msm_gem_vm_unusable(struct drm_gpuvm *gpuvm)
|
|
{
|
|
struct msm_gem_vm *vm = to_msm_vm(gpuvm);
|
|
uint32_t vm_log_len = (1 << vm->log_shift);
|
|
uint32_t vm_log_mask = vm_log_len - 1;
|
|
uint32_t nr_vm_logs;
|
|
int first;
|
|
|
|
vm->unusable = true;
|
|
|
|
/* Bail if no log, or empty log: */
|
|
if (!vm->log || !vm->log[0].op)
|
|
return;
|
|
|
|
mutex_lock(&vm->mmu_lock);
|
|
|
|
/*
|
|
* log_idx is the next entry to overwrite, meaning it is the oldest, or
|
|
* first, entry (other than the special case handled below where the
|
|
* log hasn't wrapped around yet)
|
|
*/
|
|
first = vm->log_idx;
|
|
|
|
if (!vm->log[first].op) {
|
|
/*
|
|
* If the next log entry has not been written yet, then only
|
|
* entries 0 to idx-1 are valid (ie. we haven't wrapped around
|
|
* yet)
|
|
*/
|
|
nr_vm_logs = MAX(0, first - 1);
|
|
first = 0;
|
|
} else {
|
|
nr_vm_logs = vm_log_len;
|
|
}
|
|
|
|
pr_err("vm-log:\n");
|
|
for (int i = 0; i < nr_vm_logs; i++) {
|
|
int idx = (i + first) & vm_log_mask;
|
|
struct msm_gem_vm_log_entry *e = &vm->log[idx];
|
|
pr_err(" - %s:%d: 0x%016llx-0x%016llx\n",
|
|
e->op, e->queue_id, e->iova,
|
|
e->iova + e->range);
|
|
}
|
|
|
|
mutex_unlock(&vm->mmu_lock);
|
|
}
|
|
|
|
static void
|
|
vm_log(struct msm_gem_vm *vm, const char *op, uint64_t iova, uint64_t range, int queue_id)
|
|
{
|
|
int idx;
|
|
|
|
if (!vm->managed)
|
|
lockdep_assert_held(&vm->mmu_lock);
|
|
|
|
vm_dbg("%s:%p:%d: %016llx %016llx", op, vm, queue_id, iova, iova + range);
|
|
|
|
if (!vm->log)
|
|
return;
|
|
|
|
idx = vm->log_idx;
|
|
vm->log[idx].op = op;
|
|
vm->log[idx].iova = iova;
|
|
vm->log[idx].range = range;
|
|
vm->log[idx].queue_id = queue_id;
|
|
vm->log_idx = (vm->log_idx + 1) & ((1 << vm->log_shift) - 1);
|
|
}
|
|
|
|
static void
|
|
vm_unmap_op(struct msm_gem_vm *vm, const struct msm_vm_unmap_op *op)
|
|
{
|
|
const char *reason = op->reason;
|
|
|
|
if (!reason)
|
|
reason = "unmap";
|
|
|
|
vm_log(vm, reason, op->iova, op->range, op->queue_id);
|
|
|
|
vm->mmu->funcs->unmap(vm->mmu, op->iova, op->range);
|
|
}
|
|
|
|
static int
|
|
vm_map_op(struct msm_gem_vm *vm, const struct msm_vm_map_op *op)
|
|
{
|
|
vm_log(vm, "map", op->iova, op->range, op->queue_id);
|
|
|
|
return vm->mmu->funcs->map(vm->mmu, op->iova, op->sgt, op->offset,
|
|
op->range, op->prot);
|
|
}
|
|
|
|
/* Actually unmap memory for the vma */
|
|
void msm_gem_vma_unmap(struct drm_gpuva *vma, const char *reason)
|
|
{
|
|
struct msm_gem_vm *vm = to_msm_vm(vma->vm);
|
|
struct msm_gem_vma *msm_vma = to_msm_vma(vma);
|
|
|
|
/* Don't do anything if the memory isn't mapped */
|
|
if (!msm_vma->mapped)
|
|
return;
|
|
|
|
/*
|
|
* The mmu_lock is only needed when preallocation is used. But
|
|
* in that case we don't need to worry about recursion into
|
|
* shrinker
|
|
*/
|
|
if (!vm->managed)
|
|
mutex_lock(&vm->mmu_lock);
|
|
|
|
vm_unmap_op(vm, &(struct msm_vm_unmap_op){
|
|
.iova = vma->va.addr,
|
|
.range = vma->va.range,
|
|
.reason = reason,
|
|
});
|
|
|
|
if (!vm->managed)
|
|
mutex_unlock(&vm->mmu_lock);
|
|
|
|
msm_vma->mapped = false;
|
|
}
|
|
|
|
/* Map and pin vma: */
|
|
int
|
|
msm_gem_vma_map(struct drm_gpuva *vma, int prot, struct sg_table *sgt)
|
|
{
|
|
struct msm_gem_vm *vm = to_msm_vm(vma->vm);
|
|
struct msm_gem_vma *msm_vma = to_msm_vma(vma);
|
|
int ret;
|
|
|
|
if (GEM_WARN_ON(!vma->va.addr))
|
|
return -EINVAL;
|
|
|
|
if (msm_vma->mapped)
|
|
return 0;
|
|
|
|
msm_vma->mapped = true;
|
|
|
|
/*
|
|
* The mmu_lock is only needed when preallocation is used. But
|
|
* in that case we don't need to worry about recursion into
|
|
* shrinker
|
|
*/
|
|
if (!vm->managed)
|
|
mutex_lock(&vm->mmu_lock);
|
|
|
|
/*
|
|
* NOTE: iommu/io-pgtable can allocate pages, so we cannot hold
|
|
* a lock across map/unmap which is also used in the job_run()
|
|
* path, as this can cause deadlock in job_run() vs shrinker/
|
|
* reclaim.
|
|
*
|
|
* Revisit this if we can come up with a scheme to pre-alloc pages
|
|
* for the pgtable in map/unmap ops.
|
|
*/
|
|
ret = vm_map_op(vm, &(struct msm_vm_map_op){
|
|
.iova = vma->va.addr,
|
|
.range = vma->va.range,
|
|
.offset = vma->gem.offset,
|
|
.sgt = sgt,
|
|
.prot = prot,
|
|
});
|
|
|
|
if (!vm->managed)
|
|
mutex_unlock(&vm->mmu_lock);
|
|
|
|
if (ret)
|
|
msm_vma->mapped = false;
|
|
|
|
return ret;
|
|
}
|
|
|
|
/* Close an iova. Warn if it is still in use */
|
|
void msm_gem_vma_close(struct drm_gpuva *vma)
|
|
{
|
|
struct msm_gem_vm *vm = to_msm_vm(vma->vm);
|
|
struct msm_gem_vma *msm_vma = to_msm_vma(vma);
|
|
|
|
GEM_WARN_ON(msm_vma->mapped);
|
|
|
|
drm_gpuvm_resv_assert_held(&vm->base);
|
|
|
|
if (vma->gem.obj)
|
|
msm_gem_assert_locked(vma->gem.obj);
|
|
|
|
if (vma->va.addr && vm->managed)
|
|
drm_mm_remove_node(&msm_vma->node);
|
|
|
|
drm_gpuva_remove(vma);
|
|
drm_gpuva_unlink(vma);
|
|
|
|
kfree(vma);
|
|
}
|
|
|
|
/* Create a new vma and allocate an iova for it */
|
|
struct drm_gpuva *
|
|
msm_gem_vma_new(struct drm_gpuvm *gpuvm, struct drm_gem_object *obj,
|
|
u64 offset, u64 range_start, u64 range_end)
|
|
{
|
|
struct msm_gem_vm *vm = to_msm_vm(gpuvm);
|
|
struct drm_gpuvm_bo *vm_bo;
|
|
struct msm_gem_vma *vma;
|
|
int ret;
|
|
|
|
drm_gpuvm_resv_assert_held(&vm->base);
|
|
|
|
vma = kzalloc(sizeof(*vma), GFP_KERNEL);
|
|
if (!vma)
|
|
return ERR_PTR(-ENOMEM);
|
|
|
|
if (vm->managed) {
|
|
BUG_ON(offset != 0);
|
|
BUG_ON(!obj); /* NULL mappings not valid for kernel managed VM */
|
|
ret = drm_mm_insert_node_in_range(&vm->mm, &vma->node,
|
|
obj->size, PAGE_SIZE, 0,
|
|
range_start, range_end, 0);
|
|
|
|
if (ret)
|
|
goto err_free_vma;
|
|
|
|
range_start = vma->node.start;
|
|
range_end = range_start + obj->size;
|
|
}
|
|
|
|
if (obj)
|
|
GEM_WARN_ON((range_end - range_start) > obj->size);
|
|
|
|
drm_gpuva_init(&vma->base, range_start, range_end - range_start, obj, offset);
|
|
vma->mapped = false;
|
|
|
|
ret = drm_gpuva_insert(&vm->base, &vma->base);
|
|
if (ret)
|
|
goto err_free_range;
|
|
|
|
if (!obj)
|
|
return &vma->base;
|
|
|
|
vm_bo = drm_gpuvm_bo_obtain(&vm->base, obj);
|
|
if (IS_ERR(vm_bo)) {
|
|
ret = PTR_ERR(vm_bo);
|
|
goto err_va_remove;
|
|
}
|
|
|
|
drm_gpuvm_bo_extobj_add(vm_bo);
|
|
drm_gpuva_link(&vma->base, vm_bo);
|
|
GEM_WARN_ON(drm_gpuvm_bo_put(vm_bo));
|
|
|
|
return &vma->base;
|
|
|
|
err_va_remove:
|
|
drm_gpuva_remove(&vma->base);
|
|
err_free_range:
|
|
if (vm->managed)
|
|
drm_mm_remove_node(&vma->node);
|
|
err_free_vma:
|
|
kfree(vma);
|
|
return ERR_PTR(ret);
|
|
}
|
|
|
|
static int
|
|
msm_gem_vm_bo_validate(struct drm_gpuvm_bo *vm_bo, struct drm_exec *exec)
|
|
{
|
|
struct drm_gem_object *obj = vm_bo->obj;
|
|
struct drm_gpuva *vma;
|
|
int ret;
|
|
|
|
vm_dbg("validate: %p", obj);
|
|
|
|
msm_gem_assert_locked(obj);
|
|
|
|
drm_gpuvm_bo_for_each_va (vma, vm_bo) {
|
|
ret = msm_gem_pin_vma_locked(obj, vma);
|
|
if (ret)
|
|
return ret;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
struct op_arg {
|
|
unsigned flags;
|
|
struct msm_vm_bind_job *job;
|
|
};
|
|
|
|
static void
|
|
vm_op_enqueue(struct op_arg *arg, struct msm_vm_op _op)
|
|
{
|
|
struct msm_vm_op *op = kmalloc(sizeof(*op), GFP_KERNEL);
|
|
*op = _op;
|
|
list_add_tail(&op->node, &arg->job->vm_ops);
|
|
|
|
if (op->obj)
|
|
drm_gem_object_get(op->obj);
|
|
}
|
|
|
|
static struct drm_gpuva *
|
|
vma_from_op(struct op_arg *arg, struct drm_gpuva_op_map *op)
|
|
{
|
|
return msm_gem_vma_new(arg->job->vm, op->gem.obj, op->gem.offset,
|
|
op->va.addr, op->va.addr + op->va.range);
|
|
}
|
|
|
|
static int
|
|
msm_gem_vm_sm_step_map(struct drm_gpuva_op *op, void *arg)
|
|
{
|
|
struct msm_vm_bind_job *job = ((struct op_arg *)arg)->job;
|
|
struct drm_gem_object *obj = op->map.gem.obj;
|
|
struct drm_gpuva *vma;
|
|
struct sg_table *sgt;
|
|
unsigned prot;
|
|
|
|
vma = vma_from_op(arg, &op->map);
|
|
if (WARN_ON(IS_ERR(vma)))
|
|
return PTR_ERR(vma);
|
|
|
|
vm_dbg("%p:%p:%p: %016llx %016llx", vma->vm, vma, vma->gem.obj,
|
|
vma->va.addr, vma->va.range);
|
|
|
|
vma->flags = ((struct op_arg *)arg)->flags;
|
|
|
|
if (obj) {
|
|
sgt = to_msm_bo(obj)->sgt;
|
|
prot = msm_gem_prot(obj);
|
|
} else {
|
|
sgt = NULL;
|
|
prot = IOMMU_READ | IOMMU_WRITE;
|
|
}
|
|
|
|
vm_op_enqueue(arg, (struct msm_vm_op){
|
|
.op = MSM_VM_OP_MAP,
|
|
.map = {
|
|
.sgt = sgt,
|
|
.iova = vma->va.addr,
|
|
.range = vma->va.range,
|
|
.offset = vma->gem.offset,
|
|
.prot = prot,
|
|
.queue_id = job->queue->id,
|
|
},
|
|
.obj = vma->gem.obj,
|
|
});
|
|
|
|
to_msm_vma(vma)->mapped = true;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
msm_gem_vm_sm_step_remap(struct drm_gpuva_op *op, void *arg)
|
|
{
|
|
struct msm_vm_bind_job *job = ((struct op_arg *)arg)->job;
|
|
struct drm_gpuvm *vm = job->vm;
|
|
struct drm_gpuva *orig_vma = op->remap.unmap->va;
|
|
struct drm_gpuva *prev_vma = NULL, *next_vma = NULL;
|
|
struct drm_gpuvm_bo *vm_bo = orig_vma->vm_bo;
|
|
bool mapped = to_msm_vma(orig_vma)->mapped;
|
|
unsigned flags;
|
|
|
|
vm_dbg("orig_vma: %p:%p:%p: %016llx %016llx", vm, orig_vma,
|
|
orig_vma->gem.obj, orig_vma->va.addr, orig_vma->va.range);
|
|
|
|
if (mapped) {
|
|
uint64_t unmap_start, unmap_range;
|
|
|
|
drm_gpuva_op_remap_to_unmap_range(&op->remap, &unmap_start, &unmap_range);
|
|
|
|
vm_op_enqueue(arg, (struct msm_vm_op){
|
|
.op = MSM_VM_OP_UNMAP,
|
|
.unmap = {
|
|
.iova = unmap_start,
|
|
.range = unmap_range,
|
|
.queue_id = job->queue->id,
|
|
},
|
|
.obj = orig_vma->gem.obj,
|
|
});
|
|
|
|
/*
|
|
* Part of this GEM obj is still mapped, but we're going to kill the
|
|
* existing VMA and replace it with one or two new ones (ie. two if
|
|
* the unmapped range is in the middle of the existing (unmap) VMA).
|
|
* So just set the state to unmapped:
|
|
*/
|
|
to_msm_vma(orig_vma)->mapped = false;
|
|
}
|
|
|
|
/*
|
|
* Hold a ref to the vm_bo between the msm_gem_vma_close() and the
|
|
* creation of the new prev/next vma's, in case the vm_bo is tracked
|
|
* in the VM's evict list:
|
|
*/
|
|
if (vm_bo)
|
|
drm_gpuvm_bo_get(vm_bo);
|
|
|
|
/*
|
|
* The prev_vma and/or next_vma are replacing the unmapped vma, and
|
|
* therefore should preserve it's flags:
|
|
*/
|
|
flags = orig_vma->flags;
|
|
|
|
msm_gem_vma_close(orig_vma);
|
|
|
|
if (op->remap.prev) {
|
|
prev_vma = vma_from_op(arg, op->remap.prev);
|
|
if (WARN_ON(IS_ERR(prev_vma)))
|
|
return PTR_ERR(prev_vma);
|
|
|
|
vm_dbg("prev_vma: %p:%p: %016llx %016llx", vm, prev_vma, prev_vma->va.addr, prev_vma->va.range);
|
|
to_msm_vma(prev_vma)->mapped = mapped;
|
|
prev_vma->flags = flags;
|
|
}
|
|
|
|
if (op->remap.next) {
|
|
next_vma = vma_from_op(arg, op->remap.next);
|
|
if (WARN_ON(IS_ERR(next_vma)))
|
|
return PTR_ERR(next_vma);
|
|
|
|
vm_dbg("next_vma: %p:%p: %016llx %016llx", vm, next_vma, next_vma->va.addr, next_vma->va.range);
|
|
to_msm_vma(next_vma)->mapped = mapped;
|
|
next_vma->flags = flags;
|
|
}
|
|
|
|
if (!mapped)
|
|
drm_gpuvm_bo_evict(vm_bo, true);
|
|
|
|
/* Drop the previous ref: */
|
|
drm_gpuvm_bo_put(vm_bo);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
msm_gem_vm_sm_step_unmap(struct drm_gpuva_op *op, void *arg)
|
|
{
|
|
struct msm_vm_bind_job *job = ((struct op_arg *)arg)->job;
|
|
struct drm_gpuva *vma = op->unmap.va;
|
|
struct msm_gem_vma *msm_vma = to_msm_vma(vma);
|
|
|
|
vm_dbg("%p:%p:%p: %016llx %016llx", vma->vm, vma, vma->gem.obj,
|
|
vma->va.addr, vma->va.range);
|
|
|
|
if (!msm_vma->mapped)
|
|
goto out_close;
|
|
|
|
vm_op_enqueue(arg, (struct msm_vm_op){
|
|
.op = MSM_VM_OP_UNMAP,
|
|
.unmap = {
|
|
.iova = vma->va.addr,
|
|
.range = vma->va.range,
|
|
.queue_id = job->queue->id,
|
|
},
|
|
.obj = vma->gem.obj,
|
|
});
|
|
|
|
msm_vma->mapped = false;
|
|
|
|
out_close:
|
|
msm_gem_vma_close(vma);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const struct drm_gpuvm_ops msm_gpuvm_ops = {
|
|
.vm_free = msm_gem_vm_free,
|
|
.vm_bo_validate = msm_gem_vm_bo_validate,
|
|
.sm_step_map = msm_gem_vm_sm_step_map,
|
|
.sm_step_remap = msm_gem_vm_sm_step_remap,
|
|
.sm_step_unmap = msm_gem_vm_sm_step_unmap,
|
|
};
|
|
|
|
static struct dma_fence *
|
|
msm_vma_job_run(struct drm_sched_job *_job)
|
|
{
|
|
struct msm_vm_bind_job *job = to_msm_vm_bind_job(_job);
|
|
struct msm_gem_vm *vm = to_msm_vm(job->vm);
|
|
struct drm_gem_object *obj;
|
|
int ret = vm->unusable ? -EINVAL : 0;
|
|
|
|
vm_dbg("");
|
|
|
|
mutex_lock(&vm->mmu_lock);
|
|
vm->mmu->prealloc = &job->prealloc;
|
|
|
|
while (!list_empty(&job->vm_ops)) {
|
|
struct msm_vm_op *op =
|
|
list_first_entry(&job->vm_ops, struct msm_vm_op, node);
|
|
|
|
switch (op->op) {
|
|
case MSM_VM_OP_MAP:
|
|
/*
|
|
* On error, stop trying to map new things.. but we
|
|
* still want to process the unmaps (or in particular,
|
|
* the drm_gem_object_put()s)
|
|
*/
|
|
if (!ret)
|
|
ret = vm_map_op(vm, &op->map);
|
|
break;
|
|
case MSM_VM_OP_UNMAP:
|
|
vm_unmap_op(vm, &op->unmap);
|
|
break;
|
|
}
|
|
drm_gem_object_put(op->obj);
|
|
list_del(&op->node);
|
|
kfree(op);
|
|
}
|
|
|
|
vm->mmu->prealloc = NULL;
|
|
mutex_unlock(&vm->mmu_lock);
|
|
|
|
/*
|
|
* We failed to perform at least _some_ of the pgtable updates, so
|
|
* now the VM is in an undefined state. Game over!
|
|
*/
|
|
if (ret)
|
|
msm_gem_vm_unusable(job->vm);
|
|
|
|
job_foreach_bo (obj, job) {
|
|
msm_gem_lock(obj);
|
|
msm_gem_unpin_locked(obj);
|
|
msm_gem_unlock(obj);
|
|
}
|
|
|
|
/* VM_BIND ops are synchronous, so no fence to wait on: */
|
|
return NULL;
|
|
}
|
|
|
|
static void
|
|
msm_vma_job_free(struct drm_sched_job *_job)
|
|
{
|
|
struct msm_vm_bind_job *job = to_msm_vm_bind_job(_job);
|
|
struct msm_gem_vm *vm = to_msm_vm(job->vm);
|
|
struct drm_gem_object *obj;
|
|
|
|
vm->mmu->funcs->prealloc_cleanup(vm->mmu, &job->prealloc);
|
|
|
|
atomic_sub(job->prealloc.count, &vm->prealloc_throttle.in_flight);
|
|
|
|
drm_sched_job_cleanup(_job);
|
|
|
|
job_foreach_bo (obj, job)
|
|
drm_gem_object_put(obj);
|
|
|
|
msm_submitqueue_put(job->queue);
|
|
dma_fence_put(job->fence);
|
|
|
|
/* In error paths, we could have unexecuted ops: */
|
|
while (!list_empty(&job->vm_ops)) {
|
|
struct msm_vm_op *op =
|
|
list_first_entry(&job->vm_ops, struct msm_vm_op, node);
|
|
list_del(&op->node);
|
|
kfree(op);
|
|
}
|
|
|
|
wake_up(&vm->prealloc_throttle.wait);
|
|
|
|
kfree(job);
|
|
}
|
|
|
|
static const struct drm_sched_backend_ops msm_vm_bind_ops = {
|
|
.run_job = msm_vma_job_run,
|
|
.free_job = msm_vma_job_free
|
|
};
|
|
|
|
/**
|
|
* msm_gem_vm_create() - Create and initialize a &msm_gem_vm
|
|
* @drm: the drm device
|
|
* @mmu: the backing MMU objects handling mapping/unmapping
|
|
* @name: the name of the VM
|
|
* @va_start: the start offset of the VA space
|
|
* @va_size: the size of the VA space
|
|
* @managed: is it a kernel managed VM?
|
|
*
|
|
* In a kernel managed VM, the kernel handles address allocation, and only
|
|
* synchronous operations are supported. In a user managed VM, userspace
|
|
* handles virtual address allocation, and both async and sync operations
|
|
* are supported.
|
|
*/
|
|
struct drm_gpuvm *
|
|
msm_gem_vm_create(struct drm_device *drm, struct msm_mmu *mmu, const char *name,
|
|
u64 va_start, u64 va_size, bool managed)
|
|
{
|
|
/*
|
|
* We mostly want to use DRM_GPUVM_RESV_PROTECTED, except that
|
|
* makes drm_gpuvm_bo_evict() a no-op for extobjs (ie. we loose
|
|
* tracking that an extobj is evicted) :facepalm:
|
|
*/
|
|
enum drm_gpuvm_flags flags = 0;
|
|
struct msm_gem_vm *vm;
|
|
struct drm_gem_object *dummy_gem;
|
|
int ret = 0;
|
|
|
|
if (IS_ERR(mmu))
|
|
return ERR_CAST(mmu);
|
|
|
|
vm = kzalloc(sizeof(*vm), GFP_KERNEL);
|
|
if (!vm)
|
|
return ERR_PTR(-ENOMEM);
|
|
|
|
dummy_gem = drm_gpuvm_resv_object_alloc(drm);
|
|
if (!dummy_gem) {
|
|
ret = -ENOMEM;
|
|
goto err_free_vm;
|
|
}
|
|
|
|
if (!managed) {
|
|
struct drm_sched_init_args args = {
|
|
.ops = &msm_vm_bind_ops,
|
|
.num_rqs = 1,
|
|
.credit_limit = 1,
|
|
.timeout = MAX_SCHEDULE_TIMEOUT,
|
|
.name = "msm-vm-bind",
|
|
.dev = drm->dev,
|
|
};
|
|
|
|
ret = drm_sched_init(&vm->sched, &args);
|
|
if (ret)
|
|
goto err_free_dummy;
|
|
|
|
init_waitqueue_head(&vm->prealloc_throttle.wait);
|
|
}
|
|
|
|
drm_gpuvm_init(&vm->base, name, flags, drm, dummy_gem,
|
|
va_start, va_size, 0, 0, &msm_gpuvm_ops);
|
|
drm_gem_object_put(dummy_gem);
|
|
|
|
vm->mmu = mmu;
|
|
mutex_init(&vm->mmu_lock);
|
|
vm->managed = managed;
|
|
|
|
drm_mm_init(&vm->mm, va_start, va_size);
|
|
|
|
/*
|
|
* We don't really need vm log for kernel managed VMs, as the kernel
|
|
* is responsible for ensuring that GEM objs are mapped if they are
|
|
* used by a submit. Furthermore we piggyback on mmu_lock to serialize
|
|
* access to the log.
|
|
*
|
|
* Limit the max log_shift to 8 to prevent userspace from asking us
|
|
* for an unreasonable log size.
|
|
*/
|
|
if (!managed)
|
|
vm->log_shift = MIN(vm_log_shift, 8);
|
|
|
|
if (vm->log_shift) {
|
|
vm->log = kmalloc_array(1 << vm->log_shift, sizeof(vm->log[0]),
|
|
GFP_KERNEL | __GFP_ZERO);
|
|
}
|
|
|
|
return &vm->base;
|
|
|
|
err_free_dummy:
|
|
drm_gem_object_put(dummy_gem);
|
|
|
|
err_free_vm:
|
|
kfree(vm);
|
|
return ERR_PTR(ret);
|
|
}
|
|
|
|
/**
|
|
* msm_gem_vm_close() - Close a VM
|
|
* @gpuvm: The VM to close
|
|
*
|
|
* Called when the drm device file is closed, to tear down VM related resources
|
|
* (which will drop refcounts to GEM objects that were still mapped into the
|
|
* VM at the time).
|
|
*/
|
|
void
|
|
msm_gem_vm_close(struct drm_gpuvm *gpuvm)
|
|
{
|
|
struct msm_gem_vm *vm = to_msm_vm(gpuvm);
|
|
struct drm_gpuva *vma, *tmp;
|
|
struct drm_exec exec;
|
|
|
|
/*
|
|
* For kernel managed VMs, the VMAs are torn down when the handle is
|
|
* closed, so nothing more to do.
|
|
*/
|
|
if (vm->managed)
|
|
return;
|
|
|
|
if (vm->last_fence)
|
|
dma_fence_wait(vm->last_fence, false);
|
|
|
|
/* Kill the scheduler now, so we aren't racing with it for cleanup: */
|
|
drm_sched_stop(&vm->sched, NULL);
|
|
drm_sched_fini(&vm->sched);
|
|
|
|
/* Tear down any remaining mappings: */
|
|
drm_exec_init(&exec, 0, 2);
|
|
drm_exec_until_all_locked (&exec) {
|
|
drm_exec_lock_obj(&exec, drm_gpuvm_resv_obj(gpuvm));
|
|
drm_exec_retry_on_contention(&exec);
|
|
|
|
drm_gpuvm_for_each_va_safe (vma, tmp, gpuvm) {
|
|
struct drm_gem_object *obj = vma->gem.obj;
|
|
|
|
/*
|
|
* MSM_BO_NO_SHARE objects share the same resv as the
|
|
* VM, in which case the obj is already locked:
|
|
*/
|
|
if (obj && (obj->resv == drm_gpuvm_resv(gpuvm)))
|
|
obj = NULL;
|
|
|
|
if (obj) {
|
|
drm_exec_lock_obj(&exec, obj);
|
|
drm_exec_retry_on_contention(&exec);
|
|
}
|
|
|
|
msm_gem_vma_unmap(vma, "close");
|
|
msm_gem_vma_close(vma);
|
|
|
|
if (obj) {
|
|
drm_exec_unlock_obj(&exec, obj);
|
|
}
|
|
}
|
|
}
|
|
drm_exec_fini(&exec);
|
|
}
|
|
|
|
|
|
static struct msm_vm_bind_job *
|
|
vm_bind_job_create(struct drm_device *dev, struct drm_file *file,
|
|
struct msm_gpu_submitqueue *queue, uint32_t nr_ops)
|
|
{
|
|
struct msm_vm_bind_job *job;
|
|
uint64_t sz;
|
|
int ret;
|
|
|
|
sz = struct_size(job, ops, nr_ops);
|
|
|
|
if (sz > SIZE_MAX)
|
|
return ERR_PTR(-ENOMEM);
|
|
|
|
job = kzalloc(sz, GFP_KERNEL | __GFP_NOWARN);
|
|
if (!job)
|
|
return ERR_PTR(-ENOMEM);
|
|
|
|
ret = drm_sched_job_init(&job->base, queue->entity, 1, queue,
|
|
file->client_id);
|
|
if (ret) {
|
|
kfree(job);
|
|
return ERR_PTR(ret);
|
|
}
|
|
|
|
job->vm = msm_context_vm(dev, queue->ctx);
|
|
job->queue = queue;
|
|
INIT_LIST_HEAD(&job->vm_ops);
|
|
|
|
return job;
|
|
}
|
|
|
|
static bool invalid_alignment(uint64_t addr)
|
|
{
|
|
/*
|
|
* Technically this is about GPU alignment, not CPU alignment. But
|
|
* I've not seen any qcom SoC where the SMMU does not support the
|
|
* CPU's smallest page size.
|
|
*/
|
|
return !PAGE_ALIGNED(addr);
|
|
}
|
|
|
|
static int
|
|
lookup_op(struct msm_vm_bind_job *job, const struct drm_msm_vm_bind_op *op)
|
|
{
|
|
struct drm_device *dev = job->vm->drm;
|
|
int i = job->nr_ops++;
|
|
int ret = 0;
|
|
|
|
job->ops[i].op = op->op;
|
|
job->ops[i].handle = op->handle;
|
|
job->ops[i].obj_offset = op->obj_offset;
|
|
job->ops[i].iova = op->iova;
|
|
job->ops[i].range = op->range;
|
|
job->ops[i].flags = op->flags;
|
|
|
|
if (op->flags & ~MSM_VM_BIND_OP_FLAGS)
|
|
ret = UERR(EINVAL, dev, "invalid flags: %x\n", op->flags);
|
|
|
|
if (invalid_alignment(op->iova))
|
|
ret = UERR(EINVAL, dev, "invalid address: %016llx\n", op->iova);
|
|
|
|
if (invalid_alignment(op->obj_offset))
|
|
ret = UERR(EINVAL, dev, "invalid bo_offset: %016llx\n", op->obj_offset);
|
|
|
|
if (invalid_alignment(op->range))
|
|
ret = UERR(EINVAL, dev, "invalid range: %016llx\n", op->range);
|
|
|
|
if (!drm_gpuvm_range_valid(job->vm, op->iova, op->range))
|
|
ret = UERR(EINVAL, dev, "invalid range: %016llx, %016llx\n", op->iova, op->range);
|
|
|
|
/*
|
|
* MAP must specify a valid handle. But the handle MBZ for
|
|
* UNMAP or MAP_NULL.
|
|
*/
|
|
if (op->op == MSM_VM_BIND_OP_MAP) {
|
|
if (!op->handle)
|
|
ret = UERR(EINVAL, dev, "invalid handle\n");
|
|
} else if (op->handle) {
|
|
ret = UERR(EINVAL, dev, "handle must be zero\n");
|
|
}
|
|
|
|
switch (op->op) {
|
|
case MSM_VM_BIND_OP_MAP:
|
|
case MSM_VM_BIND_OP_MAP_NULL:
|
|
case MSM_VM_BIND_OP_UNMAP:
|
|
break;
|
|
default:
|
|
ret = UERR(EINVAL, dev, "invalid op: %u\n", op->op);
|
|
break;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* ioctl parsing, parameter validation, and GEM handle lookup
|
|
*/
|
|
static int
|
|
vm_bind_job_lookup_ops(struct msm_vm_bind_job *job, struct drm_msm_vm_bind *args,
|
|
struct drm_file *file, int *nr_bos)
|
|
{
|
|
struct drm_device *dev = job->vm->drm;
|
|
int ret = 0;
|
|
int cnt = 0;
|
|
|
|
if (args->nr_ops == 1) {
|
|
/* Single op case, the op is inlined: */
|
|
ret = lookup_op(job, &args->op);
|
|
} else {
|
|
for (unsigned i = 0; i < args->nr_ops; i++) {
|
|
struct drm_msm_vm_bind_op op;
|
|
void __user *userptr =
|
|
u64_to_user_ptr(args->ops + (i * sizeof(op)));
|
|
|
|
/* make sure we don't have garbage flags, in case we hit
|
|
* error path before flags is initialized:
|
|
*/
|
|
job->ops[i].flags = 0;
|
|
|
|
if (copy_from_user(&op, userptr, sizeof(op))) {
|
|
ret = -EFAULT;
|
|
break;
|
|
}
|
|
|
|
ret = lookup_op(job, &op);
|
|
if (ret)
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (ret) {
|
|
job->nr_ops = 0;
|
|
goto out;
|
|
}
|
|
|
|
spin_lock(&file->table_lock);
|
|
|
|
for (unsigned i = 0; i < args->nr_ops; i++) {
|
|
struct drm_gem_object *obj;
|
|
|
|
if (!job->ops[i].handle) {
|
|
job->ops[i].obj = NULL;
|
|
continue;
|
|
}
|
|
|
|
/*
|
|
* normally use drm_gem_object_lookup(), but for bulk lookup
|
|
* all under single table_lock just hit object_idr directly:
|
|
*/
|
|
obj = idr_find(&file->object_idr, job->ops[i].handle);
|
|
if (!obj) {
|
|
ret = UERR(EINVAL, dev, "invalid handle %u at index %u\n", job->ops[i].handle, i);
|
|
goto out_unlock;
|
|
}
|
|
|
|
drm_gem_object_get(obj);
|
|
|
|
job->ops[i].obj = obj;
|
|
cnt++;
|
|
}
|
|
|
|
*nr_bos = cnt;
|
|
|
|
out_unlock:
|
|
spin_unlock(&file->table_lock);
|
|
|
|
out:
|
|
return ret;
|
|
}
|
|
|
|
static void
|
|
prealloc_count(struct msm_vm_bind_job *job,
|
|
struct msm_vm_bind_op *first,
|
|
struct msm_vm_bind_op *last)
|
|
{
|
|
struct msm_mmu *mmu = to_msm_vm(job->vm)->mmu;
|
|
|
|
if (!first)
|
|
return;
|
|
|
|
uint64_t start_iova = first->iova;
|
|
uint64_t end_iova = last->iova + last->range;
|
|
|
|
mmu->funcs->prealloc_count(mmu, &job->prealloc, start_iova, end_iova - start_iova);
|
|
}
|
|
|
|
static bool
|
|
ops_are_same_pte(struct msm_vm_bind_op *first, struct msm_vm_bind_op *next)
|
|
{
|
|
/*
|
|
* Last level pte covers 2MB.. so we should merge two ops, from
|
|
* the PoV of figuring out how much pgtable pages to pre-allocate
|
|
* if they land in the same 2MB range:
|
|
*/
|
|
uint64_t pte_mask = ~(SZ_2M - 1);
|
|
return ((first->iova + first->range) & pte_mask) == (next->iova & pte_mask);
|
|
}
|
|
|
|
/*
|
|
* Determine the amount of memory to prealloc for pgtables. For sparse images,
|
|
* in particular, userspace plays some tricks with the order of page mappings
|
|
* to get the desired swizzle pattern, resulting in a large # of tiny MAP ops.
|
|
* So detect when multiple MAP operations are physically contiguous, and count
|
|
* them as a single mapping. Otherwise the prealloc_count() will not realize
|
|
* they can share pagetable pages and vastly overcount.
|
|
*/
|
|
static int
|
|
vm_bind_prealloc_count(struct msm_vm_bind_job *job)
|
|
{
|
|
struct msm_vm_bind_op *first = NULL, *last = NULL;
|
|
struct msm_gem_vm *vm = to_msm_vm(job->vm);
|
|
int ret;
|
|
|
|
for (int i = 0; i < job->nr_ops; i++) {
|
|
struct msm_vm_bind_op *op = &job->ops[i];
|
|
|
|
/* We only care about MAP/MAP_NULL: */
|
|
if (op->op == MSM_VM_BIND_OP_UNMAP)
|
|
continue;
|
|
|
|
/*
|
|
* If op is contiguous with last in the current range, then
|
|
* it becomes the new last in the range and we continue
|
|
* looping:
|
|
*/
|
|
if (last && ops_are_same_pte(last, op)) {
|
|
last = op;
|
|
continue;
|
|
}
|
|
|
|
/*
|
|
* If op is not contiguous with the current range, flush
|
|
* the current range and start anew:
|
|
*/
|
|
prealloc_count(job, first, last);
|
|
first = last = op;
|
|
}
|
|
|
|
/* Flush the remaining range: */
|
|
prealloc_count(job, first, last);
|
|
|
|
/*
|
|
* Now that we know the needed amount to pre-alloc, throttle on pending
|
|
* VM_BIND jobs if we already have too much pre-alloc memory in flight
|
|
*/
|
|
ret = wait_event_interruptible(
|
|
vm->prealloc_throttle.wait,
|
|
atomic_read(&vm->prealloc_throttle.in_flight) <= 1024);
|
|
if (ret)
|
|
return ret;
|
|
|
|
atomic_add(job->prealloc.count, &vm->prealloc_throttle.in_flight);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Lock VM and GEM objects
|
|
*/
|
|
static int
|
|
vm_bind_job_lock_objects(struct msm_vm_bind_job *job, struct drm_exec *exec)
|
|
{
|
|
int ret;
|
|
|
|
/* Lock VM and objects: */
|
|
drm_exec_until_all_locked (exec) {
|
|
ret = drm_exec_lock_obj(exec, drm_gpuvm_resv_obj(job->vm));
|
|
drm_exec_retry_on_contention(exec);
|
|
if (ret)
|
|
return ret;
|
|
|
|
for (unsigned i = 0; i < job->nr_ops; i++) {
|
|
const struct msm_vm_bind_op *op = &job->ops[i];
|
|
|
|
switch (op->op) {
|
|
case MSM_VM_BIND_OP_UNMAP:
|
|
ret = drm_gpuvm_sm_unmap_exec_lock(job->vm, exec,
|
|
op->iova,
|
|
op->obj_offset);
|
|
break;
|
|
case MSM_VM_BIND_OP_MAP:
|
|
case MSM_VM_BIND_OP_MAP_NULL:
|
|
ret = drm_gpuvm_sm_map_exec_lock(job->vm, exec, 1,
|
|
op->iova, op->range,
|
|
op->obj, op->obj_offset);
|
|
break;
|
|
default:
|
|
/*
|
|
* lookup_op() should have already thrown an error for
|
|
* invalid ops
|
|
*/
|
|
WARN_ON("unreachable");
|
|
}
|
|
|
|
drm_exec_retry_on_contention(exec);
|
|
if (ret)
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Pin GEM objects, ensuring that we have backing pages. Pinning will move
|
|
* the object to the pinned LRU so that the shrinker knows to first consider
|
|
* other objects for evicting.
|
|
*/
|
|
static int
|
|
vm_bind_job_pin_objects(struct msm_vm_bind_job *job)
|
|
{
|
|
struct drm_gem_object *obj;
|
|
|
|
/*
|
|
* First loop, before holding the LRU lock, avoids holding the
|
|
* LRU lock while calling msm_gem_pin_vma_locked (which could
|
|
* trigger get_pages())
|
|
*/
|
|
job_foreach_bo (obj, job) {
|
|
struct page **pages;
|
|
|
|
pages = msm_gem_get_pages_locked(obj, MSM_MADV_WILLNEED);
|
|
if (IS_ERR(pages))
|
|
return PTR_ERR(pages);
|
|
}
|
|
|
|
struct msm_drm_private *priv = job->vm->drm->dev_private;
|
|
|
|
/*
|
|
* A second loop while holding the LRU lock (a) avoids acquiring/dropping
|
|
* the LRU lock for each individual bo, while (b) avoiding holding the
|
|
* LRU lock while calling msm_gem_pin_vma_locked() (which could trigger
|
|
* get_pages() which could trigger reclaim.. and if we held the LRU lock
|
|
* could trigger deadlock with the shrinker).
|
|
*/
|
|
mutex_lock(&priv->lru.lock);
|
|
job_foreach_bo (obj, job)
|
|
msm_gem_pin_obj_locked(obj);
|
|
mutex_unlock(&priv->lru.lock);
|
|
|
|
job->bos_pinned = true;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Unpin GEM objects. Normally this is done after the bind job is run.
|
|
*/
|
|
static void
|
|
vm_bind_job_unpin_objects(struct msm_vm_bind_job *job)
|
|
{
|
|
struct drm_gem_object *obj;
|
|
|
|
if (!job->bos_pinned)
|
|
return;
|
|
|
|
job_foreach_bo (obj, job)
|
|
msm_gem_unpin_locked(obj);
|
|
|
|
job->bos_pinned = false;
|
|
}
|
|
|
|
/*
|
|
* Pre-allocate pgtable memory, and translate the VM bind requests into a
|
|
* sequence of pgtable updates to be applied asynchronously.
|
|
*/
|
|
static int
|
|
vm_bind_job_prepare(struct msm_vm_bind_job *job)
|
|
{
|
|
struct msm_gem_vm *vm = to_msm_vm(job->vm);
|
|
struct msm_mmu *mmu = vm->mmu;
|
|
int ret;
|
|
|
|
ret = mmu->funcs->prealloc_allocate(mmu, &job->prealloc);
|
|
if (ret)
|
|
return ret;
|
|
|
|
for (unsigned i = 0; i < job->nr_ops; i++) {
|
|
const struct msm_vm_bind_op *op = &job->ops[i];
|
|
struct op_arg arg = {
|
|
.job = job,
|
|
};
|
|
|
|
switch (op->op) {
|
|
case MSM_VM_BIND_OP_UNMAP:
|
|
ret = drm_gpuvm_sm_unmap(job->vm, &arg, op->iova,
|
|
op->range);
|
|
break;
|
|
case MSM_VM_BIND_OP_MAP:
|
|
if (op->flags & MSM_VM_BIND_OP_DUMP)
|
|
arg.flags |= MSM_VMA_DUMP;
|
|
fallthrough;
|
|
case MSM_VM_BIND_OP_MAP_NULL:
|
|
ret = drm_gpuvm_sm_map(job->vm, &arg, op->iova,
|
|
op->range, op->obj, op->obj_offset);
|
|
break;
|
|
default:
|
|
/*
|
|
* lookup_op() should have already thrown an error for
|
|
* invalid ops
|
|
*/
|
|
BUG_ON("unreachable");
|
|
}
|
|
|
|
if (ret) {
|
|
/*
|
|
* If we've already started modifying the vm, we can't
|
|
* adequetly describe to userspace the intermediate
|
|
* state the vm is in. So throw up our hands!
|
|
*/
|
|
if (i > 0)
|
|
msm_gem_vm_unusable(job->vm);
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Attach fences to the GEM objects being bound. This will signify to
|
|
* the shrinker that they are busy even after dropping the locks (ie.
|
|
* drm_exec_fini())
|
|
*/
|
|
static void
|
|
vm_bind_job_attach_fences(struct msm_vm_bind_job *job)
|
|
{
|
|
for (unsigned i = 0; i < job->nr_ops; i++) {
|
|
struct drm_gem_object *obj = job->ops[i].obj;
|
|
|
|
if (!obj)
|
|
continue;
|
|
|
|
dma_resv_add_fence(obj->resv, job->fence,
|
|
DMA_RESV_USAGE_KERNEL);
|
|
}
|
|
}
|
|
|
|
int
|
|
msm_ioctl_vm_bind(struct drm_device *dev, void *data, struct drm_file *file)
|
|
{
|
|
struct msm_drm_private *priv = dev->dev_private;
|
|
struct drm_msm_vm_bind *args = data;
|
|
struct msm_context *ctx = file->driver_priv;
|
|
struct msm_vm_bind_job *job = NULL;
|
|
struct msm_gpu *gpu = priv->gpu;
|
|
struct msm_gpu_submitqueue *queue;
|
|
struct msm_syncobj_post_dep *post_deps = NULL;
|
|
struct drm_syncobj **syncobjs_to_reset = NULL;
|
|
struct sync_file *sync_file = NULL;
|
|
struct dma_fence *fence;
|
|
int out_fence_fd = -1;
|
|
int ret, nr_bos = 0;
|
|
unsigned i;
|
|
|
|
if (!gpu)
|
|
return -ENXIO;
|
|
|
|
/*
|
|
* Maybe we could allow just UNMAP ops? OTOH userspace should just
|
|
* immediately close the device file and all will be torn down.
|
|
*/
|
|
if (to_msm_vm(ctx->vm)->unusable)
|
|
return UERR(EPIPE, dev, "context is unusable");
|
|
|
|
/*
|
|
* Technically, you cannot create a VM_BIND submitqueue in the first
|
|
* place, if you haven't opted in to VM_BIND context. But it is
|
|
* cleaner / less confusing, to check this case directly.
|
|
*/
|
|
if (!msm_context_is_vmbind(ctx))
|
|
return UERR(EINVAL, dev, "context does not support vmbind");
|
|
|
|
if (args->flags & ~MSM_VM_BIND_FLAGS)
|
|
return UERR(EINVAL, dev, "invalid flags");
|
|
|
|
queue = msm_submitqueue_get(ctx, args->queue_id);
|
|
if (!queue)
|
|
return -ENOENT;
|
|
|
|
if (!(queue->flags & MSM_SUBMITQUEUE_VM_BIND)) {
|
|
ret = UERR(EINVAL, dev, "Invalid queue type");
|
|
goto out_post_unlock;
|
|
}
|
|
|
|
if (args->flags & MSM_VM_BIND_FENCE_FD_OUT) {
|
|
out_fence_fd = get_unused_fd_flags(O_CLOEXEC);
|
|
if (out_fence_fd < 0) {
|
|
ret = out_fence_fd;
|
|
goto out_post_unlock;
|
|
}
|
|
}
|
|
|
|
job = vm_bind_job_create(dev, file, queue, args->nr_ops);
|
|
if (IS_ERR(job)) {
|
|
ret = PTR_ERR(job);
|
|
goto out_post_unlock;
|
|
}
|
|
|
|
ret = mutex_lock_interruptible(&queue->lock);
|
|
if (ret)
|
|
goto out_post_unlock;
|
|
|
|
if (args->flags & MSM_VM_BIND_FENCE_FD_IN) {
|
|
struct dma_fence *in_fence;
|
|
|
|
in_fence = sync_file_get_fence(args->fence_fd);
|
|
|
|
if (!in_fence) {
|
|
ret = UERR(EINVAL, dev, "invalid in-fence");
|
|
goto out_unlock;
|
|
}
|
|
|
|
ret = drm_sched_job_add_dependency(&job->base, in_fence);
|
|
if (ret)
|
|
goto out_unlock;
|
|
}
|
|
|
|
if (args->in_syncobjs > 0) {
|
|
syncobjs_to_reset = msm_syncobj_parse_deps(dev, &job->base,
|
|
file, args->in_syncobjs,
|
|
args->nr_in_syncobjs,
|
|
args->syncobj_stride);
|
|
if (IS_ERR(syncobjs_to_reset)) {
|
|
ret = PTR_ERR(syncobjs_to_reset);
|
|
goto out_unlock;
|
|
}
|
|
}
|
|
|
|
if (args->out_syncobjs > 0) {
|
|
post_deps = msm_syncobj_parse_post_deps(dev, file,
|
|
args->out_syncobjs,
|
|
args->nr_out_syncobjs,
|
|
args->syncobj_stride);
|
|
if (IS_ERR(post_deps)) {
|
|
ret = PTR_ERR(post_deps);
|
|
goto out_unlock;
|
|
}
|
|
}
|
|
|
|
ret = vm_bind_job_lookup_ops(job, args, file, &nr_bos);
|
|
if (ret)
|
|
goto out_unlock;
|
|
|
|
ret = vm_bind_prealloc_count(job);
|
|
if (ret)
|
|
goto out_unlock;
|
|
|
|
struct drm_exec exec;
|
|
unsigned flags = DRM_EXEC_IGNORE_DUPLICATES | DRM_EXEC_INTERRUPTIBLE_WAIT;
|
|
drm_exec_init(&exec, flags, nr_bos + 1);
|
|
|
|
ret = vm_bind_job_lock_objects(job, &exec);
|
|
if (ret)
|
|
goto out;
|
|
|
|
ret = vm_bind_job_pin_objects(job);
|
|
if (ret)
|
|
goto out;
|
|
|
|
ret = vm_bind_job_prepare(job);
|
|
if (ret)
|
|
goto out;
|
|
|
|
drm_sched_job_arm(&job->base);
|
|
|
|
job->fence = dma_fence_get(&job->base.s_fence->finished);
|
|
|
|
if (args->flags & MSM_VM_BIND_FENCE_FD_OUT) {
|
|
sync_file = sync_file_create(job->fence);
|
|
if (!sync_file) {
|
|
ret = -ENOMEM;
|
|
} else {
|
|
fd_install(out_fence_fd, sync_file->file);
|
|
args->fence_fd = out_fence_fd;
|
|
}
|
|
}
|
|
|
|
if (ret)
|
|
goto out;
|
|
|
|
vm_bind_job_attach_fences(job);
|
|
|
|
/*
|
|
* The job can be free'd (and fence unref'd) at any point after
|
|
* drm_sched_entity_push_job(), so we need to hold our own ref
|
|
*/
|
|
fence = dma_fence_get(job->fence);
|
|
|
|
drm_sched_entity_push_job(&job->base);
|
|
|
|
msm_syncobj_reset(syncobjs_to_reset, args->nr_in_syncobjs);
|
|
msm_syncobj_process_post_deps(post_deps, args->nr_out_syncobjs, fence);
|
|
|
|
dma_fence_put(fence);
|
|
|
|
out:
|
|
if (ret)
|
|
vm_bind_job_unpin_objects(job);
|
|
|
|
drm_exec_fini(&exec);
|
|
out_unlock:
|
|
mutex_unlock(&queue->lock);
|
|
out_post_unlock:
|
|
if (ret && (out_fence_fd >= 0)) {
|
|
put_unused_fd(out_fence_fd);
|
|
if (sync_file)
|
|
fput(sync_file->file);
|
|
}
|
|
|
|
if (!IS_ERR_OR_NULL(job)) {
|
|
if (ret)
|
|
msm_vma_job_free(&job->base);
|
|
} else {
|
|
/*
|
|
* If the submit hasn't yet taken ownership of the queue
|
|
* then we need to drop the reference ourself:
|
|
*/
|
|
msm_submitqueue_put(queue);
|
|
}
|
|
|
|
if (!IS_ERR_OR_NULL(post_deps)) {
|
|
for (i = 0; i < args->nr_out_syncobjs; ++i) {
|
|
kfree(post_deps[i].chain);
|
|
drm_syncobj_put(post_deps[i].syncobj);
|
|
}
|
|
kfree(post_deps);
|
|
}
|
|
|
|
if (!IS_ERR_OR_NULL(syncobjs_to_reset)) {
|
|
for (i = 0; i < args->nr_in_syncobjs; ++i) {
|
|
if (syncobjs_to_reset[i])
|
|
drm_syncobj_put(syncobjs_to_reset[i]);
|
|
}
|
|
kfree(syncobjs_to_reset);
|
|
}
|
|
|
|
return ret;
|
|
}
|