blob: ec80ca51ae235267d2a8af56b427d88a12dd5724 [file] [log] [blame]
/*
* linux/drivers/video/omap2/dsscomp/device.c
*
* DSS Composition file device and ioctl support
*
* Copyright (C) 2011 Texas Instruments, Inc
* Author: Lajos Molnar <molnar@ti.com>
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 as published by
* the Free Software Foundation.
*
* 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.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
#define DEBUG
#include <linux/err.h>
#include <linux/platform_device.h>
#include <linux/slab.h>
#include <linux/device.h>
#include <linux/file.h>
#include <linux/mm.h>
#include <linux/fs.h>
#include <linux/anon_inodes.h>
#include <linux/list.h>
#include <linux/miscdevice.h>
#include <linux/uaccess.h>
#include <linux/sched.h>
#include <linux/syscalls.h>
#define MODULE_NAME "dsscomp"
#include <video/omapdss.h>
#include <video/dsscomp.h>
#include <plat/dsscomp.h>
#include "dsscomp.h"
#include <linux/debugfs.h>
static DECLARE_WAIT_QUEUE_HEAD(waitq);
static DEFINE_MUTEX(wait_mtx);
static u32 hwc_virt_to_phys(u32 arg)
{
pmd_t *pmd;
pte_t *ptep;
pgd_t *pgd = pgd_offset(current->mm, arg);
if (pgd_none(*pgd) || pgd_bad(*pgd))
return 0;
pmd = pmd_offset(pgd, arg);
if (pmd_none(*pmd) || pmd_bad(*pmd))
return 0;
ptep = pte_offset_map(pmd, arg);
if (ptep && pte_present(*ptep))
return (PAGE_MASK & *ptep) | (~PAGE_MASK & arg);
return 0;
}
/*
* ===========================================================================
* WAIT OPERATIONS
* ===========================================================================
*/
static void sync_drop(struct dsscomp_sync_obj *sync)
{
if (sync && atomic_dec_and_test(&sync->refs)) {
if (debug & DEBUG_WAITS)
pr_info("free sync [%p]\n", sync);
kfree(sync);
}
}
static int sync_setup(const char *name, const struct file_operations *fops,
struct dsscomp_sync_obj *sync, int flags)
{
if (!sync)
return -ENOMEM;
sync->refs.counter = 1;
sync->fd = anon_inode_getfd(name, fops, sync, flags);
return sync->fd < 0 ? sync->fd : 0;
}
static int sync_finalize(struct dsscomp_sync_obj *sync, int r)
{
if (sync) {
if (r < 0)
/* delete sync object on failure */
sys_close(sync->fd);
else
/* return file descriptor on success */
r = sync->fd;
}
return r;
}
/* wait for programming or release of a composition */
int dsscomp_wait(struct dsscomp_sync_obj *sync, enum dsscomp_wait_phase phase,
int timeout)
{
mutex_lock(&wait_mtx);
if (debug & DEBUG_WAITS)
pr_info("wait %s on [%p]\n",
phase == DSSCOMP_WAIT_DISPLAYED ? "display" :
phase == DSSCOMP_WAIT_PROGRAMMED ? "program" :
"release", sync);
if (sync->state < phase) {
mutex_unlock(&wait_mtx);
timeout = wait_event_interruptible_timeout(waitq,
sync->state >= phase, timeout);
if (debug & DEBUG_WAITS)
pr_info("wait over [%p]: %s %d\n", sync,
timeout < 0 ? "signal" :
timeout > 0 ? "ok" : "timeout",
timeout);
if (timeout <= 0)
return timeout ? : -ETIME;
mutex_lock(&wait_mtx);
}
mutex_unlock(&wait_mtx);
return 0;
}
EXPORT_SYMBOL(dsscomp_wait);
static void dsscomp_queue_cb(void *data, int status)
{
struct dsscomp_sync_obj *sync = data;
enum dsscomp_wait_phase phase =
status == DSS_COMPLETION_PROGRAMMED ? DSSCOMP_WAIT_PROGRAMMED :
status == DSS_COMPLETION_DISPLAYED ? DSSCOMP_WAIT_DISPLAYED :
DSSCOMP_WAIT_RELEASED, old_phase;
mutex_lock(&wait_mtx);
old_phase = sync->state;
if (old_phase < phase)
sync->state = phase;
mutex_unlock(&wait_mtx);
if (status & DSS_COMPLETION_RELEASED)
sync_drop(sync);
if (old_phase < phase)
wake_up_interruptible_sync(&waitq);
}
static int sync_release(struct inode *inode, struct file *filp)
{
struct dsscomp_sync_obj *sync = filp->private_data;
sync_drop(sync);
return 0;
}
static long sync_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
int r = 0;
struct dsscomp_sync_obj *sync = filp->private_data;
void __user *ptr = (void __user *)arg;
switch (cmd) {
case DSSCIOC_WAIT:
{
struct dsscomp_wait_data wd;
r = copy_from_user(&wd, ptr, sizeof(wd)) ? :
dsscomp_wait(sync, wd.phase,
usecs_to_jiffies(wd.timeout_us));
break;
}
default:
r = -EINVAL;
}
return r;
}
static const struct file_operations sync_fops = {
.owner = THIS_MODULE,
.release = sync_release,
.unlocked_ioctl = sync_ioctl,
};
static long setup_mgr(struct dsscomp_dev *cdev,
struct dsscomp_setup_mgr_data *d)
{
int i, r;
struct omap_dss_device *dev;
struct omap_overlay_manager *mgr;
dsscomp_t comp;
struct dsscomp_sync_obj *sync = NULL;
dump_comp_info(cdev, d, "queue");
for (i = 0; i < d->num_ovls; i++)
dump_ovl_info(cdev, d->ovls + i);
/* verify display is valid and connected */
if (d->mgr.ix >= cdev->num_displays)
return -EINVAL;
dev = cdev->displays[d->mgr.ix];
if (!dev)
return -EINVAL;
mgr = dev->manager;
if (!mgr)
return -ENODEV;
comp = dsscomp_new(mgr);
if (IS_ERR(comp))
return PTR_ERR(comp);
/* swap red & blue if requested */
if (d->mgr.swap_rb) {
swap_rb_in_mgr_info(&d->mgr);
for (i = 0; i < d->num_ovls; i++)
swap_rb_in_ovl_info(d->ovls + i);
}
r = dsscomp_set_mgr(comp, &d->mgr);
for (i = 0; i < d->num_ovls; i++) {
struct dss2_ovl_info *oi = d->ovls + i;
u32 addr = (u32) oi->address;
/* convert addresses to user space */
if (oi->cfg.color_mode == OMAP_DSS_COLOR_NV12)
oi->uv = hwc_virt_to_phys(addr +
oi->cfg.height * oi->cfg.stride);
oi->ba = hwc_virt_to_phys(addr);
r = r ? : dsscomp_set_ovl(comp, oi);
}
r = r ? : dsscomp_setup(comp, d->mode, d->win);
/* create sync object */
if (d->get_sync_obj) {
sync = kzalloc(sizeof(*sync), GFP_KERNEL);
r = sync_setup("dsscomp_sync", &sync_fops, sync, O_RDONLY);
if (sync && (debug & DEBUG_WAITS))
dev_info(DEV(cdev), "new sync [%p] on #%d\n", sync,
sync->fd);
if (r)
sync_drop(sync);
}
/* drop composition if failed to create */
if (r) {
dsscomp_drop(comp);
return r;
}
if (sync) {
sync->refs.counter++;
comp->extra_cb = dsscomp_queue_cb;
comp->extra_cb_data = sync;
}
if (d->mode & DSSCOMP_SETUP_APPLY)
r = dsscomp_delayed_apply(comp);
/* delete sync object if failed to apply or create file */
if (sync) {
r = sync_finalize(sync, r);
if (r < 0)
sync_drop(sync);
}
return r;
}
static long query_display(struct dsscomp_dev *cdev,
struct dsscomp_display_info *dis)
{
struct omap_dss_device *dev;
struct omap_overlay_manager *mgr;
int i;
/* get display */
if (dis->ix >= cdev->num_displays)
return -EINVAL;
dev = cdev->displays[dis->ix];
if (!dev)
return -EINVAL;
mgr = dev->manager;
/* fill out display information */
dis->channel = dev->channel;
dis->enabled = (dev->state == OMAP_DSS_DISPLAY_SUSPENDED) ?
dev->activate_after_resume :
(dev->state == OMAP_DSS_DISPLAY_ACTIVE);
dis->overlays_available = 0;
dis->overlays_owned = 0;
#if 0
dis->s3d_info = dev->panel.s3d_info;
#endif
dis->state = dev->state;
dis->timings = dev->panel.timings;
dis->width_in_mm = DIV_ROUND_CLOSEST(dev->panel.width_in_um, 1000);
dis->height_in_mm = DIV_ROUND_CLOSEST(dev->panel.height_in_um, 1000);
/* find all overlays available for/owned by this display */
for (i = 0; i < cdev->num_ovls && dis->enabled; i++) {
if (cdev->ovls[i]->manager == mgr)
dis->overlays_owned |= 1 << i;
else if (!cdev->ovls[i]->info.enabled)
dis->overlays_available |= 1 << i;
}
dis->overlays_available |= dis->overlays_owned;
/* fill out manager information */
if (mgr) {
dis->mgr.alpha_blending = mgr->info.alpha_enabled;
dis->mgr.default_color = mgr->info.default_color;
#if 0
dis->mgr.interlaced = !strcmp(dev->name, "hdmi") &&
is_hdmi_interlaced()
#else
dis->mgr.interlaced = 0;
#endif
dis->mgr.trans_enabled = mgr->info.trans_enabled;
dis->mgr.trans_key = mgr->info.trans_key;
dis->mgr.trans_key_type = mgr->info.trans_key_type;
} else {
/* display is disabled if it has no manager */
memset(&dis->mgr, 0, sizeof(dis->mgr));
}
dis->mgr.ix = dis->ix;
if (dis->modedb_len && dev->driver->get_modedb)
dis->modedb_len = dev->driver->get_modedb(dev,
(struct fb_videomode *) dis->modedb, dis->modedb_len);
return 0;
}
static long check_ovl(struct dsscomp_dev *cdev,
struct dsscomp_check_ovl_data *chk)
{
/* for now return all overlays as possible */
return (1 << cdev->num_ovls) - 1;
}
static long setup_display(struct dsscomp_dev *cdev,
struct dsscomp_setup_display_data *dis)
{
struct omap_dss_device *dev;
/* get display */
if (dis->ix >= cdev->num_displays)
return -EINVAL;
dev = cdev->displays[dis->ix];
if (!dev)
return -EINVAL;
if (dev->driver->set_mode)
return dev->driver->set_mode(dev,
(struct fb_videomode *) &dis->mode);
else
return 0;
}
static void fill_cache(struct dsscomp_dev *cdev)
{
unsigned long i;
struct omap_dss_device *dssdev = NULL;
cdev->num_ovls = min(omap_dss_get_num_overlays(), MAX_OVERLAYS);
for (i = 0; i < cdev->num_ovls; i++)
cdev->ovls[i] = omap_dss_get_overlay(i);
cdev->num_mgrs = min(omap_dss_get_num_overlay_managers(), MAX_MANAGERS);
for (i = 0; i < cdev->num_mgrs; i++)
cdev->mgrs[i] = omap_dss_get_overlay_manager(i);
for_each_dss_dev(dssdev) {
const char *name = dev_name(&dssdev->dev);
if (strncmp(name, "display", 7) ||
strict_strtoul(name + 7, 10, &i) ||
i >= MAX_DISPLAYS)
continue;
if (cdev->num_displays <= i)
cdev->num_displays = i + 1;
cdev->displays[i] = dssdev;
dev_dbg(DEV(cdev), "display%lu=%s\n", i, dssdev->driver_name);
cdev->state_notifiers[i].notifier_call = dsscomp_state_notifier;
blocking_notifier_chain_register(&dssdev->state_notifiers,
cdev->state_notifiers + i);
}
dev_info(DEV(cdev), "found %d displays and %d overlays\n",
cdev->num_displays, cdev->num_ovls);
}
static long comp_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
int r = 0;
struct miscdevice *dev = filp->private_data;
struct dsscomp_dev *cdev = container_of(dev, struct dsscomp_dev, dev);
void __user *ptr = (void __user *)arg;
union {
struct {
struct dsscomp_setup_mgr_data set;
struct dss2_ovl_info ovl[MAX_OVERLAYS];
} m;
struct dsscomp_setup_dispc_data dispc;
struct dsscomp_display_info dis;
struct dsscomp_check_ovl_data chk;
struct dsscomp_setup_display_data sdis;
} u;
dsscomp_gralloc_init(cdev);
switch (cmd) {
case DSSCIOC_SETUP_MGR:
{
r = copy_from_user(&u.m.set, ptr, sizeof(u.m.set)) ? :
u.m.set.num_ovls >= ARRAY_SIZE(u.m.ovl) ? -EINVAL :
copy_from_user(&u.m.ovl,
(void __user *)arg + sizeof(u.m.set),
sizeof(*u.m.ovl) * u.m.set.num_ovls) ? :
setup_mgr(cdev, &u.m.set);
break;
}
case DSSCIOC_SETUP_DISPC:
{
r = copy_from_user(&u.dispc, ptr, sizeof(u.dispc)) ? :
dsscomp_gralloc_queue_ioctl(&u.dispc);
break;
}
case DSSCIOC_QUERY_DISPLAY:
{
struct dsscomp_display_info *dis = NULL;
r = copy_from_user(&u.dis, ptr, sizeof(u.dis));
if (!r)
dis = kzalloc(sizeof(*dis->modedb) * u.dis.modedb_len +
sizeof(*dis), GFP_KERNEL);
if (dis) {
*dis = u.dis;
r = query_display(cdev, dis) ? :
copy_to_user(ptr, dis, sizeof(*dis) +
sizeof(*dis->modedb) * dis->modedb_len);
kfree(dis);
} else {
r = r ? : -ENOMEM;
}
break;
}
case DSSCIOC_CHECK_OVL:
{
r = copy_from_user(&u.chk, ptr, sizeof(u.chk)) ? :
check_ovl(cdev, &u.chk);
break;
}
case DSSCIOC_SETUP_DISPLAY:
{
r = copy_from_user(&u.sdis, ptr, sizeof(u.sdis)) ? :
setup_display(cdev, &u.sdis);
}
default:
r = -EINVAL;
}
return r;
}
/* must implement open for filp->private_data to be filled */
static int comp_open(struct inode *inode, struct file *filp)
{
return 0;
}
static const struct file_operations comp_fops = {
.owner = THIS_MODULE,
.open = comp_open,
.unlocked_ioctl = comp_ioctl,
};
static int dsscomp_debug_show(struct seq_file *s, void *unused)
{
void (*fn)(struct seq_file *s) = s->private;
fn(s);
return 0;
}
static int dsscomp_debug_open(struct inode *inode, struct file *file)
{
return single_open(file, dsscomp_debug_show, inode->i_private);
}
static const struct file_operations dsscomp_debug_fops = {
.open = dsscomp_debug_open,
.read = seq_read,
.llseek = seq_lseek,
.release = single_release,
};
static int dsscomp_probe(struct platform_device *pdev)
{
int ret;
struct dsscomp_dev *cdev = kzalloc(sizeof(*cdev), GFP_KERNEL);
if (!cdev) {
pr_err("dsscomp: failed to allocate device.\n");
return -ENOMEM;
}
cdev->dev.minor = MISC_DYNAMIC_MINOR;
cdev->dev.name = "dsscomp";
cdev->dev.mode = 0666;
cdev->dev.fops = &comp_fops;
ret = misc_register(&cdev->dev);
if (ret) {
pr_err("dsscomp: failed to register misc device.\n");
return ret;
}
cdev->dbgfs = debugfs_create_dir("dsscomp", NULL);
if (IS_ERR_OR_NULL(cdev->dbgfs))
dev_warn(DEV(cdev), "failed to create debug files.\n");
else {
debugfs_create_file("comps", S_IRUGO,
cdev->dbgfs, dsscomp_dbg_comps, &dsscomp_debug_fops);
debugfs_create_file("gralloc", S_IRUGO,
cdev->dbgfs, dsscomp_dbg_gralloc, &dsscomp_debug_fops);
#ifdef CONFIG_DSSCOMP_DEBUG_LOG
debugfs_create_file("log", S_IRUGO,
cdev->dbgfs, dsscomp_dbg_events, &dsscomp_debug_fops);
#endif
}
platform_set_drvdata(pdev, cdev);
pr_info("dsscomp: initializing.\n");
fill_cache(cdev);
/* initialize queues */
dsscomp_queue_init(cdev);
dsscomp_gralloc_init(cdev);
return 0;
}
static int dsscomp_remove(struct platform_device *pdev)
{
struct dsscomp_dev *cdev = platform_get_drvdata(pdev);
misc_deregister(&cdev->dev);
debugfs_remove_recursive(cdev->dbgfs);
dsscomp_queue_exit();
dsscomp_gralloc_exit();
kfree(cdev);
return 0;
}
static struct platform_driver dsscomp_pdriver = {
.probe = dsscomp_probe,
.remove = dsscomp_remove,
.driver = { .name = MODULE_NAME, .owner = THIS_MODULE }
};
static struct platform_device dsscomp_pdev = {
.name = MODULE_NAME,
.id = -1
};
static int __init dsscomp_init(void)
{
int err = platform_driver_register(&dsscomp_pdriver);
if (err)
return err;
err = platform_device_register(&dsscomp_pdev);
if (err)
platform_driver_unregister(&dsscomp_pdriver);
return err;
}
static void __exit dsscomp_exit(void)
{
platform_device_unregister(&dsscomp_pdev);
platform_driver_unregister(&dsscomp_pdriver);
}
#define DUMP_CHUNK 256
static char dump_buf[64 * 1024];
void dsscomp_kdump(void)
{
struct seq_file s = {
.buf = dump_buf,
.size = sizeof(dump_buf) - 1,
};
int i;
dsscomp_dbg_events(&s);
dsscomp_dbg_comps(&s);
dsscomp_dbg_gralloc(&s);
for (i = 0; i < s.count; i += DUMP_CHUNK) {
if ((s.count - i) > DUMP_CHUNK) {
char c = s.buf[i + DUMP_CHUNK];
s.buf[i + DUMP_CHUNK] = 0;
pr_cont("%s", s.buf + i);
s.buf[i + DUMP_CHUNK] = c;
} else {
s.buf[s.count] = 0;
pr_cont("%s", s.buf + i);
}
}
}
EXPORT_SYMBOL(dsscomp_kdump);
MODULE_LICENSE("GPL v2");
module_init(dsscomp_init);
module_exit(dsscomp_exit);