/* arch/arm/mach-msm/hw3d.c * * Register/Interrupt access for userspace 3D library. * * Copyright (C) 2007 Google, Inc. * Author: Brian Swetland * Heavily modified: Dima Zavin * * This software is licensed under the terms of the GNU General Public * License version 2, as published by the Free Software Foundation, and * may be copied, distributed, and modified under those terms. * * This program is distributed in the hope that 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. * */ /* #define DEBUG */ /* #define VERBOSE */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(VERBOSE) #define VDBG(x...) pr_debug(x) #else #define VDBG(x...) do {} while(0) #endif struct mem_region { unsigned long pbase; unsigned long size; void __iomem *vbase; }; /* Device minor numbers for master and client */ #define MINOR_MASTER 0 #define MINOR_CLIENT 1 struct hw3d_info { dev_t devno; struct cdev master_cdev; struct cdev client_cdev; struct clk *grp_clk; struct clk *imem_clk; int irq; struct mem_region regions[HW3D_NUM_REGIONS]; wait_queue_head_t irq_wq; bool irq_pending; bool irq_en; bool suspending; bool revoking; bool enabled; struct timer_list revoke_timer; wait_queue_head_t revoke_wq; wait_queue_head_t revoke_done_wq; unsigned int waiter_cnt; spinlock_t lock; struct file *client_file; struct task_struct *client_task; struct early_suspend early_suspend; struct wake_lock wake_lock; }; static struct hw3d_info *hw3d_info; struct hw3d_data { struct vm_area_struct *vmas[HW3D_NUM_REGIONS]; struct mutex mutex; bool closing; }; #define REGION_PAGE_ID(addr) \ ((((uint32_t)(addr)) >> (28 - PAGE_SHIFT)) & 0xf) #define REGION_PAGE_OFFS(addr) \ ((((uint32_t)(addr)) & ~(0xf << (28 - PAGE_SHIFT)))) static int hw3d_open(struct inode *, struct file *); static int hw3d_release(struct inode *, struct file *); static int hw3d_mmap(struct file *, struct vm_area_struct *); static int hw3d_flush(struct file *, fl_owner_t); static long hw3d_ioctl(struct file *, unsigned int, unsigned long); static void hw3d_vma_open(struct vm_area_struct *); static void hw3d_vma_close(struct vm_area_struct *); static struct file_operations hw3d_fops = { .open = hw3d_open, .release = hw3d_release, .mmap = hw3d_mmap, .flush = hw3d_flush, .unlocked_ioctl = hw3d_ioctl, }; static struct vm_operations_struct hw3d_vm_ops = { .open = hw3d_vma_open, .close = hw3d_vma_close, }; static bool is_master(struct hw3d_info *info, struct file *file) { int fmin = MINOR(file->f_dentry->d_inode->i_rdev); return fmin == MINOR(info->master_cdev.dev); } static bool is_client(struct hw3d_info *info, struct file *file) { int fmin = MINOR(file->f_dentry->d_inode->i_rdev); return fmin == MINOR(info->client_cdev.dev); } inline static void locked_hw3d_irq_disable(struct hw3d_info *info) { if (info->irq_en) { disable_irq_nosync(info->irq); info->irq_en = 0; } } inline static void locked_hw3d_irq_enable(struct hw3d_info *info) { if (!info->irq_en) { enable_irq(info->irq); info->irq_en = 1; } } static void hw3d_disable_interrupt(struct hw3d_info *info) { unsigned long flags; spin_lock_irqsave(&info->lock, flags); locked_hw3d_irq_disable(info); spin_unlock_irqrestore(&info->lock, flags); } static irqreturn_t hw3d_irq_handler(int irq, void *data) { struct hw3d_info *info = data; unsigned long flags; spin_lock_irqsave(&info->lock, flags); locked_hw3d_irq_disable(info); info->irq_pending = 1; spin_unlock_irqrestore(&info->lock, flags); wake_up(&info->irq_wq); return IRQ_HANDLED; } static long hw3d_wait_for_interrupt(struct hw3d_info *info, struct file *filp) { struct hw3d_data *data = filp->private_data; unsigned long flags; int ret; if (is_master(info, filp)) { pr_err("%s: cannot wait for irqs on master node\n", __func__); return -EPERM; } for (;;) { spin_lock_irqsave(&info->lock, flags); if (info->irq_pending) { info->irq_pending = 0; spin_unlock_irqrestore(&info->lock, flags); return 0; } locked_hw3d_irq_enable(info); spin_unlock_irqrestore(&info->lock, flags); ret = wait_event_interruptible(info->irq_wq, info->irq_pending || info->revoking || data->closing); /* always make sure the irq gets disabled */ if (ret == 0 && !info->irq_pending) ret = -EPIPE; if (ret < 0) { hw3d_disable_interrupt(info); return ret; } } return 0; } static long hw3d_wait_for_revoke(struct hw3d_info *info, struct file *filp) { struct hw3d_data *data = filp->private_data; int ret; if (is_master(info, filp)) { pr_err("%s: cannot revoke on master node\n", __func__); return -EPERM; } ret = wait_event_interruptible(info->revoke_wq, info->revoking || data->closing); if (ret == 0 && data->closing) ret = -EPIPE; if (ret < 0) return ret; return 0; } static void locked_hw3d_client_done(struct hw3d_info *info, int had_timer) { if (info->enabled) { pr_debug("hw3d: was enabled\n"); info->enabled = 0; clk_disable(info->grp_clk); clk_disable(info->imem_clk); } info->revoking = 0; /* double check that the irqs are disabled */ locked_hw3d_irq_disable(info); if (had_timer) wake_unlock(&info->wake_lock); wake_up(&info->revoke_done_wq); } static void do_force_revoke(struct hw3d_info *info) { unsigned long flags; /* at this point, the task had a chance to relinquish the gpu, but * it hasn't. So, we kill it */ spin_lock_irqsave(&info->lock, flags); pr_debug("hw3d: forcing revoke\n"); locked_hw3d_irq_disable(info); if (info->client_task) { pr_info("hw3d: force revoke from pid=%d\n", info->client_task->pid); force_sig(SIGKILL, info->client_task); put_task_struct(info->client_task); info->client_task = NULL; } locked_hw3d_client_done(info, 1); pr_debug("hw3d: done forcing revoke\n"); spin_unlock_irqrestore(&info->lock, flags); } #define REVOKE_TIMEOUT (2 * HZ) static void locked_hw3d_revoke(struct hw3d_info *info) { /* force us to wait to suspend until the revoke is done. If the * user doesn't release the gpu, the timer will turn off the gpu, * and force kill the process. */ wake_lock(&info->wake_lock); info->revoking = 1; wake_up(&info->revoke_wq); mod_timer(&info->revoke_timer, jiffies + REVOKE_TIMEOUT); } bool is_msm_hw3d_file(struct file *file) { struct hw3d_info *info = hw3d_info; if (MAJOR(file->f_dentry->d_inode->i_rdev) == MAJOR(info->devno) && (is_master(info, file) || is_client(info, file))) return 1; return 0; } void put_msm_hw3d_file(struct file *file) { if (!is_msm_hw3d_file(file)) return; fput(file); } int get_msm_hw3d_file(int fd, uint32_t *offs, unsigned long *pbase, unsigned long *len, struct file **filp) { struct hw3d_info *info = hw3d_info; struct file *file; struct hw3d_data *data; uint32_t offset = HW3D_OFFSET_IN_REGION(*offs); int region = HW3D_REGION_ID(*offs); int ret = 0; if (unlikely(region >= HW3D_NUM_REGIONS)) { VDBG("hw3d: invalid region %d requested\n", region); return -EINVAL; } else if (unlikely(offset >= info->regions[region].size)) { VDBG("hw3d: offset %08x outside of the requested region %d\n", offset, region); return -EINVAL; } file = fget(fd); if (unlikely(file == NULL)) { pr_info("%s: requested data from file descriptor that doesn't " "exist.", __func__); return -EINVAL; } else if (!is_msm_hw3d_file(file)) { ret = -1; goto err; } data = file->private_data; if (unlikely(!data)) { VDBG("hw3d: invalid file\n"); ret = -EINVAL; goto err; } mutex_lock(&data->mutex); if (unlikely(!data->vmas[region])) { mutex_unlock(&data->mutex); VDBG("hw3d: requested hw3d region is not mapped\n"); ret = -ENOENT; goto err; } *offs = offset; *pbase = info->regions[region].pbase; *filp = file; *len = data->vmas[region]->vm_end - data->vmas[region]->vm_start; mutex_unlock(&data->mutex); return 0; err: fput(file); return ret; } static int hw3d_flush(struct file *filp, fl_owner_t id) { struct hw3d_info *info = hw3d_info; struct hw3d_data *data = filp->private_data; if (!data) { pr_err("%s: no private data\n", __func__); return -EINVAL; } if (is_master(info, filp)) return 0; pr_debug("hw3d: closing\n"); /* releases any blocked ioctls */ data->closing = 1; wake_up(&info->revoke_wq); wake_up(&info->irq_wq); return 0; } static int should_wakeup(struct hw3d_info *info, unsigned int cnt) { unsigned long flags; int ret; spin_lock_irqsave(&info->lock, flags); ret = (cnt != info->waiter_cnt) || (!info->suspending && !info->client_file); spin_unlock_irqrestore(&info->lock, flags); return ret; } static int locked_open_wait_for_gpu(struct hw3d_info *info, unsigned long *flags) { unsigned int my_cnt; int ret; my_cnt = ++info->waiter_cnt; pr_debug("%s: wait_for_open %d\n", __func__, my_cnt); /* in case there are other waiters, wake and release them. */ wake_up(&info->revoke_done_wq); if (info->suspending) pr_info("%s: suspended, waiting (%d %d)\n", __func__, current->group_leader->pid, current->pid); if (info->client_file) pr_info("%s: has client, waiting (%d %d)\n", __func__, current->group_leader->pid, current->pid); spin_unlock_irqrestore(&info->lock, *flags); ret = wait_event_interruptible(info->revoke_done_wq, should_wakeup(info, my_cnt)); spin_lock_irqsave(&info->lock, *flags); pr_debug("%s: woke up (%d %d %p)\n", __func__, info->waiter_cnt, info->suspending, info->client_file); if (ret >= 0) { if (my_cnt != info->waiter_cnt) { pr_info("%s: someone else asked for gpu after us %d:%d" "(%d %d)\n", __func__, current->group_leader->pid, current->pid, my_cnt, info->waiter_cnt); ret = -EBUSY; } else if (info->suspending || info->client_file) { pr_err("%s: couldn't get the gpu for %d:%d (%d %p)\n", __func__, current->group_leader->pid, current->pid, info->suspending, info->client_file); ret = -EBUSY; } else ret = 0; } return ret; } static int hw3d_open(struct inode *inode, struct file *file) { struct hw3d_info *info = hw3d_info; struct hw3d_data *data; unsigned long flags; int ret = 0; pr_info("%s: pid %d tid %d opening %s node\n", __func__, current->group_leader->pid, current->pid, is_master(info, file) ? "master" : "client"); if (file->private_data != NULL) return -EINVAL; data = kzalloc(sizeof(struct hw3d_data), GFP_KERNEL); if (!data) { pr_err("%s: unable to allocate memory for hw3d_data.\n", __func__); return -ENOMEM; } mutex_init(&data->mutex); file->private_data = data; /* master always succeeds, so we are done */ if (is_master(info, file)) return 0; spin_lock_irqsave(&info->lock, flags); if (info->client_file) { pr_debug("hw3d: have client_file, need revoke\n"); locked_hw3d_revoke(info); } ret = locked_open_wait_for_gpu(info, &flags); if (ret < 0) { spin_unlock_irqrestore(&info->lock, flags); goto err; } pr_info("%s: pid %d tid %d got gpu\n", __func__, current->group_leader->pid, current->pid); info->client_file = file; get_task_struct(current->group_leader); info->client_task = current->group_leader; /* XXX: we enable these clocks if the client connects.. * probably not right? Should only turn the clocks on when the user * tries to map the registers? */ clk_enable(info->imem_clk); clk_enable(info->grp_clk); info->enabled = 1; spin_unlock_irqrestore(&info->lock, flags); return 0; err: file->private_data = NULL; kfree(data); return ret; } static int hw3d_release(struct inode *inode, struct file *file) { struct hw3d_info *info = hw3d_info; struct hw3d_data *data = file->private_data; unsigned long flags; BUG_ON(!data); file->private_data = NULL; if (is_master(info, file)) goto done; pr_info("%s: in release for pid=%d tid=%d\n", __func__, current->group_leader->pid, current->pid); spin_lock_irqsave(&info->lock, flags); if (info->client_task && info->client_task == current->group_leader) { pr_debug("hw3d: releasing %d\n", info->client_task->pid); put_task_struct(info->client_task); info->client_task = NULL; } if (info->client_file && info->client_file == file) { int pending; /* this will be true if we are still the "owner" of the gpu */ pr_debug("hw3d: had file\n"); pending = del_timer(&info->revoke_timer); locked_hw3d_client_done(info, pending); info->client_file = NULL; } else pr_warning("hw3d: release without client_file.\n"); spin_unlock_irqrestore(&info->lock, flags); done: kfree(data); return 0; } static void hw3d_vma_open(struct vm_area_struct *vma) { /* XXX: should the master be allowed to fork and keep the mappings? */ /* TODO: remap garbage page into here. * * For now, just pull the mapping. The user shouldn't be forking * and using it anyway. */ zap_page_range(vma, vma->vm_start, vma->vm_end - vma->vm_start, NULL); } static void hw3d_vma_close(struct vm_area_struct *vma) { struct file *file = vma->vm_file; struct hw3d_data *data = file->private_data; int i; pr_debug("hw3d: current %u ppid %u file %p count %ld\n", current->pid, current->parent->pid, file, file_count(file)); BUG_ON(!data); mutex_lock(&data->mutex); for (i = 0; i < HW3D_NUM_REGIONS; ++i) { if (data->vmas[i] == vma) { data->vmas[i] = NULL; goto done; } } pr_warning("%s: vma %p not of ours during vma_close\n", __func__, vma); done: mutex_unlock(&data->mutex); } static int hw3d_mmap(struct file *file, struct vm_area_struct *vma) { struct hw3d_info *info = hw3d_info; struct hw3d_data *data = file->private_data; unsigned long vma_size = vma->vm_end - vma->vm_start; int ret = 0; int region = REGION_PAGE_ID(vma->vm_pgoff); if (region >= HW3D_NUM_REGIONS) { pr_err("%s: Trying to mmap unknown region %d\n", __func__, region); return -EINVAL; } else if (vma_size > info->regions[region].size) { pr_err("%s: VMA size %ld exceeds region %d size %ld\n", __func__, vma_size, region, info->regions[region].size); return -EINVAL; } else if (REGION_PAGE_OFFS(vma->vm_pgoff) != 0 || (vma_size & ~PAGE_MASK)) { pr_err("%s: Can't remap part of the region %d\n", __func__, region); return -EINVAL; } else if (!is_master(info, file) && current->group_leader != info->client_task) { pr_err("%s: current(%d) != client_task(%d)\n", __func__, current->group_leader->pid, info->client_task->pid); return -EPERM; } else if (!is_master(info, file) && (info->revoking || info->suspending)) { pr_err("%s: cannot mmap while revoking(%d) or suspending(%d)\n", __func__, info->revoking, info->suspending); return -EPERM; } mutex_lock(&data->mutex); if (data->vmas[region] != NULL) { pr_err("%s: Region %d already mapped (pid=%d tid=%d)\n", __func__, region, current->group_leader->pid, current->pid); ret = -EBUSY; goto done; } /* our mappings are always noncached */ #ifdef pgprot_noncached vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot); #endif ret = io_remap_pfn_range(vma, vma->vm_start, info->regions[region].pbase >> PAGE_SHIFT, vma_size, vma->vm_page_prot); if (ret) { pr_err("%s: Cannot remap page range for region %d!\n", __func__, region); ret = -EAGAIN; goto done; } /* Prevent a malicious client from stealing another client's data * by forcing a revoke on it and then mmapping the GPU buffers. */ if (region != HW3D_REGS) memset(info->regions[region].vbase, 0, info->regions[region].size); vma->vm_ops = &hw3d_vm_ops; /* mark this region as mapped */ data->vmas[region] = vma; done: mutex_unlock(&data->mutex); return ret; } static long hw3d_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { struct hw3d_info *info = hw3d_info; struct hw3d_region regions[HW3D_NUM_REGIONS]; int i; if (!file->private_data) return -EINVAL; switch (cmd) { case HW3D_WAIT_FOR_REVOKE: return hw3d_wait_for_revoke(info, file); case HW3D_WAIT_FOR_INTERRUPT: return hw3d_wait_for_interrupt(info, file); case HW3D_GET_REGIONS: for (i = 0; i < HW3D_NUM_REGIONS; ++i) { regions[i].phys = info->regions[i].pbase; regions[i].map_offset = HW3D_REGION_OFFSET(i); regions[i].len = info->regions[i].size; } if (copy_to_user((void __user *)arg, regions, sizeof(regions))) return -EFAULT; break; default: return -EINVAL; } return 0; } static void hw3d_early_suspend(struct early_suspend *h) { unsigned long flags; struct hw3d_info *info; info = container_of(h, struct hw3d_info, early_suspend); spin_lock_irqsave(&info->lock, flags); info->suspending = 1; if (info->client_file) { pr_info("hw3d: Requesting revoke for suspend\n"); locked_hw3d_revoke(info); } spin_unlock_irqrestore(&info->lock, flags); } static void hw3d_late_resume(struct early_suspend *h) { unsigned long flags; struct hw3d_info *info; info = container_of(h, struct hw3d_info, early_suspend); spin_lock_irqsave(&info->lock, flags); if (info->suspending) pr_info("%s: resuming\n", __func__); info->suspending = 0; wake_up(&info->revoke_done_wq); spin_unlock_irqrestore(&info->lock, flags); } static int hw3d_resume(struct platform_device *pdev) { struct hw3d_info *info = platform_get_drvdata(pdev); unsigned long flags; spin_lock_irqsave(&info->lock, flags); if (info->suspending) pr_info("%s: resuming\n", __func__); info->suspending = 0; spin_unlock_irqrestore(&info->lock, flags); return 0; } static int __init hw3d_probe(struct platform_device *pdev) { struct hw3d_info *info; #define DEV_MASTER MKDEV(MAJOR(info->devno), MINOR_MASTER) #define DEV_CLIENT MKDEV(MAJOR(info->devno), MINOR_CLIENT) struct class *hw3d_class; struct device *master_dev; struct device *client_dev; struct resource *res[HW3D_NUM_REGIONS]; int i; int irq; int ret = 0; res[HW3D_REGS] = platform_get_resource_byname(pdev, IORESOURCE_MEM, "regs"); res[HW3D_SMI] = platform_get_resource_byname(pdev, IORESOURCE_MEM, "smi"); res[HW3D_EBI] = platform_get_resource_byname(pdev, IORESOURCE_MEM, "ebi"); irq = platform_get_irq(pdev, 0); if (!res[HW3D_REGS] || !res[HW3D_SMI] || !res[HW3D_EBI] || irq < 0) { pr_err("%s: incomplete resources\n", __func__); return -EINVAL; } info = kzalloc(sizeof(struct hw3d_info), GFP_KERNEL); if (info == NULL) { pr_err("%s: Cannot allocate memory for hw3d_info\n", __func__); ret = -ENOMEM; goto err_alloc; } info->irq = irq; wake_lock_init(&info->wake_lock, WAKE_LOCK_SUSPEND, "hw3d_revoke_lock"); spin_lock_init(&info->lock); init_waitqueue_head(&info->irq_wq); init_waitqueue_head(&info->revoke_wq); init_waitqueue_head(&info->revoke_done_wq); setup_timer(&info->revoke_timer, (void (*)(unsigned long))do_force_revoke, (unsigned long)info); platform_set_drvdata(pdev, info); info->grp_clk = clk_get(NULL, "grp_clk"); if (IS_ERR(info->grp_clk)) { pr_err("%s: Cannot get grp_clk\n", __func__); ret = PTR_ERR(info->grp_clk); goto err_get_grp_clk; } info->imem_clk = clk_get(NULL, "imem_clk"); if (IS_ERR(info->imem_clk)) { pr_err("%s: Cannot get imem_clk\n", __func__); ret = PTR_ERR(info->imem_clk); goto err_get_imem_clk; } for (i = 0; i < HW3D_NUM_REGIONS; ++i) { info->regions[i].pbase = res[i]->start; info->regions[i].size = res[i]->end - res[i]->start + 1; info->regions[i].vbase = ioremap(info->regions[i].pbase, info->regions[i].size); if (info->regions[i].vbase == 0) { pr_err("%s: Cannot remap region %d\n", __func__, i); goto err_remap_region; } } hw3d_class = class_create(THIS_MODULE, "msm_hw3d"); if (IS_ERR(hw3d_class)) goto err_fail_create_class; ret = alloc_chrdev_region(&info->devno, 0, 2, "msm_hw3d"); if (ret < 0) goto err_fail_alloc_region; /* register the master/client devices */ master_dev = device_create(hw3d_class, &pdev->dev, DEV_MASTER, "%s", "msm_hw3dm"); if (IS_ERR(master_dev)) goto err_dev_master; cdev_init(&info->master_cdev, &hw3d_fops); info->master_cdev.owner = THIS_MODULE; ret = cdev_add(&info->master_cdev, DEV_MASTER, 1); if (ret < 0) { pr_err("%s: Cannot register master device node\n", __func__); goto err_reg_master; } client_dev = device_create(hw3d_class, &pdev->dev, DEV_CLIENT, "%s", "msm_hw3dc"); if (IS_ERR(client_dev)) goto err_dev_client; cdev_init(&info->client_cdev, &hw3d_fops); info->client_cdev.owner = THIS_MODULE; ret = cdev_add(&info->client_cdev, DEV_CLIENT, 1); if (ret < 0) { pr_err("%s: Cannot register client device node\n", __func__); goto err_reg_client; } info->early_suspend.suspend = hw3d_early_suspend; info->early_suspend.resume = hw3d_late_resume; info->early_suspend.level = EARLY_SUSPEND_LEVEL_DISABLE_FB; register_early_suspend(&info->early_suspend); info->irq_en = 1; ret = request_irq(info->irq, hw3d_irq_handler, IRQF_TRIGGER_HIGH, "hw3d", info); if (ret != 0) { pr_err("%s: Cannot request irq\n", __func__); goto err_req_irq; } hw3d_disable_interrupt(info); hw3d_info = info; return 0; err_req_irq: unregister_early_suspend(&info->early_suspend); cdev_del(&info->client_cdev); err_reg_client: device_destroy(hw3d_class, DEV_CLIENT); err_dev_client: cdev_del(&info->master_cdev); err_reg_master: device_destroy(hw3d_class, DEV_MASTER); err_dev_master: unregister_chrdev_region(info->devno, 2); err_fail_alloc_region: class_unregister(hw3d_class); err_fail_create_class: err_remap_region: for (i = 0; i < HW3D_NUM_REGIONS; ++i) if (info->regions[i].vbase != 0) iounmap(info->regions[i].vbase); clk_put(info->imem_clk); err_get_imem_clk: clk_put(info->grp_clk); err_get_grp_clk: wake_lock_destroy(&info->wake_lock); kfree(info); platform_set_drvdata(pdev, NULL); err_alloc: hw3d_info = NULL; return ret; } static struct platform_driver msm_hw3d_driver = { .probe = hw3d_probe, .resume = hw3d_resume, .driver = { .name = "msm_hw3d", .owner = THIS_MODULE, }, }; static int __init hw3d_init(void) { return platform_driver_register(&msm_hw3d_driver); } device_initcall(hw3d_init);