tegrakernel/kernel/nvidia/drivers/video/tegra/dc/ext/scrncapt.c

716 lines
19 KiB
C

/*
* scrncapt.c: Screen capture functionality for tegradc ext interface.
*
* Copyright (c) 2016-2020, NVIDIA CORPORATION, All rights reserved.
*
* Author: Sungwook Kim <sungwookk@nvidia.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* 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.
*/
#include <linux/file.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/slab.h>
#include <linux/workqueue.h>
#include <linux/export.h>
#include <linux/delay.h>
#include <linux/timer.h>
#include <linux/memblock.h>
#include <linux/fb.h>
#include <uapi/video/tegra_dc_ext.h>
#include <trace/events/display.h>
#include "../dc.h"
#include "../dc_priv.h"
#include "../dc_config.h"
#include "tegra_dc_ext_priv.h"
/*
* private info for
* Tegra DC screen Capture
*/
static struct tegra_dc_scrncapt_private {
struct mutex lock;
int holder_pid;
u32 magic;
u32 pause_heads;
struct rw_semaphore *rwsema_head;
struct timer_list tmr_resume;
unsigned long tm_resume;
} scrncapt;
/*
* Interface with Tegra DC Driver
*
* Tegra DC Driver should call tegra_dc_scrncapt_disp_pause_lock()
* and tegra_dc_scrncapt_disp_pause_unlock() to protect the region
* that has an update to display which to be captured.
*/
void tegra_dc_scrncapt_disp_pause_lock(struct tegra_dc *dc)
{
down_read(&scrncapt.rwsema_head[dc->ctrl_num]);
}
void tegra_dc_scrncapt_disp_pause_unlock(struct tegra_dc *dc)
{
up_read(&scrncapt.rwsema_head[dc->ctrl_num]);
}
/*
* I/O Controls
*/
static size_t scrncapt_get_dcbuf_len(struct tegra_dc_dmabuf *dcbuf)
{
if (dcbuf && dcbuf->buf)
return dcbuf->buf->size;
else
return 0;
}
/* copy a Tegra DC DMA buffer contents
* o inputs:
* - pDst: pointer to the destination
* must have enough allocation to hold the copy
* - pSrcBuf: pointer to the source Tegra DC DMA buffer
* - len: number of bytes to be copied
* o outputs:
* - return: number of bytes copied
*/
static size_t scrncapt_copy_dcbuf(void __user *pDst,
struct tegra_dc_dmabuf *pSrcBuf, size_t len)
{
size_t copied;
unsigned long ret;
if (!len || !pDst || !pSrcBuf || !pSrcBuf->buf || !pSrcBuf->sgt
|| !pSrcBuf->sgt->nents || !pSrcBuf->sgt->sgl)
return 0;
/* to emulate the segmented memory copy
* copied = sg_copy_to_buffer(pSrcBuf->sgt->sgl,
* pSrcBuf->sgt->nents, pDst, len);
* sg_copy_to_buffer() is not compatible with EBP DisplayServer
* virtualization due to no nvmap carveout highmem support.
*/
{
void *vaddr_src;
struct scatterlist *sg;
unsigned int i;
size_t ofs, l;
unsigned long flags;
pr_debug("@@ %s: copy to %p from %p(PA=%llx) nents=%u len=%zu\n",
__func__, pDst, sg_virt(&pSrcBuf->sgt->sgl[0]),
sg_phys(&pSrcBuf->sgt->sgl[0]),
pSrcBuf->sgt->nents, len);
ofs = 0;
local_irq_save(flags);
for_each_sg(pSrcBuf->sgt->sgl, sg, pSrcBuf->sgt->nents, i) {
if (len <= ofs)
break;
l = sg->length;
l = (len < (ofs + l)) ? len - ofs : l;
vaddr_src = ioremap_cache(sg_phys(sg), l);
ret = copy_to_user((void __user *)((char *)pDst + ofs),
vaddr_src, l);
iounmap(vaddr_src);
if (ret) {
local_irq_restore(flags);
return -EFAULT;
}
ofs += l;
if (sg->offset) {
pr_debug("@@!! %s.%d: sgl[].offset:%lu\n",
__func__, __LINE__, sg->offset);
}
}
local_irq_restore(flags);
if (ofs != len) {
pr_debug("@@!! %s.%d: copied only %lu out of %zu\n",
__func__, __LINE__, ofs, len);
}
copied = ofs;
}
return copied;
}
static int scrncapt_get_info_head(struct tegra_dc *dc,
void __user *ptr)
{
int err = 0;
struct tegra_dc_ext_scrncapt_get_info_head info;
if (copy_from_user(&info, ptr, sizeof(info)))
return -EFAULT;
info.sts_en = dc->enabled;
info.hres = dc->mode.h_active;
info.vres = dc->mode.v_active;
info.flag_val_wins = dc->valid_windows;
if (copy_to_user(ptr, &info, sizeof(info)))
err = -EFAULT;
return err;
}
static int scrncapt_get_info_win(struct tegra_dc *dc, int winidx,
struct tegra_dc_ext_flip_windowattr *winattr)
{
int err = 0;
struct tegra_dc_ext *ext = dc->ext;
struct tegra_dc_win *win;
struct tegra_dc_ext_win *extwin;
int len, len_u, len_v;
win = tegra_dc_get_window(dc, winidx);
extwin = &ext->win[winidx];
memset(winattr, 0x0, sizeof(*winattr));
winattr->index = winidx;
len = scrncapt_get_dcbuf_len(extwin->cur_handle[TEGRA_DC_Y]);
len_u = scrncapt_get_dcbuf_len(extwin->cur_handle[TEGRA_DC_U]);
len_v = scrncapt_get_dcbuf_len(extwin->cur_handle[TEGRA_DC_V]);
/* hack to return buffer size instead of buff_id that needs
* unnecessary conversion */
winattr->buff_id = len;
winattr->buff_id_u = len_u;
winattr->buff_id_v = len_v;
winattr->offset = 0x0;
if (!len_u)
winattr->offset_u = win->phys_addr_u - win->phys_addr;
if (!len_v)
winattr->offset_v = win->phys_addr_v - win->phys_addr_u;
winattr->stride = win->stride;
winattr->stride_uv = win->stride_uv;
winattr->block_height_log2 = win->block_height_log2;
winattr->pixformat = win->fmt;
if (win->flags & TEGRA_WIN_FLAG_BLEND_PREMULT)
winattr->blend = TEGRA_DC_EXT_BLEND_PREMULT;
else if (win->flags & TEGRA_WIN_FLAG_BLEND_COVERAGE)
winattr->blend = TEGRA_DC_EXT_BLEND_COVERAGE;
if (win->flags & TEGRA_WIN_FLAG_TILED)
winattr->flags |= TEGRA_DC_EXT_FLIP_FLAG_TILED;
if (win->flags & TEGRA_WIN_FLAG_INVERT_H)
winattr->flags |= TEGRA_DC_EXT_FLIP_FLAG_INVERT_H;
if (win->flags & TEGRA_WIN_FLAG_INVERT_V)
winattr->flags |= TEGRA_DC_EXT_FLIP_FLAG_INVERT_V;
if (win->flags & TEGRA_WIN_FLAG_SCAN_COLUMN)
winattr->flags |= TEGRA_DC_EXT_FLIP_FLAG_SCAN_COLUMN;
if (255 != win->global_alpha)
winattr->flags |= TEGRA_DC_EXT_FLIP_FLAG_GLOBAL_ALPHA;
if (win->flags & TEGRA_WIN_FLAG_BLOCKLINEAR)
winattr->flags |= TEGRA_DC_EXT_FLIP_FLAG_BLOCKLINEAR;
if (win->flags & TEGRA_WIN_FLAG_INTERLACE)
winattr->flags |= TEGRA_DC_EXT_FLIP_FLAG_INTERLACE;
if (win->flags & TEGRA_WIN_FLAG_INPUT_RANGE_LIMITED)
winattr->flags |= TEGRA_DC_EXT_FLIP_FLAG_INPUT_RANGE_LIMITED;
else if (win->flags & TEGRA_WIN_FLAG_INPUT_RANGE_BYPASS)
winattr->flags |= TEGRA_DC_EXT_FLIP_FLAG_INPUT_RANGE_BYPASS;
if (win->flags & TEGRA_WIN_FLAG_CS_REC601)
winattr->flags |= TEGRA_DC_EXT_FLIP_FLAG_CS_REC601;
else if (win->flags & TEGRA_WIN_FLAG_CS_REC709)
winattr->flags |= TEGRA_DC_EXT_FLIP_FLAG_CS_REC709;
else if (win->flags & TEGRA_WIN_FLAG_CS_REC2020)
winattr->flags |= TEGRA_DC_EXT_FLIP_FLAG_CS_REC2020;
else if ((win->flags & TEGRA_WIN_FLAG_CS_MASK) ==
TEGRA_WIN_FLAG_CS_NONE)
winattr->flags |= TEGRA_DC_EXT_FLIP_FLAG_CS_NONE;
winattr->x = win->x.full;
winattr->y = win->y.full;
winattr->w = win->w.full;
winattr->h = win->h.full;
winattr->out_x = win->out_x;
winattr->out_y = win->out_y;
winattr->out_w = win->out_w;
winattr->out_h = win->out_h;
winattr->z = win->z;
winattr->global_alpha = win->global_alpha;
return err;
}
static int scrncapt_get_info_wins(struct tegra_dc *dc, void __user *ptr)
{
int err = 0;
int i;
struct tegra_dc_ext_scrncapt_get_info_win info;
struct tegra_dc_ext_flip_windowattr __user *pwinattr;
int num_wins;
if (copy_from_user(&info, ptr, sizeof(info)))
return -EFAULT;
if (!info.flag_wins || !info.wins)
return -EINVAL;
pwinattr = (struct tegra_dc_ext_flip_windowattr __user *)info.wins;
num_wins = 0;
for (i = 0; i < tegra_dc_get_numof_dispwindows(); i++) {
struct tegra_dc_ext_flip_windowattr winattr;
if (!(info.flag_wins & dc->valid_windows & (1 << i)))
continue;
scrncapt_get_info_win(dc, i, &winattr);
if (copy_to_user(pwinattr + num_wins, &winattr,
sizeof(winattr))) {
err = -EFAULT;
break;
}
num_wins++;
}
if (!err) {
info.num_wins = num_wins;
if (copy_to_user(ptr, &info, sizeof(info)))
err = -EFAULT;
}
return err;
}
static int scrncapt_get_info_cursor(struct tegra_dc *dc, void __user *ptr)
{
int err = 0;
struct tegra_dc_ext *ext = dc->ext;
struct tegra_dc_ext_cursor_image info = { {0} };
if (!ptr)
err = -EFAULT;
if (!err) {
u32 flags = 0x0;
switch (dc->cursor.size) {
case TEGRA_DC_CURSOR_SIZE_32X32:
flags |= TEGRA_DC_EXT_CURSOR_IMAGE_FLAGS_SIZE_32x32;
break;
case TEGRA_DC_CURSOR_SIZE_64X64:
flags |= TEGRA_DC_EXT_CURSOR_IMAGE_FLAGS_SIZE_64x64;
break;
case TEGRA_DC_CURSOR_SIZE_128X128:
flags |= TEGRA_DC_EXT_CURSOR_IMAGE_FLAGS_SIZE_128x128;
break;
case TEGRA_DC_CURSOR_SIZE_256X256:
flags |= TEGRA_DC_EXT_CURSOR_IMAGE_FLAGS_SIZE_256x256;
break;
default:
pr_warn("scrncapt: unknown dc->cursor.size:%d\n",
dc->cursor.size);
err = -EFAULT;
break;
}
switch (dc->cursor.colorfmt) {
case legacy:
flags |= TEGRA_DC_EXT_CURSOR_FLAGS_COLORFMT_LEGACY;
break;
case r8g8b8a8: /* normal */
flags |= TEGRA_DC_EXT_CURSOR_FLAGS_COLORFMT_R8G8B8A8;
break;
case a1r5g5b5:
flags |= TEGRA_DC_EXT_CURSOR_FLAGS_COLORFMT_A1R5G5B5;
break;
case a8r8g8b8:
flags |= TEGRA_DC_EXT_CURSOR_FLAGS_COLORFMT_A8R8G8B8;
break;
default:
pr_warn("scrncapt: unknown dc->cursor.colorfmt:%d\n",
dc->cursor.colorfmt);
err = -EFAULT;
break;
}
switch (dc->cursor.blendfmt) {
case TEGRA_DC_CURSOR_FORMAT_2BIT_LEGACY:
flags |= TEGRA_DC_EXT_CURSOR_FORMAT_FLAGS_2BIT_LEGACY;
break;
case TEGRA_DC_CURSOR_FORMAT_RGBA_NON_PREMULT_ALPHA:
flags |=
TEGRA_DC_EXT_CURSOR_FORMAT_FLAGS_RGBA_NON_PREMULT_ALPHA;
break;
case TEGRA_DC_CURSOR_FORMAT_RGBA_PREMULT_ALPHA:
flags |=
TEGRA_DC_EXT_CURSOR_FORMAT_FLAGS_RGBA_PREMULT_ALPHA;
break;
case TEGRA_DC_CURSOR_FORMAT_RGBA_XOR:
flags |= TEGRA_DC_EXT_CURSOR_FORMAT_FLAGS_RGBA_XOR;
break;
default:
pr_warn("scrncapt: unknown dc->cursor.blendfmt:%d\n",
dc->cursor.blendfmt);
err = -EFAULT;
break;
}
/* The cursor visivility flag TEGRA_DC_EXT_CURSOR_FLAGS_VISIBLE
* conflicts with TEGRA_DC_EXT_CURSOR_IMAGE_FLAGS_SIZE_32x32.
* Temporarily assign to bit31 for cursor visivility status. */
flags |= dc->cursor.enabled ? 1 << 31 : 0 << 31;
info.flags = flags;
/* hack to return buffer size instead of buff_id that needs
* unnecessary conversion */
info.buff_id = scrncapt_get_dcbuf_len(ext->cursor.cur_handle);
info.x = dc->cursor.x;
info.y = dc->cursor.y;
info.alpha = dc->cursor.alpha;
info.colorfmt = dc->cursor.colorfmt;
/* reverse of CURSOR_COLOR(r,g,b) macro */
info.foreground.r = (dc->cursor.fg >> 0) & 0xff;
info.foreground.g = (dc->cursor.fg >> 8) & 0xff;
info.foreground.b = (dc->cursor.fg >> 16) & 0xff;
info.background.r = (dc->cursor.bg >> 0) & 0xff;
info.background.g = (dc->cursor.bg >> 8) & 0xff;
info.background.b = (dc->cursor.bg >> 16) & 0xff;
}
if (copy_to_user(ptr, &info, sizeof(info)))
err = -EFAULT;
return err;
}
static int scrncapt_get_info_cursor_data(struct tegra_dc *dc,
void __user *ptr)
{
int err = 0;
struct tegra_dc_ext *ext = dc->ext;
struct tegra_dc_ext_scrncapt_get_info_cursor_data info;
struct tegra_dc_dmabuf *dcbuf;
void __user *pbuf;
size_t len, l = 0;
if (copy_from_user(&info, ptr, sizeof(info))) {
err = -EFAULT;
} else {
dcbuf = ext->cursor.cur_handle;
if (dcbuf) {
len = scrncapt_get_dcbuf_len(dcbuf);
if (info.size < len)
err = -ENOSPC;
else
/* verify user space is accessible */
if (!access_ok(VERIFY_WRITE, info.ptr, len))
err = -EFAULT;
}
if (!err && dcbuf) {
pbuf = (void __user *)info.ptr;
l = scrncapt_copy_dcbuf(pbuf, dcbuf, len);
if (l != len)
err = -EIO;
}
info.len = l;
if (copy_to_user(ptr, &info, sizeof(info)))
err = -EFAULT;
}
return err;
}
int tegra_dc_scrncapt_get_info(struct tegra_dc_ext_user *user,
struct tegra_dc_ext_scrncapt_get_info *args)
{
int err = 0;
int i;
struct tegra_dc_ext *ext = user->ext;
struct tegra_dc *dc = ext->dc;
struct tegra_dc_ext_scrncapt_get_info_data __user *pdt;
struct tegra_dc_ext_scrncapt_get_info_data data;
/* no support of 1st implementation */
if (TEGRA_DC_EXT_SCRNCAPT_VER_V(args->ver) != 2)
return -EFAULT;
if ((args->ver ^ scrncapt.magic) & ~0xff)
return -EFAULT;
if (tegra_dc_get_numof_dispheads() <= (unsigned)args->head)
return -EFAULT;
/* check disp paused */
mutex_lock(&scrncapt.lock);
if (!(scrncapt.pause_heads & (1 << dc->ctrl_num)))
err = -EINVAL;
mutex_unlock(&scrncapt.lock);
if (err)
return err;
pdt = (struct tegra_dc_ext_scrncapt_get_info_data __user *)args->data;
for (i = 0; i < args->num_data; i++) {
if (copy_from_user(&data, pdt + i, sizeof(data))) {
err = -EFAULT;
break;
}
switch (data.type) {
case TEGRA_DC_EXT_SCRNCAPT_GET_INFO_TYPE_HEAD:
scrncapt_get_info_head(dc, (void *)data.ptr);
break;
case TEGRA_DC_EXT_SCRNCAPT_GET_INFO_TYPE_WINS:
scrncapt_get_info_wins(dc, (void *)data.ptr);
break;
case TEGRA_DC_EXT_SCRNCAPT_GET_INFO_TYPE_CURSOR:
scrncapt_get_info_cursor(dc, (void *)data.ptr);
break;
case TEGRA_DC_EXT_SCRNCAPT_GET_INFO_TYPE_CURSOR_DATA:
scrncapt_get_info_cursor_data(dc, (void *)data.ptr);
break;
/* TODO: add CMU & CSC capture.
* these information are not used at this time. */
case TEGRA_DC_EXT_SCRNCAPT_GET_INFO_TYPE_NVDISP_CMU:
case TEGRA_DC_EXT_SCRNCAPT_GET_INFO_TYPE_NVDISP_WIN_CSC:
default:
pr_info("scrncapt: data type %d not implemented yet\n",
data.type);
err = -EINVAL;
break;
}
if (err)
break;
}
args->ver = TEGRA_DC_EXT_SCRNCAPT_VER_2(args->ver);
return err;
}
int tegra_dc_scrncapt_dup_fbuf(struct tegra_dc_ext_user *user,
struct tegra_dc_ext_scrncapt_dup_fbuf *args)
{
int err = 0;
int p;
struct tegra_dc_ext *ext = user->ext;
struct tegra_dc *dc = ext->dc;
struct tegra_dc_win *win;
struct tegra_dc_ext_win *extwin;
u8 *dest;
int ofs;
/* no support of 1st implementation */
if (TEGRA_DC_EXT_SCRNCAPT_VER_V(args->ver) != 2)
return -EFAULT;
if ((args->ver ^ scrncapt.magic) & ~0xff)
return -EFAULT;
if ((tegra_dc_get_numof_dispheads() <= (unsigned)args->head) ||
(tegra_dc_get_numof_dispwindows() <= (unsigned)args->win))
return -EINVAL;
if (!access_ok(VERIFY_WRITE, args->buffer, args->buffer_max))
return -EFAULT;
/* check disp paused & valid window */
mutex_lock(&scrncapt.lock);
if (!(scrncapt.pause_heads & (1 << dc->ctrl_num)))
err = -EINVAL;
mutex_unlock(&scrncapt.lock);
if (!(dc->valid_windows & (1 << args->win)))
err = -EBUSY;
if (err)
return err;
win = tegra_dc_get_window(dc, args->win);
extwin = &ext->win[args->win];
dest = (u8 *)args->buffer;
ofs = 0;
for (p = 0; p < TEGRA_DC_NUM_PLANES; p++) {
struct tegra_dc_dmabuf *buf;
size_t len, l;
buf = extwin->cur_handle[p];
if (!buf) {
args->plane_sizes[p] = 0;
} else {
len = scrncapt_get_dcbuf_len(buf);
if (args->buffer_max < ofs + len) {
err = -ENOSPC;
break;
}
l = scrncapt_copy_dcbuf((void __user *)(dest + ofs),
buf, len);
if (l != len) {
err = -EIO;
break;
}
args->plane_sizes[p] = len;
args->plane_offsets[p] = ofs;
ofs += len;
ofs = (ofs + (8 - 1)) & ~(8 - 1);
}
}
args->ver = TEGRA_DC_EXT_SCRNCAPT_VER_2(args->ver);
return err;
}
int tegra_dc_scrncapt_pause(struct tegra_dc_ext_control_user *ctlusr,
struct tegra_dc_ext_control_scrncapt_pause *args)
{
int err = 0;
int i, nheads;
u32 heads;
unsigned long tm;
if (TEGRA_DC_EXT_CONTROL_SCRNCAPT_MAGIC != args->magic)
return -EFAULT;
nheads = tegra_dc_get_numof_dispheads();
heads = 1 << 31; /* default to pausing all heads */
if ((1 << 31) & heads)
heads |= -1;
tm = (-1 == args->tm_resume_msec) ? 0 :
(args->tm_resume_msec ?
args->tm_resume_msec * HZ / 1000 + 1 : HZ / 2);
mutex_lock(&scrncapt.lock);
if (scrncapt.pause_heads) {
err = -EBUSY;
} else {
for (i = 0; i < nheads; i++) {
if ((1 << i) & heads)
down_write(&scrncapt.rwsema_head[i]);
}
scrncapt.pause_heads = heads;
scrncapt.holder_pid = current->pid;
scrncapt.magic = args->magic ^ (jiffies << 8);
/* set-up a timer to limit the disp pausing time */
scrncapt.tm_resume = tm;
if (tm) {
scrncapt.tmr_resume.expires = jiffies + tm;
add_timer(&scrncapt.tmr_resume);
}
}
mutex_unlock(&scrncapt.lock);
if (!err)
pr_info("scrncapt: disp paused, timer-set:%lu\n",
tm * 1000 / HZ);
args->magic = scrncapt.magic;
args->tm_resume_msec = tm ? : -1;
args->num_heads = nheads;
args->num_wins = tegra_dc_get_numof_dispwindows();
return err;
}
int tegra_dc_scrncapt_resume(struct tegra_dc_ext_control_user *ctlusr,
struct tegra_dc_ext_control_scrncapt_resume *args)
{
int err = 0;
int i;
u32 heads;
if (args->magic != scrncapt.magic)
return -EFAULT;
mutex_lock(&scrncapt.lock);
if (!scrncapt.pause_heads) {
err = -EINVAL;
} else {
if (scrncapt.tm_resume)
del_timer(&scrncapt.tmr_resume);
heads = scrncapt.pause_heads;
scrncapt.pause_heads = 0x0;
scrncapt.holder_pid = 0x0;
if ((1 << 31) & heads)
heads |= -1;
for (i = 0; i < tegra_dc_get_numof_dispheads(); i++) {
if ((1 << i) & heads)
up_write(&scrncapt.rwsema_head[i]);
}
}
mutex_unlock(&scrncapt.lock);
if (!err)
pr_info("scrncapt: disp resumed\n");
return err;
}
/* timer call-back
* to resume display automatically after timeout
*/
static void tegra_dc_scrncapt_timer_cb(unsigned long arg)
{
int i;
u32 heads;
heads = scrncapt.pause_heads;
scrncapt.pause_heads = 0x0;
scrncapt.holder_pid = 0x0;
if ((1 << 31) & heads)
heads |= -1;
for (i = 0; i < tegra_dc_get_numof_dispheads(); i++) {
if ((1 << i) & heads)
up_write(&scrncapt.rwsema_head[i]);
}
pr_info("scrncapt: pause timeout, auto resuming.\n");
}
int tegra_dc_scrncapt_init(void)
{
int i, nheads, nwins;
nheads = tegra_dc_get_numof_dispheads();
nwins = tegra_dc_get_numof_dispwindows();
if (nheads <= 0 || nwins <= 0) {
pr_err("%s: heads:%d windows:%d cannot be negative or zero\n",
__func__, nheads, nwins);
return -EINVAL;
}
memset(&scrncapt, 0, sizeof(scrncapt));
mutex_init(&scrncapt.lock);
scrncapt.rwsema_head = kzalloc(nheads *
sizeof(struct rw_semaphore), GFP_KERNEL);
if (IS_ERR_OR_NULL(scrncapt.rwsema_head)) {
pr_err("%s: Insufficient memory\n", __func__);
return -ENOMEM;
}
pr_info("scrncapt: init (heads:%d wins:%d planes:%d)\n",
nheads, nwins, TEGRA_DC_NUM_PLANES);
for (i = 0; i < nheads; i++)
init_rwsem(&scrncapt.rwsema_head[i]);
init_timer(&scrncapt.tmr_resume);
scrncapt.tmr_resume.function = &tegra_dc_scrncapt_timer_cb;
scrncapt.tmr_resume.data = (unsigned long)&scrncapt;
scrncapt.magic = TEGRA_DC_EXT_CONTROL_SCRNCAPT_MAGIC ^ (jiffies << 8);
return 0;
}
int tegra_dc_scrncapt_exit(void)
{
pr_info("scrncapt: exit\n");
kfree(scrncapt.rwsema_head);
return 0;
}